Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ build --java_runtime_version=remotejdk_11
# https://github.com/GoogleContainerTools/rules_distroless/actions/runs/7118944984/job/19382981899?pr=9#step:8:51
common:linux --sandbox_tmpfs_path=/tmp


# Attach validation actions to catch merged-usr convention errors in image layers.
# The aspect uses attr_aspects = ["tars"], so it propagates along the tars dependency
# chain of every oci_image target: oci_image -> pkg_tar/tar -> individual packages.
# Bazel automatically requests the _validation output group at the end of the build.
common --aspects=//private/util:validate_usr_symlinks.bzl%validate_usr_symlinks
Comment thread
loosebazooka marked this conversation as resolved.

# Load any settings specific to the current user.
# .bazelrc.user should appear in .gitignore so that settings are not shared with team members
# This needs to be last statement in this
Expand Down
3 changes: 2 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ bazel_dep(name = "gazelle", version = "0.38.0")
bazel_dep(name = "rules_rust", version = "0.63.0")
bazel_dep(name = "container_structure_test", version = "1.19.1")
bazel_dep(name = "rules_oci", version = "2.2.7")
bazel_dep(name = "rules_distroless", version = "0.6.2")
bazel_dep(name = "rules_distroless", version = "0.8.0")
bazel_dep(name = "rules_python", version = "1.5.3")
bazel_dep(name = "rules_cc", version = "0.2.4")
bazel_dep(name = "gawk", version = "5.3.2.bcr.5")

### OCI ###
# Note: rules_oci registers toolchains in its MODULE.bazel
Expand Down
12 changes: 7 additions & 5 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions private/util/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports_files(["validate_usr_symlinks.awk"])

