From 627edcfcb880a0a43855870b7fa41654ad727787 Mon Sep 17 00:00:00 2001 From: navigator Date: Thu, 8 Oct 2026 01:09:12 +0000 Subject: [PATCH] fix: a delta layer is the final tree's diff against its parent bt-layer packed a child layer as the two deck upper directories one after the other, root.build's then root.patched's. A file the first stage created and the second deleted is content in one and a whiteout in the other, so the tarball carried its bytes and then the whiteout: the tree a layer unpacks to was right and the bytes were still published. That is how the postgresql layer of 2026-10-08 carried the snakeoil key and certificate ssl-cert makes at installation and removelists-final takes out; layer_ audit_keys reads the final tree, where the whiteout had already hidden them, so it passed. layer_delta_lists computes the members from the final tree and the parent instead: every path either upper holds that root.patched still has, packed from root.patched; a whiteout only for a path the parent has and the final tree does not; an opaque directory over a parent directory from its upper, so the mark is kept. A file created and deleted inside the layer leaves nothing. The lists are NUL separated and sorted, and tar reads them with --null --no-recursion, so the tarball stays reproducible (the -u re-export test is byte identical). bt-layer passes the parent's rootfs and fails when the members cannot be listed. tests/layer: a fake key created in stage 1 and whited out in stage 2 is neither named nor present byte for byte in a real tarball; a parent file removed is a whiteout; a file removed by stage 1 and re-created by stage 2 is packed once; an opaque directory comes from its upper; the refusals; the real predicates on what an unprivileged run can make. The fake product's final tree now holds what its uppers add, as an overlay mount shows it. Opaque marks are read or the build stops: layer_is_opaque returns 2 when getfattr is missing or fails with anything but "No such attribute", and layer_delta_lists refuses to run without getfattr (package attr, now in docs/setup and in CI). Before, a getfattr error read as "not opaque" and a parent file the layer deleted came back. No test of the lists follows a symlink: layer_exists_in and layer_real_dir_in walk REL component by component with lstat semantics, so a stage 1 file under a directory stage 2 replaced with a link to the build host is not packed through the link, and a parent link is neither a whiteout target nor a directory to be opaque over. --- .github/workflows/tests.yml | 5 +- bin/layer-lib | 156 +++++++++++++++++++++++++++++++++++- bt-layer | 13 ++- docs/setup | 5 ++ tests/layer | 150 +++++++++++++++++++++++++++++++++- 5 files changed, 318 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f01a320..a0bbd81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,5 +11,6 @@ jobs: uses: keel-linux/.github/.github/workflows/test-shell.yml@main with: threshold: 99 - # tests/machine-id makes small ISOs, as fab does - apt-packages: squashfs-tools xorriso + # tests/machine-id makes small ISOs, as fab does; bt-layer reads + # overlayfs opaque marks with getfattr + apt-packages: squashfs-tools xorriso attr diff --git a/bin/layer-lib b/bin/layer-lib index c3d6595..b8518e8 100755 --- a/bin/layer-lib +++ b/bin/layer-lib @@ -469,16 +469,168 @@ layer_tar_flags() { done } +# layer_is_whiteout PATH: an overlayfs whiteout, a character device 0:0. +layer_is_whiteout() { + [[ -c "$1" && ! -L "$1" ]] && [[ "$(stat -c '%t:%T' "$1")" == 0:0 ]] +} + +# layer_is_opaque PATH: a directory overlayfs marks opaque, which hides +# every entry of the lower directory. Status 0 opaque, 1 not, 2 the mark +# could not be read: getfattr (package attr) missing or failing is an error +# and never reads as "not opaque", or a directory that hid its parent's +# files would let them back into the image. Reading trusted.* needs root, +# which bt-layer has. +layer_is_opaque() { + local path=$1 + local out rc=0 + [[ -d "$path" && ! -L "$path" ]] || return 1 + local getfattr=${LAYER_GETFATTR:-getfattr} + out=$(LC_ALL=C "$getfattr" --absolute-names --only-values -n trusted.overlay.opaque -- "$path" 2>&1) || rc=$? + if [[ $rc -eq 0 ]]; then + [[ "$out" == y ]] + return + fi + [[ $rc -eq 1 && "$out" == *"No such attribute"* ]] && return 1 + layer_error "cannot read the overlay opaque mark of $path (status $rc): $out" + return 2 +} + +# layer_exists PATH: there is an entry at PATH, a dangling symlink included. +layer_exists() { + [[ -e "$1" || -L "$1" ]] +} + +# layer_real_dir_in ROOT REL: every component of REL under ROOT is a +# directory and none is a symlink. A tree's absolute symlinks point at the +# build host, so a test that follows one answers for the host, not the tree. +layer_real_dir_in() { + local path=$1 + local rel=${2#./} + local part + [[ "$rel" == . ]] && rel= + [[ -d "$path" && ! -L "$path" ]] || return 1 + while [[ -n "$rel" ]]; do + part=${rel%%/*} + path+=/$part + [[ -d "$path" && ! -L "$path" ]] || return 1 + if [[ "$rel" == */* ]]; then rel=${rel#*/}; else rel=; fi + done +} + +# layer_exists_in ROOT REL: ROOT/REL is an entry of the tree under ROOT, a +# dangling symlink included, reached without following a symlink on the way +# (a later stage that replaced a directory with a link leaves the earlier +# stage's files under it unreachable, not present). +layer_exists_in() { + local root=$1 + local rel=${2#./} + [[ "$rel" == . || -z "$rel" ]] && { layer_real_dir_in "$root" .; return; } + if [[ "$rel" == */* ]]; then + layer_real_dir_in "$root" "${rel%/*}" || return 1 + else + layer_real_dir_in "$root" . || return 1 + fi + layer_exists "$root/$rel" +} + +# layer_delta_lists BUILD_DIR PARENT_ROOTFS LISTS: the members of a delta +# layer, as NUL separated lists under LISTS, computed from the final tree and +# the parent rather than taken from the deck's upper directories as they +# are. +# +# Both stages of a build write an upper directory: root.build (the packages) +# and root.patched (overlays, conf scripts, removelists). A file the first +# stage created and the second deleted is in root.build's upper with its +# content and in root.patched's as a whiteout; packing both uppers shipped +# the content and then the whiteout, so the tree a layer unpacks to was +# right and the bytes were still in the tarball (the snakeoil key ssl-cert +# makes, which removelists-final takes out). So: +# +# present every path either upper holds that the final tree, +# root.patched, still has, packed from root.patched +# whiteouts.STAGE a whiteout of that stage for a path the parent has and +# the final tree does not, packed from that upper (a +# whiteout for a path the parent never had hides nothing +# and is dropped, and with it everything the layer made +# and removed again) +# opaque.STAGE a directory of that stage marked opaque over a +# directory of the parent, packed from that upper so the +# mark is kept; root.patched's wins over root.build's +# +# The lists are sorted, so the tarball does not depend on find's order. +layer_delta_lists() { + local build_dir=$1 + local parent=$2 + local lists=$3 + local merged=$build_dir/root.patched + local stage upper rel opaque + [[ -d "$merged" ]] || { layer_error "no final tree at $merged"; return 1; } + [[ -d "$parent" ]] || { layer_error "no parent tree at $parent"; return 1; } + if ! command -v "${LAYER_GETFATTR:-getfattr}" >/dev/null; then + layer_error "getfattr (package attr) not available, cannot tell opaque directories" + return 1 + fi + mkdir -p "$lists" + : > "$lists/present.unsorted" + for stage in root.build root.patched; do + upper=$build_dir/.deck/$stage/upper + [[ -d "$upper" ]] || { layer_error "no upper directory at $upper"; return 1; } + : > "$lists/whiteouts.$stage" + : > "$lists/opaque.$stage" + (cd "$upper" && find . -print0) > "$lists/found.$stage" + while IFS= read -r -d '' rel; do + if layer_is_whiteout "$upper/$rel"; then + layer_exists_in "$merged" "$rel" && continue + layer_exists_in "$parent" "$rel" || continue + printf '%s\0' "$rel" >> "$lists/whiteouts.$stage" + elif layer_exists_in "$merged" "$rel"; then + opaque=0 + layer_is_opaque "$upper/$rel" || opaque=$? + [[ $opaque -lt 2 ]] || return 1 + if [[ $opaque -eq 0 ]] && layer_real_dir_in "$parent" "$rel"; then + printf '%s\0' "$rel" >> "$lists/opaque.$stage" + else + printf '%s\0' "$rel" >> "$lists/present.unsorted" + fi + fi + done < "$lists/found.$stage" + rm -f "$lists/found.$stage" + done + # a path the later stage also covers is that stage's alone + for stage in whiteouts opaque; do + LC_ALL=C sort -zu "$lists/$stage.root.patched" -o "$lists/$stage.root.patched" + LC_ALL=C sort -zu "$lists/$stage.root.build" \ + | LC_ALL=C comm -z -23 - "$lists/$stage.root.patched" > "$lists/$stage.root.build.only" + mv "$lists/$stage.root.build.only" "$lists/$stage.root.build" + done + LC_ALL=C sort -zu "$lists/present.unsorted" \ + | LC_ALL=C comm -z -23 - <(LC_ALL=C sort -zu "$lists/opaque.root.build" "$lists/opaque.root.patched") \ + > "$lists/present" + rm -f "$lists/present.unsorted" +} + +# layer_tar_members KIND BUILD_DIR [PARENT_ROOTFS LISTS_DIR]: the tar +# arguments that name a layer's members. A rootfs layer is the whole final +# tree; a delta layer is its diff against the parent (layer_delta_lists). layer_tar_members() { local kind=$1 local build_dir=$2 + local parent=${3:-} + local lists=${4:-} + local stage case $kind in rootfs) printf '%s\n' -C "$build_dir/root.patched" . ;; delta) - printf '%s\n' -C "$build_dir/.deck/root.build/upper" . - printf '%s\n' -C "$build_dir/.deck/root.patched/upper" . + [[ -n "$parent" && -n "$lists" ]] \ + || { layer_error "a delta layer needs the parent tree and a directory for its lists"; return 1; } + layer_delta_lists "$build_dir" "$parent" "$lists" || return 1 + printf '%s\n' --null --no-recursion -C "$build_dir/root.patched" -T "$lists/present" + for stage in root.build root.patched; do + printf '%s\n' -C "$build_dir/.deck/$stage/upper" \ + -T "$lists/opaque.$stage" -T "$lists/whiteouts.$stage" + done ;; *) layer_error "unknown layer kind '$kind'" diff --git a/bt-layer b/bt-layer index 48cfa03..3b1bf26 100755 --- a/bt-layer +++ b/bt-layer @@ -50,8 +50,11 @@ Environment:: Output, in \$BT_BUILDS/layers/:: - layer.tar.zst full rootfs without --parent, the root.build and - root.patched deck upper directories with --parent + layer.tar.zst full rootfs without --parent; with --parent, the + final tree's diff against the parent: what the + build added or changed, and a whiteout for what + it removed of the parent (nothing for a file + the build created and removed again) layer.tar.zst.sha256 sha256sum of the tarball layer.manifest plain text, one "key value" per line; "units" lists every component this layer carries as a fab @@ -195,7 +198,11 @@ rsync -aHAX --delete "$build_dir/root.patched/" "$rootfs_copy/" tarball=$layers_dir/$layer.tar.zst mapfile -t tar_flags < <(layer_tar_flags "$SOURCE_DATE_EPOCH") -mapfile -t tar_members < <(layer_tar_members "$kind" "$build_dir") +tar_lists=$build_dir/.layer-members +rm -rf "$tar_lists" +mapfile -t tar_members < <(layer_tar_members "$kind" "$build_dir" "$layers_dir/${parent:-none}.rootfs" "$tar_lists") \ + || fatal "cannot list the members of $layer" +[[ ${#tar_members[@]} -gt 0 ]] || fatal "cannot list the members of $layer" info "packing $kind layer $layer into $tarball" started=$(date +%s) tar "${tar_flags[@]}" -cf - "${tar_members[@]}" \ diff --git a/docs/setup b/docs/setup index 704f5f0..03e19ba 100644 --- a/docs/setup +++ b/docs/setup @@ -36,6 +36,11 @@ bt-docker :: apt-get install aufs-tools +bt-layer :: + + # getfattr reads the overlayfs opaque marks; bt-layer stops without it + apt-get install zstd attr + bt-xen :: apt-get install bzip2 diff --git a/tests/layer b/tests/layer index 4cce080..8c06a2c 100755 --- a/tests/layer +++ b/tests/layer @@ -22,7 +22,13 @@ # layer_unit_paths (none, two); layer_manifest_render (ok, odd args, # bad key); layer_manifest_get (ok, missing file, missing key); # layer_manifest_opt (present, absent, missing file); -# layer_tar_flags; layer_tar_members (rootfs, delta, unknown); +# layer_tar_flags; layer_tar_members (rootfs, delta without a parent, +# unknown); layer_delta_lists (a key created and removed leaves no byte, a +# parent file removed is a whiteout, a file re-created is packed once, an +# opaque directory comes from its upper, no final tree, no parent, no +# upper, no getfattr, getfattr failing, no test follows a symlink); layer_is_whiteout; +# layer_is_opaque (set, unset, getfattr failing, missing, a file); +# layer_exists; layer_real_dir_in; layer_exists_in; # layer_audit_packages (clean, unconfigured package); # layer_audit_keys (clean, no list, no root, a PKCS#8 key named by its # image path, an OpenPGP secret key and not a public one, @@ -77,6 +83,15 @@ export BT source "$BT/bin/layer-lib" tmp=$(mktemp -d) +# bt-layer reads overlayfs opaque marks with getfattr, which CI installs +# (package attr). Unprivileged on fake trees it finds no mark; a host without +# it gets a stand-in that answers the same, so the run does not depend on it. +if ! command -v getfattr >/dev/null; then + mkdir -p "$tmp/no-attr-bin" + printf '#!/bin/sh\necho "No such attribute" >&2; exit 1\n' > "$tmp/no-attr-bin/getfattr" + chmod +x "$tmp/no-attr-bin/getfattr" + export LAYER_GETFATTR=$tmp/no-attr-bin/getfattr +fi trap 'rm -rf "$tmp"' EXIT expect_fail() { @@ -142,6 +157,11 @@ build/stamps/root.patched: echo deb > build/.deck/root.build/upper/var/cache/apt/archives/x.deb echo bin > build/.deck/root.build/upper/var/cache/apt/pkgcache.bin echo conf > build/.deck/root.patched/upper/etc/x.conf + mkdir -p build/root.patched/usr/lib build/root.patched/var/cache/apt/archives/partial + cp build/.deck/root.build/upper/usr/lib/x.so build/root.patched/usr/lib/x.so + cp build/.deck/root.build/upper/var/cache/apt/archives/x.deb build/root.patched/var/cache/apt/archives/x.deb + cp build/.deck/root.build/upper/var/cache/apt/pkgcache.bin build/root.patched/var/cache/apt/pkgcache.bin + cp build/.deck/root.patched/upper/etc/x.conf build/root.patched/etc/x.conf echo "\$(COMMON_OVERLAYS)" > build/overlays.used echo "\$(COMMON_CONF)" > build/conf.used echo "\$(UNITS)" > build/units.used @@ -370,10 +390,132 @@ for want in --sort=name --mtime=@1700000000 --numeric-owner --xattrs \ grep -qxF -- "$want" <<<"$flags" || fatal "tar flag missing: $want" done assert_eq members-rootfs "$(layer_tar_members rootfs /b | tr '\n' ' ')" "-C /b/root.patched . " -assert_eq members-delta "$(layer_tar_members delta /b | tr '\n' ' ')" \ - "-C /b/.deck/root.build/upper . -C /b/.deck/root.patched/upper . " +expect_fail members-delta-noparent layer_tar_members delta /b expect_fail members-unknown layer_tar_members squashfs /b +info "10a: delta members are the final tree's diff against the parent" +# A layer build has two stages, each with its own upper directory. A file the +# first creates and the second deletes is content in one and a whiteout in the +# other; it must leave nothing in the tarball. Whiteouts are character devices +# 0:0, which only root can make, so the test marks its stand-ins by name and +# redefines the predicate; the real one is asserted on real files below. +d=$tmp/delta +parent=$d/parent +# getfattr stand-ins: the lists need one, the predicate is redefined anyway +mkdir -p "$d" +gf=$d/getfattr-stub +opaque_rc() { local rc=0; LAYER_GETFATTR=$1 layer_is_opaque "$2" 2>/dev/null || rc=$?; echo "$rc"; } +printf '#!/bin/sh\nprintf y\n' > "$gf.set" +printf '#!/bin/sh\necho "$5: No such attribute" >&2; exit 1\n' > "$gf.unset" +printf '#!/bin/sh\necho "$5: Operation not supported" >&2; exit 1\n' > "$gf.fails" +chmod +x "$gf".* +mkdir -p "$d/b/root.patched" "$d/b/.deck/root.build/upper" "$d/b/.deck/root.patched/upper" "$parent/etc/old" +mkdir -p "$d/b/.deck/root.build/upper/etc/ssl/private" "$d/b/.deck/root.build/upper/usr/lib" "$d/b/.deck/root.patched/upper/etc/ssl/private" +echo parent-owned > "$parent/etc/old.conf" +echo parent-child > "$parent/etc/old/a" +# stage 1 makes a key and a library; stage 2 removes the key, removes a +# parent file, re-creates a file stage 1 removed, and replaces a parent +# directory opaquely +echo 'FAKE-KEY-BYTES-THAT-MUST-NOT-SHIP' > "$d/b/.deck/root.build/upper/etc/ssl/private/fake.key" +echo lib > "$d/b/.deck/root.build/upper/usr/lib/y.so" +echo WHITEOUT > "$d/b/.deck/root.patched/upper/etc/ssl/private/fake.key.wh" +echo WHITEOUT > "$d/b/.deck/root.patched/upper/etc/old.conf.wh" +echo WHITEOUT > "$d/b/.deck/root.build/upper/etc/again.wh" +echo again > "$d/b/.deck/root.patched/upper/etc/again" +mkdir -p "$d/b/.deck/root.patched/upper/etc/old" +echo new > "$d/b/.deck/root.patched/upper/etc/old/b" +echo OPAQUE > "$d/b/.deck/root.patched/upper/etc/old/.opaque" +# the final tree, as the mounted deck shows it +mkdir -p "$d/b/root.patched/etc/ssl/private" "$d/b/root.patched/usr/lib" "$d/b/root.patched/etc/old" +cp "$d/b/.deck/root.build/upper/usr/lib/y.so" "$d/b/root.patched/usr/lib/y.so" +cp "$d/b/.deck/root.patched/upper/etc/again" "$d/b/root.patched/etc/again" +cp "$d/b/.deck/root.patched/upper/etc/old/b" "$d/b/root.patched/etc/old/b" +# stand-ins: a whiteout is NAME.wh, renamed to NAME when the lists are made +for w in $(cd "$d/b/.deck" && find . -name '*.wh'); do mv "$d/b/.deck/$w" "$d/b/.deck/${w%.wh}"; done +rm "$d/b/.deck/root.patched/upper/etc/old/.opaque" +( + export LAYER_GETFATTR=$gf.unset + layer_is_whiteout() { [[ -f "$1" && "$(cat "$1")" == WHITEOUT ]]; } + layer_is_opaque() { [[ -d "$1" && "$1" == */root.patched/upper/./etc/old ]]; } + mapfile -t members < <(layer_tar_members delta "$d/b" "$parent" "$d/lists") + mapfile -t flags < <(layer_tar_flags 1700000000) + tar "${flags[@]}" -cf - "${members[@]}" | zstd -q -o "$d/delta.tar.zst" + printf '%s\n' "${members[@]}" > "$d/members.txt" +) +listing=$(zstd -dc "$d/delta.tar.zst" | tar -t | sort | tr '\n' ' ') +if zstd -dc "$d/delta.tar.zst" | grep -aq 'FAKE-KEY-BYTES-THAT-MUST-NOT-SHIP'; then + fatal "delta: the bytes of a key the build created and removed are in the tarball" +fi +grep -q 'fake.key' <<<"$listing" && fatal "delta: the removed key is named in the tarball: $listing" +assert_eq delta-members "$listing" "./ ./etc/ ./etc/again ./etc/old.conf ./etc/old/ ./etc/old/b ./etc/ssl/ ./etc/ssl/private/ ./usr/ ./usr/lib/ ./usr/lib/y.so " +# the stage 2 whiteout of the key really was there, so it is the predicate +# and the parent check that kept it out, not a missing file +assert_eq delta-key-whiteout-seen "$(cat "$d/b/.deck/root.patched/upper/etc/ssl/private/fake.key")" WHITEOUT +assert_eq delta-again-once "$(zstd -dc "$d/delta.tar.zst" | tar -t | grep -c '^./etc/again$')" 1 +assert_eq delta-old-is-whiteout "$(zstd -dc "$d/delta.tar.zst" | tar -xO ./etc/old.conf)" WHITEOUT +assert_eq delta-opaque-from-upper "$(tr '\0' '\n' < "$d/lists/opaque.root.patched")" ./etc/old +grep -q '^--null$' "$d/members.txt" || fatal "delta: the lists are not read NUL separated" +expect_fail delta-no-final layer_delta_lists "$d/none" "$parent" "$d/l2" +expect_fail delta-no-parent layer_delta_lists "$d/b" "$d/none" "$d/l2" +mkdir -p "$d/c/root.patched" +expect_fail delta-no-upper layer_delta_lists "$d/c" "$parent" "$d/l3" +# the real predicates, on what an unprivileged run can make +layer_is_whiteout /dev/null && fatal "/dev/null is not a whiteout" +layer_is_whiteout "$parent/etc/old.conf" && fatal "a regular file is not a whiteout" +if command -v getfattr >/dev/null; then + assert_eq opaque-real-unmarked "$(opaque_rc getfattr "$parent/etc/old")" 1 +fi +layer_exists "$parent/etc/old.conf" || fatal "layer_exists misses a file" +ln -s /nowhere "$d/dangling" +layer_exists "$d/dangling" || fatal "layer_exists misses a dangling symlink" +# the opaque mark is read or the build stops: a getfattr that is missing or +# fails must not read as "not opaque", which brought a deleted parent file back +assert_eq opaque-set "$(opaque_rc "$gf.set" "$parent/etc/old")" 0 +assert_eq opaque-unset "$(opaque_rc "$gf.unset" "$parent/etc/old")" 1 +assert_eq opaque-getfattr-fails "$(opaque_rc "$gf.fails" "$parent/etc/old")" 2 +assert_eq opaque-getfattr-missing "$(opaque_rc "$tmp/no-such-getfattr" "$parent/etc/old")" 2 +assert_eq opaque-file "$(opaque_rc "$gf.set" "$parent/etc/old.conf")" 1 +LAYER_GETFATTR=$tmp/no-such-getfattr expect_fail delta-no-getfattr layer_delta_lists "$d/b" "$parent" "$d/l4" +mkdir -p "$d/e/root.patched/etc" "$d/e/.deck/root.build/upper/etc/old" "$d/e/.deck/root.patched/upper" +LAYER_GETFATTR=$gf.fails expect_fail delta-getfattr-fails layer_delta_lists "$d/e" "$parent" "$d/l5" +# no test follows a symlink: a stage 1 file under a directory stage 2 +# replaced with a link to the host, a stage 1 whiteout under a parent +# directory that is a link, and an opaque directory over a parent link +s=$d/s +host=$d/host +mkdir -p "$host/x" "$s/parent/etc" "$s/b/root.patched/opt" "$s/b/.deck/root.build/upper/opt/x" \ + "$s/b/.deck/root.patched/upper/opt" "$s/b/.deck/root.build/upper/etc/plink" "$s/b/.deck/root.patched/upper/etc/olink" +echo 'HOST-BYTES-THAT-MUST-NOT-SHIP' > "$host/x/secret" +echo stage1 > "$s/b/.deck/root.build/upper/opt/x/secret" +ln -s "$host/x" "$s/b/.deck/root.patched/upper/opt/x" +ln -s "$host/x" "$s/b/root.patched/opt/x" +ln -s "$host/x" "$s/parent/etc/plink" +ln -s "$host" "$s/parent/etc/olink" +echo WHITEOUT > "$s/b/.deck/root.build/upper/etc/plink/secret" +mkdir -p "$s/b/root.patched/etc/olink" +( + export LAYER_GETFATTR=$gf.unset + layer_is_whiteout() { [[ -f "$1" && "$(cat "$1")" == WHITEOUT ]]; } + layer_is_opaque() { [[ -d "$1" && ! -L "$1" && "$1" == */olink ]]; } + layer_delta_lists "$s/b" "$s/parent" "$s/lists" + mapfile -t members < <(layer_tar_members delta "$s/b" "$s/parent" "$s/lists") + mapfile -t flags < <(layer_tar_flags 1700000000) + tar "${flags[@]}" -cf - "${members[@]}" | zstd -q -o "$s/delta.tar.zst" +) +if zstd -dc "$s/delta.tar.zst" | grep -aq 'HOST-BYTES-THAT-MUST-NOT-SHIP'; then + fatal "delta: a file of the build host reached through a symlink is in the tarball" +fi +grep -qz '^./opt/x/secret$' "$s/lists/present" && fatal "delta: a file under a replaced directory is listed" +assert_eq symlink-no-whiteout "$(tr '\0' '\n' < "$s/lists/whiteouts.root.build")" "" +assert_eq symlink-no-opaque "$(tr '\0' '\n' < "$s/lists/opaque.root.patched")" "" +grep -qz '^./etc/olink$' "$s/lists/present" || fatal "delta: a directory over a parent link is not present" +layer_real_dir_in "$s/b/root.patched" ./opt/x && fatal "layer_real_dir_in follows a symlink" +layer_real_dir_in "$s/b/root.patched" . || fatal "layer_real_dir_in misses the root" +layer_exists_in "$s/b/root.patched" ./opt/x/secret && fatal "layer_exists_in follows a symlinked directory" +layer_exists_in "$s/b/root.patched" ./opt/x || fatal "layer_exists_in misses a symlink" +layer_exists_in "$s/b/root.patched" ./opt || fatal "layer_exists_in misses a top directory" +layer_exists_in "$s/b/root.patched" . || fatal "layer_exists_in misses the root" + info "10b: layer_audit_packages" audit_root=$tmp/auditfs mkdir -p "$audit_root/var/lib/dpkg" @@ -684,7 +826,7 @@ assert_eq core-size "$(layer_manifest_get "$L/core.manifest" size)" "$(stat -c % assert_eq core-sha256-file "$(cut -d ' ' -f 1 "$L/core.tar.zst.sha256")" \ "$(layer_manifest_get "$L/core.manifest" sha256)" assert_eq core-members "$(zstd -dc "$L/core.tar.zst" | tar -t | sort | tr '\n' ' ')" \ - "./ ./bin/ ./bin/bash ./etc/ ./etc/hostname ./etc/machine-id ./var/ ./var/lib/ ./var/lib/dbus/ ./var/lib/dbus/machine-id " + "./ ./bin/ ./bin/bash ./etc/ ./etc/hostname ./etc/machine-id ./etc/x.conf ./usr/ ./usr/lib/ ./usr/lib/x.so ./var/ ./var/cache/ ./var/cache/apt/ ./var/cache/apt/archives/ ./var/lib/ ./var/lib/dbus/ ./var/lib/dbus/machine-id " # the build wrote an id; the layer, and the rootfs its children start from, # carry an empty file and a D-Bus link to it assert_eq core-machine-id "$(zstd -dc "$L/core.tar.zst" | tar -xO ./etc/machine-id | wc -c)" 0