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
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ 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
16 changes: 16 additions & 0 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ pattern, the digest match, the grep status check, `-I` or the symlink rule
each turns the suite red. Coverage: layer-lib and bt-layer unchanged at one
uncovered line each, the same lines as on 19.x.

## 2026-10-01: the machine-id of the ISO and the images made from it

`bin/reset-machine-id` applies `layer_reset_machine_id` to any rootfs, and
`bin/iso-machine-id-check` refuses an ISO whose live squashfs carries an id,
read with `layer_machine_id_is_reset` (Keel-Linux/tracker#24). `bt-iso` builds
`root.patched` first, resets it, then lets fab squash it, and checks the ISO
before screenshots and release, `-u` included. `bin/rootfs-cleanup`, which
every image made from the ISO's rootfs runs last (bt-vm, bt-container,
bt-xen, bt-openstack, bt-ec2, bt-otc, bt-docker and the others), resets the
id after the chroot work. The new suite `tests/machine-id` makes small ISOs
with mksquashfs and xorriso, so CI installs `squashfs-tools` and `xorriso`.
Reverting the `bt-iso` or the `rootfs-cleanup` change each turns it red.
Coverage: bin/reset-machine-id 9 of 9, bin/iso-machine-id-check 30 of 30,
bin/rootfs-cleanup 11 of 11, layer-lib 390 of 391 (the predicate covered in
`tests/layer`, the same one uncovered line as on 19.x), bt-layer unchanged.

## 2026-09-30: the machine-id reset

`layer_reset_machine_id` leaves every exported layer with an empty
Expand Down
89 changes: 89 additions & 0 deletions bin/iso-machine-id-check
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/bash -e
# Copyright (c) 2026 KeelLinux
#
# This file is part of buildtasks.
#
# Buildtasks is free software; you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.

# Refuses an ISO whose live root filesystem carries a machine-id. tkl-installer
# unpacks that squashfs onto the disk as it is and makes no id of its own, so
# an id in the ISO is the id of every machine installed from it, and of every
# image bt-vm, bt-container and the others derive from it.
#
# Reads the squashfs out of the ISO with xorriso, which fab already needs to
# write the ISO, and only the two files out of the squashfs.

APP=$(basename "$0")
fatal() { echo "FATAL [$APP]: $*" 1>&2; exit 1; }
info() { echo "INFO [$APP]: $*"; }

usage() {
cat<<EOF
Syntax: $APP iso
Fails unless the ISO's live root filesystem has an empty /etc/machine-id and
no /var/lib/dbus/machine-id other than the link to it. The squashfs is copied
out next to the ISO, not to /tmp, which may be a small tmpfs, and removed.

Environment::

ISO_SQUASHFS - path of the root squashfs inside the ISO
(default: $ISO_SQUASHFS_DEFAULT)
EOF
exit 1
}

ISO_SQUASHFS_DEFAULT=/live/10root.squashfs
ISO_SQUASHFS=${ISO_SQUASHFS:-$ISO_SQUASHFS_DEFAULT}

if [[ "$#" != "1" ]]; then
usage
fi
iso=$1
[[ -f "$iso" ]] || fatal "no such file: $iso"
for tool in xorriso unsquashfs; do
command -v "$tool" >/dev/null || fatal "$tool not found"
done

# the library beside this script, whatever BT the caller exported
# shellcheck source=bin/layer-lib
source "$(dirname "$(readlink -f "$0")")/layer-lib"

# describe PATH: what is at PATH, for the message
describe() {
if [[ -L "$1" ]]; then
echo "a link to $(readlink "$1")"
elif [[ -d "$1" ]]; then
echo "a directory"
elif [[ -f "$1" ]]; then
echo "a file holding '$(head -c 64 "$1")'"
else
echo "missing"
fi
}

work=$(mktemp -d -p "$(dirname "$iso")" .iso-machine-id-check.XXXXXX) \
|| fatal "cannot make a work directory next to $iso"
trap 'rm -rf "$work"' EXIT

