diff --git a/artifacts/findings.yaml b/artifacts/findings.yaml index ea01cd3..c04e32a 100644 --- a/artifacts/findings.yaml +++ b/artifacts/findings.yaml @@ -6100,3 +6100,65 @@ artifacts: detected-by: implementing gust:hal as real MMIO and running it on F100 silicon, 2026-09-07 severity: major triage-status: confirmed + + - id: AFD-110 + type: ai-found-defect + title: "jess's cascade fusion was NOT REPRODUCIBLE — two consecutive fuses of identical inputs produced different binaries, making the attestation chain unfalsifiable; fixed with --reproducible, and the lowered objects are byte-identical to the recorded baseline" + status: open + description: |- + 2026-09-07. Per-piece release-watch of meld v0.55.0 (2026-09-07), which adopts + validation-by-default on the --memory shared paths — the change jess argued for on + meld#390/#391. jess fuses with --pack-rebase, so this is jess's build path. + + *** THE DEFECT WAS JESS'S, AND IT WAS INVISIBLE *** + hardware/renode/cascade-invoke/build.sh did not pass --reproducible (meld#325). Measured on + the real five stage inputs, two CONSECUTIVE fuses with jess's exact invocation: + run 1 md5 57a7cefe4ed956579194a789010575c7 + run 2 md5 8123737100fea61640748dfd4ab93ee8 + Every rebuild produced a different binary, because meld derives the attestation id from a + random UUID and the timestamp from the wall clock. That makes the attestation chain this + campaign rests on UNFALSIFIABLE: you cannot claim a shipped image came from these inputs if + rebuilding never reproduces it. Nothing in jess would have reported this — the artifact was + never compared against a second build of itself. + WITH --reproducible: two fuses agree exactly (md5 44eea1de03760aa01a43c7ab42ef3083). The flag + is supported by the PIN (0.52.0), so this needed no version bump. + + *** WHAT IT COSTS, MEASURED RATHER THAN ASSUMED *** + --reproducible is NOT metadata-only. `component-provenance` changes too: + without: .scratch/v1341/iekf.wasm, .scratch/v1341/rate.wasm, ... + with: component-8, component-0, ... + Input NAMES become positional placeholders. The input HASHES are preserved, and those are the + real identity — tools/deps/artifacts.pins already maps 43982be4...=rate, 076b8a81...=mixer, + d85c325f...=attitude, 32a45a53...=position, 600f9047...=iekf. The discarded names were local + .scratch paths, not stable identities, so the trade is favourable and was taken deliberately. + The flag's help text describes only the attestation id and timestamp, so this behaviour is + undocumented; raised as the second half of meld#401. + + *** NOTHING THAT REACHES SILICON CHANGED — THE DECISIVE CHECK *** + Code and data are byte-identical with and without the flag (module diffed with BOTH metadata + sections stripped: 7,949 identical lines). And the lowered ARM objects match the AFD-106 + baseline exactly, despite the fused wasm shrinking 63,156 -> 61,167 B: + cortex-m7dp md5 5362d8318f0066266d7dc29ba0c7563c 41,140 B (AFD-106 baseline) + cortex-m4f md5 90e8fa9bee4b31c54a64e9306685b955 42,562 B (AFD-106 baseline) + A size change in the fused artifact with an unchanged lowered object is exactly the shape that + would otherwise read as a regression; recording both numbers is what makes it legible. + + *** THE GATE *** + build.sh now fuses TWICE and requires byte-equality. A flag that silently stopped applying + looks exactly like one that works, and nothing else in the pipeline would notice. The failing + case is demonstrated observable by the two differing md5s above — the gate is not vacuous. + + *** meld v0.55.0 PER-PIECE VERDICT: CLEAN, with one incomplete fix *** + - fused CODE+DATA byte-identical 0.52.0 -> 0.55.0 on jess's real inputs; validation-by-default + cost nothing and would not have been wanted opt-in. + - the "Fusing N components" fix jess reported on #390 is CONFIRMED on the console (10 -> 5 for + 5 input files) but does NOT reach the attestation, which still records components_fused=10 + under 0.55.0 (modules_merged=5 is correct in both). The corrected number reaches the line a + consumer reads once while building, and not the artifact that is shipped, signed and + audited. Filed as meld#401. Not blocking jess: jess reads modules_merged. + Pin unchanged at 0.52.0 — the fused payload is identical, so a bump buys nothing today. + tags: [meld, release-watch, reproducibility, attestation, supply-chain, meld-325, meld-401, afd-106] + fields: + detected-by: per-piece release-watch of meld v0.55.0 against the real fused cascade, 2026-09-07 + severity: major + triage-status: confirmed diff --git a/hardware/renode/cascade-invoke/build.sh b/hardware/renode/cascade-invoke/build.sh index 0d8143a..434a12f 100755 --- a/hardware/renode/cascade-invoke/build.sh +++ b/hardware/renode/cascade-invoke/build.sh @@ -114,8 +114,35 @@ announce_tool meld "$MELD" announce_tool loom "$LOOM" echo "== 1. fuse + lower (the object and its init tables come from ONE module) ==" +# --reproducible (#325) is NOT optional here. Without it meld derives the attestation id +# from a random UUID and the timestamp from the wall clock, so TWO CONSECUTIVE FUSES OF THE +# SAME INPUTS PRODUCE DIFFERENT ARTIFACTS — measured, not assumed: md5 57a7cefe... then +# 8123737100... on identical inputs. Every rebuild produced a new binary, which makes the +# attestation chain this campaign is built on unfalsifiable: you cannot say a shipped image +# came from these inputs if rebuilding never reproduces it. +# +# What it costs: `component-provenance` records `component-N` placeholders instead of the +# input FILENAMES. The input HASHES are preserved, and those are the real identity — +# tools/deps/artifacts.pins already maps 43982be4...=rate, 076b8a81...=mixer, and so on. +# The discarded names were local .scratch paths, not stable identities. +# What it does NOT cost: code and data are byte-identical either way (verified by diffing +# the module with both metadata sections stripped: 7,949 identical lines). +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}" \ run_meld fuse "$SCRATCH"/v1341/{rate,mixer,attitude,position,iekf}.wasm \ - --memory shared --pack-rebase -o "$OUT/c.wasm" >"$OUT/meld.log" 2>&1 || fail "meld" + --memory shared --pack-rebase --reproducible -o "$OUT/c.wasm" >"$OUT/meld.log" 2>&1 || fail "meld" + +# THE GATE. Fuse a second time and require byte-equality. A flag that silently stopped +# applying looks exactly like one that works, and this is the only place that would notice. +if [ -z "${SKIP_REPRO_GATE:-}" ]; then + SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}" \ + run_meld fuse "$SCRATCH"/v1341/{rate,mixer,attitude,position,iekf}.wasm \ + --memory shared --pack-rebase --reproducible -o "$OUT/c.repro.wasm" >>"$OUT/meld.log" 2>&1 \ + || fail "meld (reproducibility second fuse)" + cmp -s "$OUT/c.wasm" "$OUT/c.repro.wasm" \ + || fail "the fusion is NOT reproducible: two fuses of identical inputs differ" + rm -f "$OUT/c.repro.wasm" + echo " fusion is byte-reproducible (two fuses of identical inputs agree)" +fi run_loom optimize "$OUT/c.wasm" -o "$OUT/c.loom.wasm" >"$OUT/loom.log" 2>&1 || fail "loom" "$SYNTH" compile "$OUT/c.loom.wasm" -t cortex-m7dp --cortex-m --relocatable \ --embedder-data-init --embedder-global-init -o "$OUT/cascade.o" >"$OUT/synth.log" 2>&1 || fail "synth"