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 commit 34c3d57474 (directory times) and commit
f191ca982c (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:
Martin Pitt
2026-08-28 11:16:29 +02:00
parent edbf043333
commit 94bb5f8e4d
3 changed files with 227 additions and 16 deletions
+131 -2
View File
@@ -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