Skip to content

fix(#932): --proven-safe must never elide against a floor it invented (SECURITY) - #934

Merged
avrabe merged 2 commits into
mainfrom
fix/proven-safe-imported-memory-932
Aug 8, 2026
Merged

fix(#932): --proven-safe must never elide against a floor it invented (SECURITY)#934
avrabe merged 2 commits into
mainfrom
fix/proven-safe-imported-memory-932

Conversation

@avrabe

@avrabe avrabe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #932. v0.56 lane RQ-56-PSAFE.

The defect

Imported memories are not in all_memories, so the derived floor was .unwrap_or(0)'no floor establishable' became 'the floor is 0 bytes'.

claimed memory_min_bytes before after
0 (vacuous) ACCEPTED — 0 guards refused, 2 guards
65536 (the truth) REFUSED, 2 guards refused, 2 guards
declared memory, truthful elides 2→0 elides 2→0 (no regression)

The floor is now Option<u32>; None refuses.

My first test was vacuous, and mutation caught it

It used a dummy hash, so every compile was refused at the hash gate — the guards survived for a reason unrelated to #932, and reverting the fix did not fail it. It now scrapes the real hash from synth's own diagnostic so the hash gate passes and the floor check is the only thing left that can refuse.

That needed a product change too: the floor refusal reports the computed hash, which a consumer debugging a rejected document needs regardless.

Now potent — reverting the fix reproduces ACCEPTED … against the 0 B floor and the test fails.

Named residual

The truthful document for an imported memory is still refused, because the decoder discards the import's declared minimum (TypeRef::Memory(_)). Safe but not yet useful; tracked on #932 rather than left as a surprise.

Root cause of the miss

The #901 differential used a declared memory, so the imported shape was never exercised — the v0.53 lesson that a validator tests the shape it was written against.

clippy 0 · fmt 0 · 2/2 tests · red-first verified.

avrabe added 2 commits August 7, 2026 21:40
… (SECURITY)

For a module whose memory is IMPORTED the derived floor was

    all_memories.first().map(|m| m.initial_bytes()).unwrap_or(0)

and imported memories are not in `all_memories`, so `unwrap_or(0)` turned 'no
floor can be established' into 'the floor is 0 bytes'. Measured on v0.55.0:

    baseline, no --proven-safe .......... 2 udf guards
    memory_min_bytes = 0     ACCEPTED ... 0 udf guards   <-- guards STRIPPED
    memory_min_bytes = 65536 REFUSED .... 2 udf guards   <-- the TRUTH rejected

The fail-closed contract INVERTED: the honest document rejected, the vacuous one
stripping real guards, while synth printed 'proved 1 access site in-bounds
against the 0 B floor' — self-refuting, since no access is in bounds of a
zero-byte memory. An imported memory is real at run time, so that is an
unguarded access at an attacker-controlled offset.

The floor is now Option<u32> and None REFUSES. Absence of evidence is never
evidence of safety — the rule the rest of this seam states and this line broke.

MY FIRST TEST WAS VACUOUS, and mutation caught it: it used a dummy hash, so
every compile was refused at the HASH gate and the guards survived for a reason
unrelated to #932 — reverting the fix did NOT fail it. It now scrapes the real
hash from synth's own diagnostic so the hash gate PASSES and the floor check is
the only thing that can refuse. Re-verified: reverting the fix reproduces
'ACCEPTED ... against the 0 B floor' and the test fails.

That required a product change too: the floor refusal now reports the computed
hash, which a consumer debugging a rejected document needs anyway.

NO REGRESSION: a DECLARED memory still accepts a truthful document and elides
2 -> 0 guards against the real 65536 B floor, asserted by the second test so
this fix cannot be mistaken for deleting the feature.

NAMED RESIDUAL, not silent: the truthful document for an IMPORTED memory is
still refused, because the decoder discards the import's declared minimum
(TypeRef::Memory(_) -> ImportKind::Memory). Safe but not yet useful; carrying
the minimum through is tracked on #932.

ROOT CAUSE OF THE MISS: the #901 differential used a DECLARED memory, so the
imported shape was never exercised — the v0.53 lesson that a validator tests the
shape it was written against.

clippy 0, fmt 0, 2/2 tests, red-first verified.

Refs #932, RQ-56-PSAFE
The CI Test job failed with 'fixture must emit guards, got 0' — on macOS the
test passed, on the ubuntu runner it counted ZERO. The test parsed `synth
disasm` output for `udf` mnemonics.

That is a lesson already recorded in this repo and violated anyway: DISASSEMBLY
TEXT IS HOST-DEPENDENT — read structure, not rendering. Worse, I switched TO
disasm specifically to avoid an llvm-objdump host dependency, trading one host
dependency for the known-worse one.

Now uses synth's OWN reported `Total code size: N bytes`: its own number,
identical on every host, and it moves for exactly the reason under test —
eliding a bounds guard REMOVES instructions (24 B guarded -> 8 B elided).

Red-first RE-verified after the rewrite, and note the near-miss: the first
mutation attempt silently did not apply, because `cargo fmt` had reformatted the
anchor line — so the 'MUTATED' run was actually unmutated and proved nothing.
Asserting the replacement applied is what caught it. With the mutation genuinely
applied: 'code SHRANK 24 -> 8 bytes' and the test fails.

clippy 0, fmt 0, 2/2.
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe enabled auto-merge (squash) August 8, 2026 15:54
@avrabe
avrabe merged commit 810cddc into main Aug 8, 2026
57 checks passed
@avrabe
avrabe deleted the fix/proven-safe-imported-memory-932 branch August 8, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant