Skip to content

Commit 8a165b8

Browse files
authored
Merge pull request #1912 from GoogleContainerTools/validate_merged_usr
feat: validate actions for merged-usr violations
2 parents 406690b + ea6c116 commit 8a165b8

7 files changed

Lines changed: 207 additions & 6 deletions

File tree

.bazelrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ build --java_runtime_version=remotejdk_11
2626
# https://github.com/GoogleContainerTools/rules_distroless/actions/runs/7118944984/job/19382981899?pr=9#step:8:51
2727
common:linux --sandbox_tmpfs_path=/tmp
2828

29+
30+
# Attach validation actions to catch merged-usr convention errors in image layers.
31+
# The aspect uses attr_aspects = ["tars"], so it propagates along the tars dependency
32+
# chain of every oci_image target: oci_image -> pkg_tar/tar -> individual packages.
33+
# Bazel automatically requests the _validation output group at the end of the build.
34+
common --aspects=//private/util:validate_usr_symlinks.bzl%validate_usr_symlinks
35+
2936
# Load any settings specific to the current user.
3037
# .bazelrc.user should appear in .gitignore so that settings are not shared with team members
3138
# This needs to be last statement in this

MODULE.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ bazel_dep(name = "gazelle", version = "0.38.0")
1111
bazel_dep(name = "rules_rust", version = "0.63.0")
1212
bazel_dep(name = "container_structure_test", version = "1.19.1")
1313
bazel_dep(name = "rules_oci", version = "2.2.7")
14-
bazel_dep(name = "rules_distroless", version = "0.6.2")
14+
bazel_dep(name = "rules_distroless", version = "0.8.0")
1515
bazel_dep(name = "rules_python", version = "1.5.3")
1616
bazel_dep(name = "rules_cc", version = "0.2.4")
17+
bazel_dep(name = "gawk", version = "5.3.2.bcr.5")
1718

1819
### OCI ###
1920
# Note: rules_oci registers toolchains in its MODULE.bazel

MODULE.bazel.lock

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

