RQ-59-STARTFN (#1046): the (start ...) section was silently discarded by ALL THREE backends — decode it and refuse loudly; fourth silent drop (globals inits, #1052) found and filed - #1053
Merged
Conversation
…le, asserted as a loud refusal naming the start section All 7 refusal cases FAIL on current main (exit 0, no warning, start function absent from the object); the start-free control passes. The assertions deliberately name the refusal (non-zero exit + reason string containing 'start function' and '#1046'), NOT the absence of invocation — which was already true on the broken behaviour and is exactly why the bug survived every existing test. Refs #1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ery compile path — the decoder had no StartSection arm at all The section fell through the catch-all and was discarded outright: all three backends compiled a (start)-carrying module, exited 0, printed no warning, and the start function was not even in the object. wasmtime runs it at instantiation (WASM Core §4.5.5) and returns 42 on the repro; synth-compiled code read never-initialized memory and returned 0. - wasm_decoder: Payload::StartSection arm + DecodedModule.start_function - main.rs: refuse_dropped_start_function at all three compile decode sites (module path, single-function path, WAST merge path) — non-zero exit + a reason naming the start section, the #851/#1041 refusal shape - deliberately NO escape hatch: the start function's code is not in the artifact, so no embedder contract can honor it (unlike #1041's --embedder-data-init, where the embedder holds the bytes) - start-function INVOCATION is a capability follow-on, not this fix 0 fixtures/harnesses affected (no .wat/.wast/.wasm in the repo carries a start section — swept textually AND by binary section id); emitted bytes for start-free modules are unchanged (guard only fires on Some), frozen anchors untouched; EXPECTED_DECLINES (#992) unchanged (keys on scripts/repro/*.wat, none of which carry (start)). Red-first: the 7 refusal tests in start_section_refusal_1046.rs failed on the parent commit and pass now; the start-free control passes throughout. Gates: fmt, clippy -D warnings, cargo test --workspace (true exit 0), claim_check 50/50, model_coverage_audit ok, rivet validate 50 errors (= main baseline, #1012). Refs #1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Aug 26, 2026
…inks, and a duplicate `issue:` key broke the typed parse My conflict resolution spliced RQ-59-I64SHIFT INTO the middle of RQ-59-DATASEG's mapping — between its `tags:` and its `links:`/`fields:` — so DATASEG silently lost its trace links to BR-001 and I64SHIFT inherited them, and appending `issue: "#1048"` produced a DUPLICATE KEY next to `issue: "#1041"` in one mapping. WHY THE CHECK MISSED IT: I validated the resolution with `yaml.safe_load`, which SILENTLY KEEPS THE LAST VALUE on a duplicate key and reported "YAML OK". rivet's serde-typed loader rejects it, and reports the position of the FIRST top-level key (`unknown field 'metadata' ... line 56`), which reads like a header problem 700 lines away from the actual fault. A permissive parser is not an oracle for a typed schema. Baseline-compared rather than read absolute (rivet FAILS on main by construction — the federated half is unresolved, #1012): main: 50 errors, OURS=0 before this fix: 51 errors, OURS=1 <- the parse error after this fix: 50 errors, OURS=0 <- back at baseline Also flips RQ-59-STARTFN proposed -> implemented: its fix merged as c960903 (#1053) with a real `Payload::StartSection` decoder arm and a refusal test. RQ-59-I64SHIFT stays `proposed` — PR #1054 is still open. Verified with a duplicate-key-STRICT YAML loader as well as rivet: 16 artifacts, no duplicate ids, all three of DATASEG/I64SHIFT/STARTFN carry their own links and the right issue number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe
added a commit
that referenced
this pull request
Aug 26, 2026
…59-STARTFN (#1046) (#1050) * plan(v0.59): RQ-59-POPCNT implemented; scope two more shipped miscompiles found by the census lanes POPCNT: #1039 merged — i32.popcnt no longer clobbers R11. Verified live on main: the expansion runs on R12 alone and BOTH backends now carry a loud guard ("Popcnt destination must be R0-R10: R11 is the linear-memory base"). TWO NEW FIX-FIRST ARTIFACTS, both shipped miscompiles, both found by lanes that were scoped to MEASURE rather than to hunt: RQ-59-I64SHIFT (#1048) — the i64 variable-shift expansions clobber their own shift-amount operand. Thumb-2 and A32 both mask the amount IN PLACE (& 63) and write amt-32 into the amount's home HIGH register, so re-reading the amount after i64.shl/shr_u/shr_s returns a mangled value. Executed, both selectors: amt=64 returns 1 where 65 is correct; amt=67 returns 11 where 75 is correct. Same class as #1021, DIFFERENT REGISTER TIER: #1021 clobbered a reserved global (R11); this clobbers the CALLER'S OWN OPERAND. That is why TIERCENSUS's reserved-register subset was {Popcnt} and still did not bound the risk — the operand tier is outside what that subset measures. And the observation that matters most: the wired i64_shr_599 differential PASSES on the very binary where this miscompiles. The oracle is structurally blind to the class — which is the argument for VCR-TIER-001's canary gate (v0.60), a guard that would have caught BOTH. RQ-59-STARTFN (#1046) — all three backends silently drop the (start) function. The decoder has NO Payload::StartSection arm. wasmtime returns 42 on the repro; synth-compiled code returns 0, exit 0, no warning, on ARM, RISC-V and AArch64. THIRD silent-drop of the same shape in one session, after #1041 (ARM relocatable discarding active data segments) and alongside #1048. Minimum fix is a loud refusal matching the aarch64 #851 style; implementing start-function invocation is capability work and not this artifact. The DATASEG lane also reported two softer same-class observations (non-zero global inits, active element-segment table images) that may sit inside the documented R9/R11 embedder contract — the lane must determine which side each falls on, and a fourth silent drop would outrank the fix. v0.59 scope is now 15 artifacts. claim_check green; rivet validate 50 errors, identical to main's #1012 baseline (adds none). Refs #1021, #1048, #1046, #242. * fix(rivet): repair the RQ-59-I64SHIFT splice — DATASEG had lost its links, and a duplicate `issue:` key broke the typed parse My conflict resolution spliced RQ-59-I64SHIFT INTO the middle of RQ-59-DATASEG's mapping — between its `tags:` and its `links:`/`fields:` — so DATASEG silently lost its trace links to BR-001 and I64SHIFT inherited them, and appending `issue: "#1048"` produced a DUPLICATE KEY next to `issue: "#1041"` in one mapping. WHY THE CHECK MISSED IT: I validated the resolution with `yaml.safe_load`, which SILENTLY KEEPS THE LAST VALUE on a duplicate key and reported "YAML OK". rivet's serde-typed loader rejects it, and reports the position of the FIRST top-level key (`unknown field 'metadata' ... line 56`), which reads like a header problem 700 lines away from the actual fault. A permissive parser is not an oracle for a typed schema. Baseline-compared rather than read absolute (rivet FAILS on main by construction — the federated half is unresolved, #1012): main: 50 errors, OURS=0 before this fix: 51 errors, OURS=1 <- the parse error after this fix: 50 errors, OURS=0 <- back at baseline Also flips RQ-59-STARTFN proposed -> implemented: its fix merged as c960903 (#1053) with a real `Payload::StartSection` decoder arm and a refusal test. RQ-59-I64SHIFT stays `proposed` — PR #1054 is still open. Verified with a duplicate-key-STRICT YAML loader as well as rivet: 16 artifacts, no duplicate ids, all three of DATASEG/I64SHIFT/STARTFN carry their own links and the right issue number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A FOURTH silent drop was found and FILED first — it outranks this fix
#1052 — the ARM plain-relocatable path silently drops nonzero GLOBAL INITIALIZERS, and it is OUTSIDE the documented embedder contract (verdict + executed repro below). Filed with the repro executed on the v0.59 tree per the RQ-59-DATASEG lane's open-observation handoff. The element-segment sibling observation resolves the other way: inside the contract, language quoted below.
Closes the third silent drop of the session: all three backends discarded the
(start ...)section — the decoder had noPayload::StartSectionarm at all, so the section fell through the catch-all. #1041 (data segments), #1046 (this), #1048 (i64-shift operand): synth accepted input, discarded part of its semantics, and reported success.Start-function INVOCATION is deliberately NOT this artifact — it is a capability question (the self-contained Reset_Handler could call it before any export; the relocatable contract would need an exported init hook). This PR is the honest-frontier minimum: refuse loudly, the #851/#1049 house shape.
Rivet:
RQ-59-STARTFN— the artifact entry lives on PR #1050 (plan/v59-i64shift-startfn), not yet on main; this branch is off main and does not duplicate it.Red-first transcript
Before (main, 18a4786) — the filed repro (
(start $init)writes 42 to memory 0; wasmtimeget() == 42):After (this branch):
Same refusal on ARM self-contained, cortex-r5 (A32), RISC-V, AArch64, the single-function path, and the WAST merge path.
The assertion shape is the point (commit 1, red on its parent): the test asserts a non-zero exit + a reason string naming the start section — it deliberately does NOT assert "the start function was not called", which was already true on the broken behaviour and would have been vacuously green. All 7 refusal tests failed on main; the start-free control passed throughout; all 8 pass now.
No escape hatch — deliberately unlike #1049's
--embedder-data-init: the start function's CODE is not in the artifact (reachability walks exports only), so there is nothing an embedder could call. A(start)naming an exported function still refuses (nothing invokes it at instantiation; test pins this).Verdict on the two open RQ-59-DATASEG observations
1. Nonzero global initializers on the plain relocatable path — OUTSIDE the contract. Fourth silent drop. Filed as #1052.
Executed repro:
(global $g (mut i32) (i32.const 42))+global.getexport,--relocatable --target cortex-m4→ exit 0, and the whole object ispush {r4,lr}; ldr.w r0,[r9]; pop {r4,pc}— 0x2A appears nowhere in the ELF, no.data, no__synth_globals. wasmtime: 42.What the contract actually documents is the BASE, not the values: the selector says "R9 = globals base"; the only relocatable-path globals harness (
i64_globals_643_differential.py) maps "a zeroed scratch region" and annotates it "# zeroed globals table (inits are 0)" — correct only because that fixture's inits ARE zero. There is no sentence anywhere assigning initializer evaluation to the embedder — contrast the data-segment sentence that exists ("the embedder populates its init segments",multi_segment_static_data_differential.py). Every other path handles inits explicitly: ARM self-contained materializes them at reset (#649 — which treated exactly this class as a BUG),--native-pointer-abiships them as.data, aarch64 ships__synth_globals("explicitly NOT preconditions: synth EMITS both" — FEATURE_MATRIX), RV32 loud-skips. The plain relocatable path alone neither ships, nor declines, nor documents.2. Active element-segment table images — INSIDE the contract.
call_indirect_594_differential.py: "ArmOp::CallIndirect contract (same as the Thumb-2 path): R11 holds the function-pointer table base; entry i is a 4-byte code address. The harness builds that table with func_0's address"; encoder: "Table base setup must be done by caller/runtime." The table entries are final linked code addresses that cannot exist in an ET_REL object as runtime-RAM contents; the object exports everyfunc_Nthe segments name (verified on the executed elem repro:func_0,func_1,pickall in the symtab), and the embedder holds the module's segment layout. Building the table is the integrator's documented step. (One-line residual noted in #1052: the #676 type-id sidecar's structural-class-id derivation rule is documented only inwasm_decoder.rsdoc comments.)Decoder section class sweep (
decode_wasm_module)memory.init/data.drophitconvert_operator → None→ per-function LOUD-SKIP with the op named (#369 lane) — semantics unreachable without the opsthrow/try/... loud-skip per function via the same #369 lane — a tag with no using op is inertname, non-wsc.facts)(component)binary ERRORS ("No exported functions found in module") — not silent, but the diagnostic is misleading; components route through synth-frontend. Residual, not a dropThe defect class was specifically: a section whose semantics are reachable with NO operator involvement (start, active data, global inits) — those are invisible to the #369 op-level loud-skip lane and need section-level handling. Start was the last unhandled one in this decoder; global inits (#1052) are its sibling one layer up (decoded but never emitted on one path).
Harness impact
0 harnesses/fixtures affected. Swept three ways:
grep -E '\(start[[:space:]]+[$0-9]'over all.wat/.wast/.rs/.py(only this PR's own test), a binary scan of all 21 in-repo.wasmfiles for section id 8 (none), and the full workspace suite (green).EXPECTED_DECLINES(#992) unchanged — it keys onscripts/repro/*.wat, none of which carry(start). No escape-hatch flag needed (and none would be honest — see above). Frozen anchors: emitted bytes for start-free modules are untouched (the guard only fires onSome); all frozen-fixture differentials in the suite pass.synth verify(analysis, no artifact) is deliberately not gated, matching #1041's compile-only scope.Gates
cargo fmt --allclean;cargo clippy --workspace --all-targets -- -D warningsgreencargo test --workspaceTRUE exit 0 (147 suites ok, 0 failures)python3 scripts/claim_check.py claims.yaml50/50;python3 scripts/model_coverage_audit.py --checkok (separate runs)rivet validate: FAIL (50 errors) — identical count to main (rivet externals are declared against a volume that does not exist — the federated half of the trace graph is never validated #1012 baseline)Refs #1046. Fourth-drop filing: #1052.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L