Ashok G

Linux, Observed · Linux 7.3 · Draft

10 — Reproducible, or it didn’t happen

Requirements: chapter 5’s kernel source and diffoscope. Start with the small archive, before you try the kernel.

The goal

Reproducible means anyone can recreate “bit-by-bit identical copies” (definition). The enemies are time, paths, ordering and randomness:

Source of differenceFix
TimestampsSOURCE_DATE_EPOCH, and KBUILD_BUILD_TIMESTAMP for the kernel
User and host namesKBUILD_BUILD_USER, KBUILD_BUILD_HOST
Build paths-ffile-prefix-map
File order in archivesSort them with LC_ALL=C
Filesystem metadatamkfs.erofs -T, or mke2fs -E hash_seed=UUID
The kernel’s build counterKBUILD_BUILD_VERSION

The kernel’s own guide lists what else can get in the way, such as MODULE_SIG_ALL and RANDSTRUCT (reproducible-builds.rst).

Build IDs aren’t random, unless you choose --build-id=uuid. Hash-style IDs change because the content changed, so they’re a symptom (ld docs).

Hands-on

Step 1: pin the environment

export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
export KBUILD_BUILD_TIMESTAMP="$(date -u -d @$SOURCE_DATE_EPOCH)"
export KBUILD_BUILD_USER=lo KBUILD_BUILD_HOST=lo
export LC_ALL=C TZ=UTC

Run the first line from your manifest’s git repository. Expected: date -u -d @$SOURCE_DATE_EPOCH prints the date you expect.

Step 2: build the same archive twice

cd /lo/out/ramfs
find . -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} +
find . | LC_ALL=C sort | cpio -o -H newc --reproducible --owner=0:0 > ../a.cpio
sleep 2
find . -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} +
find . | LC_ALL=C sort | cpio -o -H newc --reproducible --owner=0:0 > ../b.cpio
cd .. && sha256sum a.cpio b.cpio

Expected: the two hashes match. --reproducible is a GNU cpio option, so check cpio --help.

Step 3: break it on purpose

Leave out sort or the touch line in one build, then:

diffoscope a.cpio b.cpio --html report.html

Expected: the report names the file or field that changed. Time how long it took you to find it.

Step 4: compress it

gzip -n leaves out the name and timestamp. Compare two compressed outputs with sha256sum.

Step 5: build the kernel twice

Save your config first, because mrproper deletes it:

cd /lo/build/linux-7.3
cp .config /lo/out/lo.config
export KBUILD_BUILD_VERSION=1
make ARCH=x86_64 mrproper
cp /lo/out/lo.config .config
make ARCH=x86_64 olddefconfig && make ARCH=x86_64 -j"$(nproc)"
sha256sum arch/x86/boot/bzImage vmlinux > /lo/out/kbuild1.sha
make ARCH=x86_64 mrproper
cp /lo/out/lo.config .config
make ARCH=x86_64 olddefconfig && make ARCH=x86_64 -j"$(nproc)"
sha256sum arch/x86/boot/bzImage vmlinux > /lo/out/kbuild2.sha
diff /lo/out/kbuild1.sha /lo/out/kbuild2.sha

Expected: diff prints nothing. If it doesn’t, save both vmlinux files and run diffoscope on them. Check version strings first.

Step 6: move the tree

Copy the source to a new path, add KCFLAGS=-ffile-prefix-map=$PWD=., and repeat. Then try a second machine with the same pinned toolchain.

If it fails

  • /proc/version differs: KBUILD_BUILD_* weren’t exported in that shell.
  • Different after mrproper: KBUILD_BUILD_VERSION is unset, or the configs differ.
  • git state differs: uncommitted edits change kernel.release. Build from a clean tarball.
  • unrecognized option --reproducible: older or non-GNU cpio. Use the kernel’s usr/gen_init_cpio.

Principle: a claim you can’t check is a story

“This image was built from these sources” is a claim. Reproducibility turns it into a check anyone can run. The same standard applies to a benchmark and to a security property: if the only evidence is the author’s say-so, it’s a story, however honest the author. Design systems, and write-ups, so the reader can verify rather than trust.

The release is the pair: image hash and manifest hash.