private/util/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
exports_files(["validate_usr_symlinks.awk"])
2+
3+
sh_test(
4+
name = "validate_usr_symlinks_test",
5+
srcs = ["validate_usr_symlinks_test.sh"],
6+
data = [
7+
"validate_usr_symlinks.awk",
8+
"@bazel_tools//tools/bash/runfiles",
9+
"@gawk",
10+
],
11+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
BEGIN {
2+
# Mapping from root-level path to expected symlink destination.
3+
# https://github.com/floppym/merge-usr/blob/15dd02207bdee7ca6720d7024e8c0ffdc166ed23/merge-usr#L17-L25
4+
# Note: Debian does NOT merge /usr/sbin into /usr/bin, so /sbin -> usr/sbin.
5+
expected["bin"] = "usr/bin"
6+
expected["sbin"] = "usr/sbin"
7+
expected["lib"] = "usr/lib"
8+
expected["lib32"] = "usr/lib32"
9+
expected["lib64"] = "usr/lib64"
10+
expected["libx32"] = "usr/libx32"
11+
prefixes = "bin|sbin|lib|lib32|lib64|libx32"
12+
}
13+
{
14+
original_path = $1
15+
path = original_path
16+
# Normalize: strip leading ./ or /
17+
sub(/^\.\//, "", path)
18+
sub(/^\//, "", path)
19+
20+
if (path in expected) {
21+
if ($0 !~ /type=link/) {
22+
VIOLATIONS[original_path] = original_path " is not a symlink (must link to " expected[path] ")"
23+
} else if (match($0, / link=([^ \t]+)/, dest) && dest[1] != expected[path]) {
24+
VIOLATIONS[original_path] = original_path " symlinks to '" dest[1] "' instead of '" expected[path] "'"
25+
}
26+
} else if (path ~ ("^(" prefixes ")/")) {
27+
VIOLATIONS[original_path] = original_path " found under a merged-usr symlink path (should not exist)"
28+
}
29+
}
30+
END {
31+
for (violation in VIOLATIONS) {
32+
print "VIOLATION: " VIOLATIONS[violation] " violates usr-merge convention."
33+
print violation
34+
}
35+
if (length(VIOLATIONS) > 0) {
36+
exit 1
37+
}
38+
print "" > validation_output_file
39+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"Bazel aspect to validate merged-usr conventions in tar files."
2+
3+
load("@aspect_bazel_lib//lib:tar.bzl", "tar_lib")
4+
5+
# https://wiki.gentoo.org/wiki/Merge-usr
6+
# https://salsa.debian.org/md/usrmerge/raw/master/debian/README.Debian
7+
# https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge/
8+
# Mapping taken from https://github.com/floppym/merge-usr/blob/15dd02207bdee7ca6720d7024e8c0ffdc166ed23/merge-usr#L17-L25
9+
# https://salsa.debian.org/md/usrmerge/-/tree/master/debian?ref_type=heads
10+
11+
def _validate_usr_symlink_impl(target, ctx):
12+
if target.label.name.find("debian12") != -1:
13+
return []
14+
15+
if not hasattr(ctx.rule.files, "tars"):
16+
return []
17+
bsdtar = ctx.toolchains[tar_lib.toolchain_type]
18+
19+
output = ctx.actions.declare_file(target.label.name + ".mtree")
20+
21+
args = ctx.actions.args()
22+
args.add("--create")
23+
args.add("--file", output)
24+
args.add("--format=mtree")
25+
args.add_all(ctx.rule.files.tars, format_each = "@%s")
26+
27+
ctx.actions.run(
28+
executable = bsdtar.tarinfo.binary,
29+
inputs = ctx.rule.files.tars,
30+
outputs = [output],
31+
tools = bsdtar.default.files,
32+
arguments = [args],
33+
mnemonic = "PackageListing",
34+
)
35+
36+
validation_output = ctx.actions.declare_file(target.label.name + ".validation")
37+
ctx.actions.run(
38+
executable = ctx.executable._awk,
39+
inputs = [output, ctx.file._validate_symlinks],
40+
outputs = [validation_output],
41+
arguments = [
42+
"-v",
43+
"validation_output_file=" + validation_output.path,
44+
"-f",
45+
ctx.file._validate_symlinks.path,
46+
output.path,
47+
],
48+
mnemonic = "ValidateUsrSymlinks",
49+
)
50+
51+
return [
52+
OutputGroupInfo(_validation = depset([validation_output])),
53+
]
54+
55+
validate_usr_symlinks = aspect(
56+
implementation = _validate_usr_symlink_impl,
57+
attrs = {
58+
"_awk": attr.label(default = "@gawk//:gawk", cfg = "exec", executable = True),
59+
"_validate_symlinks": attr.label(
60+
default = "//private/util:validate_usr_symlinks.awk",
61+
allow_single_file = True,
62+
),
63+
},
64+
attr_aspects = ["tars"],
65+
toolchains = [tar_lib.toolchain_type],
66+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Bazel runfiles resolution
5+
# shellcheck source=/dev/null
6+
source "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" 2>/dev/null ||
7+
source "$(dirname "$0")/../bazel_tools/tools/bash/runfiles/runfiles.bash" 2>/dev/null ||
8+
{ echo >&2 "ERROR: cannot find runfiles.bash"; exit 1; }
9+
10+
GAWK="$(rlocation gawk/gawk)"
11+
AWK_SCRIPT="$(rlocation distroless/private/util/validate_usr_symlinks.awk)"
12+
13+
run() {
14+
printf '%s\n' "$1" | "$GAWK" -v validation_output_file=/dev/null -f "$AWK_SCRIPT"
15+
}
16+
17+
fail() { echo "FAIL: $*" >&2; exit 1; }
18+
19+
# --- passing cases ---
20+
21+
run "./bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
22+
|| fail "./bin -> usr/bin should pass"
23+
24+
run "/bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
25+
|| fail "/bin -> usr/bin should pass"
26+
27+
run "bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
28+
|| fail "bin -> usr/bin should pass"
29+
30+
run "./sbin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/sbin" \
31+
|| fail "./sbin -> usr/sbin should pass"
32+
33+
run "./lib type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib" \
34+
|| fail "./lib -> usr/lib should pass"
35+
36+
run "./lib32 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib32" \
37+
|| fail "./lib32 -> usr/lib32 should pass"
38+
39+
run "./lib64 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib64" \
40+
|| fail "./lib64 -> usr/lib64 should pass"
41+
42+
run "./lib64 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib64
43+
./usr/bin/ls type=file mode=0755 nlink=1 uid=0 gid=0 size=12345" \
44+
|| fail "content under ./usr/ alongside valid symlinks should pass"
45+
46+
# --- failing cases ---
47+
48+
run "./bin type=dir mode=0755 nlink=2 uid=0 gid=0" \
49+
&& fail "./bin as a directory should fail" || true
50+
51+
run "./bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/sbin" \
52+
&& fail "./bin -> usr/sbin should fail" || true
53+
54+
run "bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/fin" \
55+
&& fail "bin -> usr/fin should fail" || true
56+
57+
run "/bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/fin" \
58+
&& fail "/bin -> usr/fin should fail" || true
59+
60+
run "./sbin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
61+
&& fail "./sbin -> usr/bin should fail (Debian keeps sbin separate)" || true
62+
63+
run "./lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
64+
&& fail "content under ./lib/ should fail" || true
65+
66+
run "lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
67+
&& fail "content under lib/ should fail" || true
68+
69+
run "/lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
70+
&& fail "content under /lib/ should fail" || true
71+
72+
run "./bin/ls type=file mode=0755 nlink=1 uid=0 gid=0 size=12345" \
73+
&& fail "content under ./bin/ should fail" || true
74+
75+
echo "All tests passed."

0 commit comments

Comments
 (0)