if ! xorriso -osirrox on -indev "$iso" -extract "$ISO_SQUASHFS" "$work/root.squashfs" \
>"$work/xorriso.log" 2>&1 || [[ ! -f "$work/root.squashfs" ]]; then
cat "$work/xorriso.log" >&2
fatal "cannot read $ISO_SQUASHFS out of $iso"
fi
# unsquashfs exits 0 when a named file is not in the image, so a missing
# /etc/machine-id is told apart by the check below, not here
if ! unsquashfs -q -n -d "$work/root" "$work/root.squashfs" \
"$LAYER_MACHINE_ID" "$LAYER_DBUS_MACHINE_ID" >"$work/unsquashfs.log" 2>&1; then
cat "$work/unsquashfs.log" >&2
fatal "cannot read the machine-id out of $ISO_SQUASHFS in $iso"
fi
if ! layer_machine_id_is_reset "$work/root"; then
fatal "$iso does not ship an empty machine-id: every machine installed" \
"from it would share one. /$LAYER_MACHINE_ID is" \
"$(describe "$work/root/$LAYER_MACHINE_ID")," \
"/$LAYER_DBUS_MACHINE_ID is $(describe "$work/root/$LAYER_DBUS_MACHINE_ID")"
fi
info "$iso: empty /$LAYER_MACHINE_ID, each machine makes its own"
25 changes: 22 additions & 3 deletions bin/layer-lib
Original file line number Diff line number Diff line change
Expand Up @@ -579,14 +579,33 @@ layer_audit_keys() {
#
# /etc/machine-id is left an empty file, as Debian's cloud images ship it,
# and systemd makes the id at each machine's boot. Not missing and not
# "uninitialized": either makes that boot a first boot to systemd, which
# presets every unit and undoes what the build enabled and masked
# (machine-id(5), "First Boot Semantics"). D-Bus's copy becomes the link to
# "uninitialized": either makes that boot a first boot to systemd, which runs
# preset-all over every unit (machine-id(5), "First Boot Semantics"). Built
# without first-boot-full-preset, as Debian 13's systemd 257 is (upstream
# means to make it the default later, so check again on forky), that only
# enables: every
# unit a preset enables is enabled, including the ones the build disabled on
# purpose, and nothing is disabled. D-Bus's copy becomes the link to
# it that tmpfiles.d/dbus.conf makes when it is absent, because systemd reads
# that file when /etc/machine-id is empty and would bring the old id back.
LAYER_MACHINE_ID=etc/machine-id
LAYER_DBUS_MACHINE_ID=var/lib/dbus/machine-id

# layer_machine_id_is_reset ROOTFS: true when ROOTFS is in that state: an
# empty /etc/machine-id that is a file of its own, and no D-Bus copy or the
# link to it. What an image is checked against when it was not made here
# (bin/iso-machine-id-check).
layer_machine_id_is_reset() {
local rootfs=$1
local id=$rootfs/$LAYER_MACHINE_ID
local dbus=$rootfs/$LAYER_DBUS_MACHINE_ID
[[ -f "$id" && ! -L "$id" && ! -s "$id" ]] || return 1
if [[ -e "$dbus" || -L "$dbus" ]]; then
[[ -L "$dbus" && "$(readlink "$dbus")" == "/$LAYER_MACHINE_ID" ]] || return 1
fi
return 0
}

# A tree already in that state is not written, so a child layer does not
# copy its parent's empty file up into its own tarball.
layer_reset_machine_id() {
Expand Down
41 changes: 41 additions & 0 deletions bin/reset-machine-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash -e
# Copyright (c) 2026 KeelLinux
#
# This file is part of buildtasks.
#
# Buildtasks is free software; you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.

# Leaves a root filesystem with the machine identity every image must ship:
# an empty /etc/machine-id and /var/lib/dbus/machine-id, when present, the
# link to it, so that each machine makes its own id at boot. The same reset
# bt-layer applies to every layer (layer_reset_machine_id in bin/layer-lib),
# for the images that do not go through bt-layer: the ISO, which bt-iso
# squashes from root.patched, and every image made from the ISO's rootfs
# (bt-vm, bt-container, bt-xen, bt-openstack and the others, through
# bin/rootfs-cleanup).

APP=$(basename "$0")
fatal() { echo "FATAL [$APP]: $*" 1>&2; exit 1; }

usage() {
cat<<EOF
Syntax: $APP rootfs-dir
Empties /etc/machine-id in rootfs-dir and links /var/lib/dbus/machine-id to it
EOF
exit 1
}
if [[ "$#" != "1" ]]; then
usage
fi

rootfs=$1
[[ -d "$rootfs" ]] || fatal "no such directory: $rootfs"

# the library beside this script, whatever BT the caller exported
# shellcheck source=bin/layer-lib
source "$(dirname "$(readlink -f "$0")")/layer-lib"

layer_reset_machine_id "$rootfs" || fatal "cannot reset the machine-id of $rootfs"
10 changes: 9 additions & 1 deletion bin/rootfs-cleanup
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ CLEANUP='/boot/*.bak /var/log/dpkg.log /var/log/apt/* /var/lib/apt/lists/* /var/
usage() {
cat<<EOF
Syntax: $APP rootfs-dir
Cleans up logs, caches and leftover files in rootfs
Cleans up logs, caches and leftover files in rootfs, and leaves it with an
empty /etc/machine-id (bin/reset-machine-id)
EOF
exit 1
}
Expand All @@ -30,3 +31,10 @@ rootfs=$1

tklpatch-chroot "$rootfs" "rm -rf $CLEANUP"
fuser -k "$rootfs" || true

# Last, after every patch and package upgrade that ran in the chroot: the
# rootfs comes from the ISO, and systemd's postinst writes a machine-id of
# its own on every configure, so whatever ran since the ISO was made may
# have left one that every machine made from this image would share.
# reset-machine-id says what failed
"$(dirname "$(readlink -f "$0")")/reset-machine-id" "$rootfs" || exit 1
27 changes: 26 additions & 1 deletion bt-iso
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,22 @@ else
info "Preparation done. Building appliance $appname."
cd "$BT_PRODUCTS/$appname"
product_build_clean
make 2>&1 | tee build/log || true
mkdir -p build
# root.patched first, so its machine-id is emptied before fab squashes it
# into the ISO (bin/reset-machine-id); the rest of the build then reuses
# it, since nothing it depends on has changed
make build/stamps/root.patched 2>&1 | tee build/log || true
if [[ -f build/stamps/root.patched ]]; then
# a root.sandbox left mounted on root.patched by an earlier run
# (DEBUG skips the clean) would be an overlay whose lower layer
# changes under it
deck -D build/root.sandbox 2>/dev/null || true
if ! "$BT/bin/reset-machine-id" build/root.patched; then
[[ -n "$BT_DEBUG" ]] || product_build_clean
fatal "cannot reset the machine-id of $appname"
fi
make 2>&1 | tee -a build/log || true
fi

if [[ ! -e build/product.iso ]]; then
if [[ -z "$BT_DEBUG" ]]; then
Expand All @@ -410,6 +425,16 @@ else
fi
fi

# whether built here or reused with -u, an ISO that ships a machine-id is
# not released, nor screenshot
if ! "$BT/bin/iso-machine-id-check" build/product.iso; then
if [[ -n "$use_existing" ]]; then
fatal "build/product.iso ships a machine-id - rebuild it without -u"
fi
[[ -n "$BT_DEBUG" ]] || product_build_clean
fatal "build/product.iso ships a machine-id although root.patched was reset - a build step after it wrote one"
fi

if [[ -z "$no_screens" ]]; then
if [[ -n "$use_existing" ]]; then
info "Product iso being loaded as docker container"
Expand Down
10 changes: 6 additions & 4 deletions tests/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
# bin/layer-measure-lib, bin/layer-report-lib and bt-layer-measure under
# tests/measure
# bin/layer-compare-lib under tests/compare
# bin/reset-machine-id, bin/iso-machine-id-check and bin/rootfs-cleanup
# under tests/machine-id
# Exits 1 when a measured file is below the threshold (default 95), 2 when a
# tool is missing.
#
# tests/coverage.sh [THRESHOLD] (or COVERAGE_THRESHOLD in the environment)
#
# COVERAGE_DIR keeps the kcov report (default: a temporary directory).
# Needs the Debian packages kcov, zstd, git and gpg.
# Needs the Debian packages kcov, zstd, git, gpg, squashfs-tools and xorriso.
#
# The tests run the bt-* scripts from a scratch copy (they need their own
# config directory), so kcov lists the copy next to the checkout: the two
Expand All @@ -25,17 +27,17 @@ set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
threshold="${1:-${COVERAGE_THRESHOLD:-95}}"

for tool in kcov zstd git gpg; do
for tool in kcov zstd git gpg mksquashfs unsquashfs xorriso; do
if ! command -v "$tool" >/dev/null; then
echo "$tool not found (apt-get install $tool)" >&2
exit 2
fi
done

report="${COVERAGE_DIR:-$(mktemp -d)}"
measured=/bin/layer-lib,/bt-layer,/bin/aplinfo-lib,/bt-aplinfo,/bin/signature-lib,/bin/generate-signature,/bin/layer-measure-lib,/bin/layer-compare-lib,/bin/layer-report-lib,/bt-layer-measure
measured=/bin/layer-lib,/bt-layer,/bin/aplinfo-lib,/bt-aplinfo,/bin/signature-lib,/bin/generate-signature,/bin/layer-measure-lib,/bin/layer-compare-lib,/bin/layer-report-lib,/bt-layer-measure,/bin/reset-machine-id,/bin/iso-machine-id-check,/bin/rootfs-cleanup
skipped=/tests/,/scratch-bt/bin/generate-signature
for suite in layer aplinfo signature measure compare; do
for suite in layer aplinfo signature measure compare machine-id; do
kcov --include-pattern="$measured" --exclude-pattern="$skipped" \
"$report" "$here/$suite"
done
Expand Down
14 changes: 14 additions & 0 deletions tests/layer
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
# layer_reset_machine_id (an id and a D-Bus copy, already reset and
# left untouched, missing, a symlink, a D-Bus link elsewhere, no D-Bus
# copy, no /etc, a directory in the way of either);
# layer_machine_id_is_reset (reset, no D-Bus copy, populated, a D-Bus
# file of its own);
# layer_check_build (ok, no stamp, not mounted, no upper, a private key,
# no key list);
# layer_hash_size (ok, missing); layer_write_sha256; layer_umount_targets
Expand Down Expand Up @@ -519,6 +521,18 @@ rm "$mid_root/var/lib/dbus/machine-id"
layer_reset_machine_id "$mid_root"
[[ ! -e "$mid_root/var/lib/dbus/machine-id" && ! -L "$mid_root/var/lib/dbus/machine-id" ]] \
|| fatal "mid-no-dbus: a D-Bus copy was made"
# the predicate the ISO check reads an image with agrees with the reset
layer_machine_id_is_reset "$mid_root" || fatal "mid-is-reset-no-dbus: refused"
ln -s /etc/machine-id "$mid_root/var/lib/dbus/machine-id"
layer_machine_id_is_reset "$mid_root" || fatal "mid-is-reset-link: refused"
rm "$mid_root/var/lib/dbus/machine-id"
echo f0e97605ab594989b4d78f4a126b5b37 > "$mid_root/var/lib/dbus/machine-id"
expect_fail mid-is-reset-dbus-file layer_machine_id_is_reset "$mid_root"
rm "$mid_root/var/lib/dbus/machine-id"
chmod 0644 "$mid_root/etc/machine-id"
echo f0e97605ab594989b4d78f4a126b5b37 > "$mid_root/etc/machine-id"
expect_fail mid-is-reset-populated layer_machine_id_is_reset "$mid_root"
layer_reset_machine_id "$mid_root"
expect_fail mid-no-etc layer_reset_machine_id "$tmp/no-such-root"
out=$(layer_reset_machine_id "$tmp/no-such-root" 2>&1) || true
[[ "$out" == *"no /etc"* ]] || fatal "mid-no-etc: unexpected message: $out"
Expand Down
Loading
Loading