fix(linux/build): byte-patch Rock Ridge TF dates after xorriso (M1.1 iter37)
Some checks failed
Build SilverMetal Linux ISO (reproducibility-gated) / builder-image (push) Successful in 1s
Build SilverMetal Linux ISO (reproducibility-gated) / build-and-verify (push) Failing after 17m27s

Run #4284's diagnostic (iter36) confirmed xorriso ignores every
date-setting command we throw at it for the node it just -updated:

    flag=0x0e  →  CREATION + MODIFICATION + ACCESS (short form)
    CREATION   (set from source file btime via touch -d):
        7e 05 08 00 2c 3a 00     (= SOURCE_DATE_EPOCH)
    MODIFICATION   (still wall-clock):
        A=7e 05 08 01 02 2c 00   B=7e 05 08 01 12 33 00
    ACCESS   (still wall-clock):
        A=7e 05 08 01 02 2c 00   B=7e 05 08 01 12 32 00

Tested across iters 34-36:
  * `-alter_date_r all "=N" /`           — only fixed CREATION (b)
  * `-alter_date all "=N" path` after -update — same
  * `-volume_date c m x f u s "=N"`      — volume-level only
  * `touch -d "@N" "${new_sqfs}"` before — fixed CREATION via btime
  * various orderings, with/without `--` terminators
None override xorriso's wall-clock stamping of MOD/ACCESS at -commit.

Concede that fight and just patch the bytes after xorriso writes the
ISO. We KNOW exactly what's wrong — the TF entry for
/live/filesystem.squashfs has its CREATION slot correct (= 7-byte
ISO9660 short-form encoding of SOURCE_DATE_EPOCH) but MODIFICATION
and ACCESS still hold the post-process commit time. So copy the 7
CREATION bytes over the 7 MODIFICATION bytes and 7 ACCESS bytes.

The patcher (embedded Python, since silvermetal-builder ships
python3):
  * Finds every TF entry header (`54 46 1a 01 0e`) near the
    "filesystem.squashfs" NM tag (96-byte window — anchors both
    ends so we don't touch some other file's TF entry).
  * Copies CREATION (offset +5..+12) onto MODIFICATION (+12..+19)
    and ACCESS (+19..+26).
  * Skips entries already correct (so re-running is a no-op).
  * Reports how many entries were patched.

This is surgical: only the entry we know is broken, and only when
its MOD/ACCESS actually differ from the (known-correct) CREATION.

If the next run still drifts, the diagnostic byte-offset will tell
us where the residual leak is (almost certainly in some volume
descriptor field we haven't covered yet — at which point we extend
the patcher).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 02:22:56 +01:00
parent 60384e70c8
commit 6bafa85231

View File

@@ -292,6 +292,62 @@ post_process_for_reproducibility() {
sudo --non-interactive mv -f "${new_iso}" "${iso_file}"
sudo --non-interactive rm -f "${new_sqfs}"
# Post-write byte patch: xorriso's -update writes wall-clock
# mtime/atime into Rock Ridge TF directory entries at -commit time
# regardless of -alter_date / -alter_date_r / -find set_to_mtime /
# touching the source file's mtime (verified across iters 34-36).
# The leak is contained to the TF entry of the one node we
# replaced (/live/filesystem.squashfs) — every other timestamp in
# the ISO was already SDE before -update, and -update doesn't touch
# them.
#
# We have the SDE-derived 7-byte ISO9660 short-form date in the
# CREATION slot of that TF entry already (because we touched the
# source file's btime). We just copy those 7 bytes over the
# MODIFICATION (next 7 bytes) and ACCESS (7 after that) slots.
#
# The TF entry header is `54 46 1a 01 0e` ("TF" length=26 ver=1
# flags=0x0e: CREATION+MODIFICATION+ACCESS, short form). The
# filename "filesystem.squashfs" follows immediately after the NM
# Rock Ridge entry that follows the TF dates. So a unique 5-byte
# marker plus the filename anchors the location.
echo "post-process: byte-patching Rock Ridge TF dates on /live/filesystem.squashfs"
sudo --non-interactive python3 - "${iso_file}" <<'PYEOF'
import sys, struct
path = sys.argv[1]
TF_HDR = b'TF\x1a\x01\x0e' # "TF" length=26 ver=1 flags=0x0e
NAME_TAG = b'filesystem.squashfs' # appears in the NM Rock Ridge entry
with open(path, 'r+b') as f:
data = f.read()
# Find TF entries near a "filesystem.squashfs" NM tag. We don't trust
# any single anchor; require both within a 96-byte window so we don't
# accidentally rewrite some other TF entry that happens to be near
# the NM tag of a different file.
patched = 0
i = 0
while True:
j = data.find(TF_HDR, i)
if j < 0:
break
# Look for the filename within ~96 bytes after this TF header.
window = data[j:j + 96]
if NAME_TAG in window:
# Date 1 starts at j + 5; copy its 7 bytes onto Date 2 (j+12)
# and Date 3 (j+19).
creation = data[j + 5:j + 12]
if creation[3:7] != b'\x00\x00\x00\x00': # sanity: not all zeroes
# only patch if MOD or ACCESS actually differs from CREATION
if data[j + 12:j + 19] != creation or data[j + 19:j + 26] != creation:
f.seek(j + 12)
f.write(creation)
f.seek(j + 19)
f.write(creation)
patched += 1
print(f' patched TF at offset {j} (creation={creation.hex()})')
i = j + 1
print(f'post-process: byte-patcher fixed {patched} TF entr{"y" if patched == 1 else "ies"}')
PYEOF
echo "post-process: ISO rebuilt with reproducible squashfs"
sha256sum "${iso_file}"
}