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 difference | Fix |
|---|---|
| Timestamps | SOURCE_DATE_EPOCH, and KBUILD_BUILD_TIMESTAMP for the kernel |
| User and host names | KBUILD_BUILD_USER, KBUILD_BUILD_HOST |
| Build paths | -ffile-prefix-map |
| File order in archives | Sort them with LC_ALL=C |
| Filesystem metadata | mkfs.erofs -T, or mke2fs -E hash_seed=UUID |
| The kernel’s build counter | KBUILD_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/versiondiffers:KBUILD_BUILD_*weren’t exported in that shell.- Different after
mrproper:KBUILD_BUILD_VERSIONis unset, or the configs differ. gitstate differs: uncommitted edits changekernel.release. Build from a clean tarball.unrecognized option --reproducible: older or non-GNU cpio. Use the kernel’susr/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.