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