sh_test(
name = "validate_usr_symlinks_test",
srcs = ["validate_usr_symlinks_test.sh"],
data = [
"validate_usr_symlinks.awk",
"@bazel_tools//tools/bash/runfiles",
"@gawk",
],
)
39 changes: 39 additions & 0 deletions private/util/validate_usr_symlinks.awk
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
BEGIN {
# Mapping from root-level path to expected symlink destination.
# https://github.com/floppym/merge-usr/blob/15dd02207bdee7ca6720d7024e8c0ffdc166ed23/merge-usr#L17-L25
# Note: Debian does NOT merge /usr/sbin into /usr/bin, so /sbin -> usr/sbin.
expected["bin"] = "usr/bin"
expected["sbin"] = "usr/sbin"
expected["lib"] = "usr/lib"
expected["lib32"] = "usr/lib32"
expected["lib64"] = "usr/lib64"
expected["libx32"] = "usr/libx32"
prefixes = "bin|sbin|lib|lib32|lib64|libx32"
}
{
original_path = $1
path = original_path
# Normalize: strip leading ./ or /
sub(/^\.\//, "", path)
sub(/^\//, "", path)

if (path in expected) {
if ($0 !~ /type=link/) {
VIOLATIONS[original_path] = original_path " is not a symlink (must link to " expected[path] ")"
} else if (match($0, / link=([^ \t]+)/, dest) && dest[1] != expected[path]) {
VIOLATIONS[original_path] = original_path " symlinks to '" dest[1] "' instead of '" expected[path] "'"
}
} else if (path ~ ("^(" prefixes ")/")) {
VIOLATIONS[original_path] = original_path " found under a merged-usr symlink path (should not exist)"
}
}
Comment thread
thesayyn marked this conversation as resolved.
END {
for (violation in VIOLATIONS) {
print "VIOLATION: " VIOLATIONS[violation] " violates usr-merge convention."
print violation
}
if (length(VIOLATIONS) > 0) {
exit 1
}
print "" > validation_output_file
}
66 changes: 66 additions & 0 deletions private/util/validate_usr_symlinks.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"Bazel aspect to validate merged-usr conventions in tar files."

load("@aspect_bazel_lib//lib:tar.bzl", "tar_lib")

# https://wiki.gentoo.org/wiki/Merge-usr
# https://salsa.debian.org/md/usrmerge/raw/master/debian/README.Debian
# https://www.freedesktop.org/wiki/Software/systemd/TheCaseForTheUsrMerge/
# Mapping taken from https://github.com/floppym/merge-usr/blob/15dd02207bdee7ca6720d7024e8c0ffdc166ed23/merge-usr#L17-L25
# https://salsa.debian.org/md/usrmerge/-/tree/master/debian?ref_type=heads

def _validate_usr_symlink_impl(target, ctx):
if target.label.name.find("debian12") != -1:
return []

if not hasattr(ctx.rule.files, "tars"):
return []
bsdtar = ctx.toolchains[tar_lib.toolchain_type]

output = ctx.actions.declare_file(target.label.name + ".mtree")

args = ctx.actions.args()
args.add("--create")
args.add("--file", output)
args.add("--format=mtree")
args.add_all(ctx.rule.files.tars, format_each = "@%s")

ctx.actions.run(
executable = bsdtar.tarinfo.binary,
inputs = ctx.rule.files.tars,
outputs = [output],
tools = bsdtar.default.files,
arguments = [args],
mnemonic = "PackageListing",
)

validation_output = ctx.actions.declare_file(target.label.name + ".validation")
ctx.actions.run(
executable = ctx.executable._awk,
inputs = [output, ctx.file._validate_symlinks],
outputs = [validation_output],
arguments = [
"-v",
"validation_output_file=" + validation_output.path,
"-f",
ctx.file._validate_symlinks.path,
output.path,
],
Comment thread
thesayyn marked this conversation as resolved.
mnemonic = "ValidateUsrSymlinks",
)

return [
OutputGroupInfo(_validation = depset([validation_output])),
]

validate_usr_symlinks = aspect(
implementation = _validate_usr_symlink_impl,
attrs = {
"_awk": attr.label(default = "@gawk//:gawk", cfg = "exec", executable = True),
"_validate_symlinks": attr.label(
default = "//private/util:validate_usr_symlinks.awk",
allow_single_file = True,
),
},
attr_aspects = ["tars"],
toolchains = [tar_lib.toolchain_type],
)
75 changes: 75 additions & 0 deletions private/util/validate_usr_symlinks_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail

# Bazel runfiles resolution
# shellcheck source=/dev/null
source "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" 2>/dev/null ||
source "$(dirname "$0")/../bazel_tools/tools/bash/runfiles/runfiles.bash" 2>/dev/null ||
{ echo >&2 "ERROR: cannot find runfiles.bash"; exit 1; }

GAWK="$(rlocation gawk/gawk)"
AWK_SCRIPT="$(rlocation distroless/private/util/validate_usr_symlinks.awk)"

run() {
printf '%s\n' "$1" | "$GAWK" -v validation_output_file=/dev/null -f "$AWK_SCRIPT"
}

fail() { echo "FAIL: $*" >&2; exit 1; }

# --- passing cases ---
Comment thread
thesayyn marked this conversation as resolved.

run "./bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
|| fail "./bin -> usr/bin should pass"

run "/bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
|| fail "/bin -> usr/bin should pass"

run "bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
|| fail "bin -> usr/bin should pass"

run "./sbin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/sbin" \
|| fail "./sbin -> usr/sbin should pass"

run "./lib type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib" \
|| fail "./lib -> usr/lib should pass"

run "./lib32 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib32" \
|| fail "./lib32 -> usr/lib32 should pass"

run "./lib64 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib64" \
|| fail "./lib64 -> usr/lib64 should pass"

run "./lib64 type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/lib64
./usr/bin/ls type=file mode=0755 nlink=1 uid=0 gid=0 size=12345" \
|| fail "content under ./usr/ alongside valid symlinks should pass"

# --- failing cases ---

Comment thread
thesayyn marked this conversation as resolved.
run "./bin type=dir mode=0755 nlink=2 uid=0 gid=0" \
&& fail "./bin as a directory should fail" || true

run "./bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/sbin" \
&& fail "./bin -> usr/sbin should fail" || true

run "bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/fin" \
&& fail "bin -> usr/fin should fail" || true

run "/bin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/fin" \
&& fail "/bin -> usr/fin should fail" || true

run "./sbin type=link mode=0777 nlink=1 uid=0 gid=0 link=usr/bin" \
&& fail "./sbin -> usr/bin should fail (Debian keeps sbin separate)" || true

run "./lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
&& fail "content under ./lib/ should fail" || true

Comment thread
thesayyn marked this conversation as resolved.
run "lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
&& fail "content under lib/ should fail" || true

run "/lib/libfoo.so.1 type=file mode=0644 nlink=1 uid=0 gid=0 size=4096" \
&& fail "content under /lib/ should fail" || true

run "./bin/ls type=file mode=0755 nlink=1 uid=0 gid=0 size=12345" \
&& fail "content under ./bin/ should fail" || true

echo "All tests passed."
Loading