Skip to content

fix(#944): verified machine-readable origins for compiler-introduced object branches — the 9 were never unattributed, the real 33 now are attributed - #967

Merged
avrabe merged 4 commits into
mainfrom
fix/provenance-gap-944
Aug 14, 2026
Merged

fix(#944): verified machine-readable origins for compiler-introduced object branches — the 9 were never unattributed, the real 33 now are attributed#967
avrabe merged 4 commits into
mainfrom
fix/provenance-gap-944

Conversation

@avrabe

@avrabe avrabe commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

#944 — the 9 "object branches with no WASM origin": triage, attribution, gate

The premise was false — in a good way

Reproducing the count from the committed evidence (pulseengine/gale benches/gust/drivers/mcdc/evidence/switch-thin.{witness,provenance}.json, joined exactly as witness object-disposition does, on (func_index, instruction_offset)) gives exactly the 9 keys from the issue — and every one of them is a provenance entry for a real source WASM op, not an object branch without origin:

# func off function source op kind in map object realization
1 6 740 wit_bindgen::rt::cabi_realloc br preserved unconditional B @ 0x4e
2 12 929 …fsm::Guest>::tick br preserved unconditional B @ 0xea
3 16 1251 …fsm::Guest>::run_switch br preserved unconditional B @ 0xdc
4 16 1292 …fsm::Guest>::run_switch select folded-predication predicated IT-move @ 0x11c — no branch at all
5 16 1357 …fsm::Guest>::run_switch br preserved unconditional B @ 0x18c
6 23 1842 …fsm::Guest>::mark_resumed select folded-predication predicated IT-move @ 0x12e
7 24 1960 _export_mark_swapped_cabi wrapper br preserved unconditional B @ 0xe0
8 25 2145 …fsm::Guest>::current_window select folded-predication predicated IT-move @ 0x13e
9 28 2166 (function with no instrumentable branch) br preserved unconditional B @ 0x2

Category verdict: none of the 9 is a miscompile, and none is even an unattributed compiler-introduced branch. They are 6 unconditional brs and 3 selects folded to predication — source ops synth's map records faithfully, which witness never instruments (its manifest carries only br_if / br_table_target / br_table_default / if_then / if_else). only_in_synth in witness's reconciler means "synth entry with no witness branch record"; its hardcoded divergence text — "(object branch witness never instrumented)" — is what turned "uninstrumented br/select" into "object branch with no source", and the issue's framing followed from that message. The 9→8→4 movement across the source variants in the follow-up comment tracks which br/select source ops survive each source-level simplification — consistent, and the "irreducible 4" are the br/select ops in cabi_realloc and the export wrappers. Witness-side classification (filter/label only_in_synth by the op field synth already emits) is witness#109 territory.

The REAL unattributed population — and its attribution

The genuinely compiler-introduced conditional branches on switch-thin are the 33 resolved: false entries in object_cond_branches (invisible to witness's join, surfaced only with a free-text guessing note). All 33 trace, via the encode-time line_map, to memory.fill / memory.copy lowerings. Reproduced and disassembly-verified on current main (ELF bytes via pyelftools+capstone, never synth disasm text), per introducing op:

introducing op branches verified shape verdict
memory.fill 1 loop bound test (cmp; bhs, zero-trip safe) (a) serves fill count semantics
memory.copy 3 overlap-direction test (cmp dst,src; bhi) + forward and backward copy-loop bound tests (a) serves memmove overlap semantics
i32.div_s 3 div-by-zero guard + INT_MIN/-1 overflow guard pair, each skipping a udf (a) WASM trap semantics
i32.div_u / i32.rem_s / i32.rem_u 1 each div-by-zero guard (a) WASM trap semantics
if 1 the decision's own conditional branch not introduced at all — a source decision v1 mis-bucketed

Probed-and-clean on the same path (no cond branches emitted): call_indirect, memory.grow, i64 div/rem (software), i64 shifts/clz/eqz/compares. No category (b) — no branch that should not exist — was found anywhere in this lane.

What this PR changes

  • crates/synth-core/src/provenance.rs
    • ObjectCondBranch gains an additive origin field (absent when None; witness's serde ignores unknown fields, wire format stays synth-provenance-v1): a kebab-case machine-readable origin derived from the op the branch's line_map entry traces to — never guessed. Closed, disassembly-verified vocabulary: bulk-memory-fill-loop, bulk-memory-copy-loop, division-trap-guard.
    • An introduced branch whose op family is NOT in the verified map stays origin: None with the op named in the note — unexplained-but-declared beats a confident wrong label (the issue's own follow-up demonstrated the plausible label being wrong).
    • if is now a covered source decision ("If" preserved entry; its conditional branch resolves). Bonus for the witness join: witness does instrument if_then/if_else, so covered if entries can now match instead of falling into no-provenance.
  • scripts/repro/provenance_introduced_944.wat + crates/synth-cli/tests/provenance_introduced_origin_944.rs — the gate:
    • unattributed floor = 0 on the fixture: every object conditional branch either resolves to a source decision or carries a named origin;
    • no excuse-widening: exact per-origin counts pinned (1 fill + 3 copy + 3/1/1/1 division) and a closed origin vocabulary — reaching 0 by blanket-labeling turns it red;
    • mutation-verified red-first: unmapping MemoryCopy → 3 UNATTRIBUTED panics; blanket-labeling (_ => Some("division-trap-guard")) trips the synth-core negative-control unit test.

No CHANGELOG/version/status.json changes (release-hub constraint). No doc-claim changes; claim_check stays 43/43.

Closes #944.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 4 commits August 13, 2026 20:30
…anches + cover 'if' as a source decision

The 9 'only-in-synth' divergences in #944 are provenance ENTRIES for real
source ops (6 unconditional br, 3 folded select) that witness's manifest
never instruments — not object branches without WASM origin. The genuine
compiler-introduced conditional branches on that module are the bulk-memory
expansion guards, previously surfaced only with a guessing free-text note.

- ObjectCondBranch gains additive 'origin': kebab-case, derived from the
  encode-time line_map (never guessed), only for disassembly-verified
  families: bulk-memory-fill-loop, bulk-memory-copy-loop,
  division-trap-guard. Unverified families stay declared-unattributed.
- WasmOp::If is now a covered source decision (preserved entry, resolved
  branch) — its conditional branch was mis-bucketed as introduced.
- Unit tests incl. the non-vacuity negative control (unverified family
  must NOT be labeled).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e API session limit

Committed by the hub coordinator so the work survives the interruption. NOT
reviewed and NOT complete: the lane stopped mid-task, so this is a restore
point, not a claim that anything is verified. The lane resumes from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ibuted floor 0, closed vocabulary

Integration gate over scripts/repro/provenance_introduced_944.wat
(cortex-m3 --all-exports --relocatable --emit-provenance):

- pins EXACT per-origin counts (1 fill-loop, 3 copy-loop, 3+1+1+1
  division guards) so an attribution loss OR an excuse-widening both
  turn red (mutation-verified: unmapping MemoryCopy -> 3 UNATTRIBUTED
  panics; blanket-labeling trips the synth-core negative control);
- asserts the if decision is covered+resolved, not bucketed as
  introduced;
- holds the module-wide unattributed floor at 0 with a closed,
  disassembly-verified origin vocabulary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.94737% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-core/src/provenance.rs 98.94% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit eea92e1 into main Aug 14, 2026
57 checks passed
@avrabe
avrabe deleted the fix/provenance-gap-944 branch August 14, 2026 04:17
avrabe added a commit that referenced this pull request Aug 14, 2026
…MS (#970) (#972)

Six artifacts still read `proposed` while their work is on main. Re-graded to
`implemented`, so the release-readiness query means something:

  RQ-57-SKIPEXIT   #952  (#960)   declined REQUESTED export exits non-zero
  RQ-57-PROVGAP    #944  (#967)   verified origins for introduced branches
  RQ-57-DOCSWEEP   #946  (#968)   Tiers 2+3 honesty sweep
  RQ-57-SPWILD     #946  (#969)   writes_sp 175-of-222 wildcard expanded
  RQ-57-A64PARAM   #851  (#971)   aarch64 written-param homing
  RQ-561-ZEROMEM   #953  (v0.56.1) shipped two releases ago, never re-graded

NEW: RQ-57-COUNTPARAMS (#970). It has no artifact because nobody planned it —
the #851 lane found it while fixing the aarch64 instance of the same shape.
An unplanned finding with no artifact is invisible to the release query, which
is exactly how #933 slipped a release, so it gets one now.

What it records that the issue alone does not:

  * The severity differs by BACKEND and both halves are stated at the
    confidence they were established. RISC-V is EXECUTED-confirmed: with a
    poisoned stack the function returns 0xDEADBEEF — an UNINITIALISED stack
    slot, i.e. previous frame contents. That is information disclosure, not a
    wrong value. ARM's exact wrong value was INFERRED FROM DISASSEMBLY, never
    executed; the artifact says so, because the #851 lane's first draft stated
    it as measured and the advisor caught it.
  * ARM is only INCIDENTALLY correct on the simple shape (a merge-point
    `str r1,[sp]` catches the still-live param) and breaks once a call
    clobbers the param register — so "ARM looks fine" is not evidence.
  * Red-first must be per-backend BY EXECUTION. Assuming one backend's
    evidence transfers is what left this latent after the aarch64 fix.
  * The named residual survives: the `None` branch of
    `current_func_param_count` still uses the unsound heuristic — unreachable
    from the CLI, reachable via the direct `compile_function` API.

Per the user's direction this ships in v0.57 rather than as a v0.56.3 patch.

rivet: errors unchanged (50 before and after); warnings 164 -> 166, which is
the two every artifact in this file carries (trailer-reference naming, and the
`verifies` link that lands with the test) — verified identical for
RQ-57-SPWILD and RQ-57-GPIO, so this is one more artifact of the same shape,
not a new defect. claim_check 43/43.

Refs #970


Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Aug 14, 2026
* chore(release): v0.57.0 assembly — "the checkers were the defects"

Nine artifacts. In five of them the bug was in the machinery that checks the
compiled code, not in the compiled code:

  #975  ArmSemantics silently no-oped 87 of 222 ops — a Rocq-proved,
        default-on rotl rule was "validated" by a model that executed neither
        of its instructions
  #976  the gpio differential CANNOT discriminate the miscompile it guards —
        complementary conditions, so no input to that driver can
  #969  writes_sp claimed exhaustiveness over a wildcard absorbing 175 of 222
  #967  the "9 unattributed branches" were manufactured by witness's own
        hardcoded divergence text
  #979  the prescribed release: back-fill would have written 32 false entries,
        with the one correct pre-existing value beside them as the disproof

The unifying property is that each of those checks COULD NOT FAIL. This
release makes them able to fail and proves it by making them fail on purpose.

Also fixed, and the most severe item: #974 — a conditionally-written parameter
was demoted to a zero-init local on ARM and RISC-V. Exit 0, no decline, wrong
code; on RISC-V it reads an UNINITIALISED stack slot (0xDEADBEEF under a
poisoned stack), an information-disclosure shape. ARM behaves identically —
which the issue predicted otherwise, and only execution settled.

Release surfaces, all four swept and checker-confirmed at 0.57.0:
  Cargo.toml [workspace.package] + 10 path-dep pins
  MODULE.bazel, npm/package.json, Cargo.lock (cargo metadata)
  scripts/check_version_pins.py: OK

Derived artifacts regenerated (--emit-status): artifacts/status.json,
docs/status/FEATURE_MATRIX.md. Claim gate: 43/43.

Open by design, named not hidden: #973 (ARM select miscompile, found only
because a lane compiled ARM fixtures — which CI never does), #977 (ELF-magic
flake, second sighting), #938 (breaking object 0.x-minor bump, auto-merge
disabled), #912 (open with four remaining: items).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* fix(release): act on the v0.57.0 cold review — 8 accuracy defects, 4 of them mine

Cold review of the assembled release. Nothing blocked the tag; everything below
is accuracy. Four of the eight were errors in the CHANGELOG I had just written,
which is the reason the review exists.

THE GENERALIZABLE FINDING, and it is pointed given this release's theme:
`check_generated_fresh` byte-compares the RENDERED FEATURE_MATRIX against the
TEMPLATE. `render_feature_matrix` only substitutes `{{...}}` fields, so the gate
proves the render is faithful to the template — and NEVER that the template is
faithful to the code. Every stale number below lives in template prose no
substitution touches. In a release titled "the checkers were the defects", that
is the checker that cannot fail. Three independent stale numbers survived a
green 43/43.

USER-FACING FALSE, verified by compiling rather than by reading:
  FEATURE_MATRIX listed "writing a PARAM local in a LEAF function" as a LOUD
  DECLINE on aarch64. #971 shipped exactly that. A leaf `local.set` on a param
  compiles: 32 bytes of machine code, exit 0. Also corrected in the same row:
  homing is no longer non-leaf-only, and the float-param decline widened with
  it. Fixed in the TEMPLATE (the render is generated) + regen.

MY CHANGELOG ERRORS:
  * "145 of the 175 pre-declined / 30 reachable" matched no partition. The
    shipped source (wcet_loops.rs:1232) says 142 give up with `true`, leaving
    33. Re-derived: 142/33. Corrected.
  * "demoted to a zero-initialised local" is wrong for the two backends the
    entry is about — zero-init is gated on first-access-being-a-READ, and in
    the cond-write shape the first access IS the write, so nothing initialises
    the slot. That is WHY it reads poison; the old wording made an
    information-disclosure bug sound like a benign wrong value, and contradicted
    the entry's own next sentence.
  * "Nine artifacts" — there are ten, and RQ-57-DOCSWEEP (#946/#968) had NO
    CHANGELOG entry at all despite touching CLAUDE.md, coq/STATUS.md,
    PROJECT_STATUS.md, the matrix template and eight source files. Added.
  * "Five in-tree oracles took that opt-in" — eight scripts plus three Rust
    tests. All eight carry floors, but `i64_param_518_riscv_loudskip`'s is
    `compiles >= 1`, which is a floor and NOT the "tight" one the paragraph
    claimed for the set. Named rather than folded into the claim.

STALE COUNTS (the template-prose class above):
  ORACLE_WIRING.md, the matrix template and claims.yaml all said "137 oracles /
  295,621 emulator entries". Re-derived independently — and the reviewer's
  number and mine agree exactly: 144 oracles / 296,059. Both `count-min` pins
  moved 137 -> 144 with them (same `emulations >=` pattern, two sibling claims);
  the pinned verbatim texts moved too, or the ledger would have gone red
  against its own corrected doc.

REVERSE STALENESS (a doc calling SHIPPED work missing):
  `synth verify` declines shift rules citing "SMT modeling of the variable-shift
  register encoding is an open gap". #975 CLOSED that gap — it modelled
  LslReg/LsrReg/AsrReg/RorReg as Rm<7:0> (ARMv7-M A7.7.68/70/12/117) and moved
  five lowerings Invalid -> Verified. Both comments corrected to say what is
  true: the modelling gap is closed, the remaining decline is a WIRING residual.
  Behaviour deliberately unchanged — rewiring the rule table is a
  verification-surface change, not release assembly. Filed as #981.

ARTIFACT:
  RQ-57-PROVGAP still asserted "9 object branches with no WASM origin" as fact
  while its own PR disproved it. Outcome recorded, as RQ-57-BACKFILL already did.

  docs/architecture/CRATE_STRUCTURE.md said 18 crates; there are 19. A
  RECURRENCE — PROJECT_STATUS.md cites this exact drift as why it was gutted in
  the #946 sweep, and one file over it was live again.

Gates after: claim_check 43/43, check_version_pins OK at 0.57.0,
cargo check -p synth-cli rc=0.

Refs #980, #981

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* style: rustfmt the #975 decline-reason comment (indent 14 -> 12)

My own miss: I ran cargo check on the edited file but not cargo fmt, and
Format is a required context. The comment content is unchanged — only the
indentation rustfmt wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

provenance: 9 object branches with no WASM origin on a verified isolation module — object-code obligations nothing can discharge

1 participant