repart: clamp copied timestamps to $SOURCE_DATE_EPOCH
Use the new copy timestamp clamping mechanism from the previous commit to fix unreproducible wall-clock files in the generated images. That covers both modes: online mode writes through the kernel, and offline mode stages the tree in a temporary directory before handing it to mkfs or mcopy. `mcopy -m` then carries the clamped mtimes into the FAT. This complements commit34c3d57474(directory times) and commitf191ca982c(FAT insertion order). Cover all combinations (online/offline, with and without epoch) in the new `testcase_vfat_reproducibility()`. Update `testcase_make_symlinks()` to cover the symlink creation/re-fix their parent directory code paths. Sadly, images are still not reproducible byte for byte. Document systemd-repart's $SOURCE_DATE_EPOCH handling and remaining reproducibility issues in the manpage.
This commit is contained in:
@@ -2166,7 +2166,7 @@ EOF
|
||||
}
|
||||
|
||||
testcase_make_symlinks() {
|
||||
local defs imgs output
|
||||
local defs imgs output epoch
|
||||
|
||||
if systemd-detect-virt --quiet --container; then
|
||||
echo "Skipping MakeSymlinks= test in container."
|
||||
@@ -2191,7 +2191,13 @@ MakeSymlinks=/dir/foo-%a:/bar-%a
|
||||
MakeSymlinks=/dir/bar-%a:../bar-%a
|
||||
EOF
|
||||
|
||||
systemd-repart --offline="$OFFLINE" \
|
||||
# Build with an epoch, so that this also covers do_make_symlinks()' time stamping. It has to
|
||||
# stamp the symlink itself and repair the directory it lands in, because creating an entry there
|
||||
# bumps that directory back to the wall clock.
|
||||
epoch=1700000000
|
||||
|
||||
env SOURCE_DATE_EPOCH="$epoch" \
|
||||
systemd-repart --offline="$OFFLINE" \
|
||||
--definitions="$defs" \
|
||||
--empty=create \
|
||||
--size=1G \
|
||||
@@ -2205,6 +2211,12 @@ EOF
|
||||
assert_eq "$(readlink "$imgs/mnt/dir/foo")" "/bar"
|
||||
assert_eq "$(readlink "$imgs/mnt/dir/foo-${architecture}")" "/bar-${architecture}"
|
||||
assert_eq "$(readlink "$imgs/mnt/dir/bar-${architecture}")" "../bar-${architecture}"
|
||||
|
||||
# the symlink's own mtime (no -L)
|
||||
assert_eq "$(stat -c %Y "$imgs/mnt/dir/foo")" "$epoch"
|
||||
# a MakeDirectories= dir, stamped with the epoch and then bumped by the symlinks below it.
|
||||
assert_eq "$(stat -c %Y "$imgs/mnt/dir")" "$epoch"
|
||||
|
||||
systemd-dissect -U "$imgs/mnt"
|
||||
}
|
||||
|
||||
@@ -2756,6 +2768,123 @@ EOF
|
||||
cmp "$imgs/test1.img" "$imgs/test2.img"
|
||||
}
|
||||
|
||||
testcase_vfat_reproducibility() {
|
||||
local defs imgs img dot_efi dot_linux
|
||||
|
||||
if ! command -v mdir >/dev/null; then
|
||||
echo "Skipping vfat reproducibility test, mtools is not installed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")"
|
||||
imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")"
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$defs' '$imgs'" RETURN
|
||||
|
||||
tee "$defs/esp.conf" <<EOF
|
||||
[Partition]
|
||||
Type=esp
|
||||
Format=vfat
|
||||
CopyFiles=/:/
|
||||
EOF
|
||||
|
||||
# $SOURCE_DATE_EPOCH is a clamp, not an override, so put one file on either side of it to cover
|
||||
# both directions.
|
||||
mkdir -p "$imgs/tree/EFI/Linux"
|
||||
echo old >"$imgs/tree/EFI/Linux/old.efi"
|
||||
echo new >"$imgs/tree/EFI/Linux/new.efi"
|
||||
touch --date=@1600000000 "$imgs/tree/EFI/Linux/old.efi"
|
||||
touch --date=@1750000000 "$imgs/tree/EFI/Linux/new.efi"
|
||||
|
||||
# The timestamps as mdir(1) reports them.
|
||||
local -r time_epoch="2023-11-14 22:13" # $SOURCE_DATE_EPOCH below, i.e. @1700000000
|
||||
local -r time_old="2020-09-13 12:26" # old.efi's mtime, before the epoch
|
||||
local -r time_new="2025-06-15 15:06" # new.efi's mtime, after the epoch
|
||||
|
||||
# Build $1 from the tree and set $img to it, in the "file@@offset" form mdir(1) wants. The
|
||||
# remaining arguments are passed to env(1), to control $SOURCE_DATE_EPOCH.
|
||||
build_image() {
|
||||
local name="$1" output offset
|
||||
shift
|
||||
|
||||
output=$(env "$@" \
|
||||
systemd-repart \
|
||||
--offline="$OFFLINE" \
|
||||
--definitions="$defs" \
|
||||
--empty=create \
|
||||
--size=auto \
|
||||
--seed="$seed" \
|
||||
--dry-run=no \
|
||||
--root="$imgs/tree" \
|
||||
--json=pretty \
|
||||
"$imgs/$name")
|
||||
|
||||
offset=$(jq -r '.[0].offset' <<<"$output")
|
||||
img="$imgs/$name@@$offset"
|
||||
|
||||
# dump listing for debugging
|
||||
fat_entry -/ ::/
|
||||
}
|
||||
|
||||
# Print $img's mdir(1) entry for the given path. mdir lists a directory's contents; a trailing `*`
|
||||
# gets the directory's own entry instead. Insensitive path matching, thus works with mtools' 8.3
|
||||
# lower case and kernel vfat's upper case names.
|
||||
fat_entry() {
|
||||
env MTOOLS_SKIP_CHECK=1 TZ=UTC mdir -i "$img" "$@"
|
||||
}
|
||||
|
||||
# Same, for the "." entry of directory $1, which cannot be addressed directly.
|
||||
fat_dot_entry() {
|
||||
fat_entry "$1" | grep -E '^\. +<DIR>'
|
||||
}
|
||||
|
||||
build_image epoch.img SOURCE_DATE_EPOCH=1700000000
|
||||
|
||||
# The directories and the file newer than the epoch are clamped down to it, ...
|
||||
assert_in "$time_epoch" "$(fat_entry '::/EFI*')"
|
||||
assert_in "$time_epoch" "$(fat_entry '::/EFI/Linux*')"
|
||||
assert_in "$time_epoch" "$(fat_entry '::/EFI/Linux/new.efi')"
|
||||
# ... while the older one keeps its own mtime.
|
||||
assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
|
||||
|
||||
# "." and ".." behave differently
|
||||
dot_efi=$(fat_dot_entry '::/EFI')
|
||||
dot_linux=$(fat_dot_entry '::/EFI/Linux')
|
||||
if [[ "$OFFLINE" == "yes" ]]; then
|
||||
# mmd creates them and respects SOURCE_DATE_EPOCH
|
||||
assert_in "$time_epoch" "$dot_efi"
|
||||
assert_in "$time_epoch" "$dot_linux"
|
||||
else
|
||||
# The kernel's vfat driver creates them with wallclock mtime. Our utimensat()
|
||||
# afterwards only rewrites the directory's entry in its *parent*, never this pair.
|
||||
# Out of reach from userspace, remains unreproducible. Just log them.
|
||||
echo "'.' entries carry the wall clock, as expected online: $dot_efi / $dot_linux"
|
||||
fi
|
||||
|
||||
# Without an epoch, both files keep their own mtime.
|
||||
build_image wallclock.img -u SOURCE_DATE_EPOCH
|
||||
|
||||
assert_in "$time_new" "$(fat_entry '::/EFI/Linux/new.efi')"
|
||||
assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
|
||||
|
||||
# A value we cannot parse is dropped rather than passed on
|
||||
build_image bogus.img SOURCE_DATE_EPOCH=99999999999999999999
|
||||
|
||||
assert_in "$time_new" "$(fat_entry '::/EFI/Linux/new.efi')"
|
||||
assert_in "$time_old" "$(fat_entry '::/EFI/Linux/old.efi')"
|
||||
|
||||
# SOURCE_DATE_EPOCH in hex - we do parse that, but mtools doesn't
|
||||
build_image hex.img SOURCE_DATE_EPOCH=0x6553F100
|
||||
|
||||
assert_in "$time_epoch" "$(fat_entry '::/EFI*')"
|
||||
|
||||
# Note the images still are not reproducible byte for byte: the volume label entry carries
|
||||
# mkfs.fat's wall clock, in *both* modes, and that needs a dosfstools with $SOURCE_DATE_EPOCH
|
||||
# support (https://github.com/dosfstools/dosfstools/commit/8da7bc93315c, release > 4.2). Once
|
||||
# that lands, offline mode can move to a `cmp` like in testcase_ext_reproducibility; online
|
||||
# mode cannot, because of the "." and ".." entries above.
|
||||
}
|
||||
|
||||
testcase_luks2_keyhash() {
|
||||
local defs imgs output root
|
||||
|
||||
|
||||
Reference in New Issue
Block a user