Skip to content

fix(f2): receipts keyed on (model sha256, executable sha256, device) — a different binary can never skip validation - #3808

Closed
noahgift wants to merge 4 commits into
mainfrom
PMAT-3748-f2-receipt-key
Closed

noahgift wants to merge 4 commits into
mainfrom
PMAT-3748-f2-receipt-key

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

#3748: F2 receipts keyed on (model sha256, executable sha256, device)

Closes #3748.

Receipts were keyed per MODEL only. A different apr binary at the same
version could skip F2 validation on another build's receipt, and every run
re-paid the 64-position CPU forward (2.4 s lambda / 10.1 s gx10) even when
nothing changed.

Fix (f2_receipt.rs): F2ReceiptKey{model_sha256, exe_sha256, device}.
decide compares all three and names the one that differs
(ExeSha256Mismatch). receipt_path is <model>-<exe16>-<device>.json, so
builds A→B→A never overwrite each other. Both hashes are cached by
FileIdentity{canonical path, size, mtime_ns, inode}
(cached_sha256/exe_sha256), re-stat after hashing, and fall back to
hashing on any cache failure. Schema bumps to 2. Every F2 line names its
source and the executable's sha16 ([source=receipt exe=…] /
[source=fresh exe=…]), and F2Outcome.exe_sha256 carries it for cells.

Shown on device, before and after (Qwen3.5-9B-Q4_K_M, fresh
APR_F2_RECEIPT_DIR, apr run --gpu, gpu-q --prio 1): before, binary X2
(X + one appended byte, same version) skipped F2 on X's receipt on both
lambda and gx10. After, at e771c8995: B (A + one byte) never skips (fresh,
7.4 s lambda / 9.3 s gx10); A→B→A never overwrite; a warm run hashes nothing
(0 ms, down from 2503/10048 ms cold); warm wall time fell 14.3→11.9 s
(lambda) and 27.7→14.8 s (gx10). Full tables in
docs/audits/impl-PMAT-3748-receipt.md.

Mutants (each applied, run, reverted; restored tree 22/22 green):

  • key on the version, not the executable → falsifier_a_planted_receipt_from_a_different_executable_revalidates, falsifier_the_same_version_from_another_executable_never_skips RED
  • key on the model only → builds_a_then_b_then_a_never_overwrite_each_other, a_second_device_is_a_second_receipt_file RED
  • stale cache after an in-place rebuild → the_hash_cache_hits_warm_and_misses_on_any_identity_change RED

Checks: cargo test -p aprender-serve --lib f2_receipt 22 passed ·
cargo test -p apr-cli --lib 7300 passed · cargo fmt --all -- --check ·
cargo deny check advisories · cargo test -p aprender-contracts --lib
1689 passed · cuda release build on both hosts.

Not in this row: recording source inside ladder/derived cells is the
judge's side (#3745 S2 / #3712 B2). The qwen3moe F2 (#3714) writes no
receipt and validates every run — unaffected.

Status: receipt-only, per the cop's batching default. Quorum lanes are
queued for when the model pool comes back (gemini/gpt-oss both out as of
this branch's push — see #3772). Not armed.

noahgift and others added 4 commits September 21, 2026 23:55
…r build's receipt can never skip validation (Refs #3748)

aprender-37's Qwen3.5 GPU correctness cells logged "CPU reference forward
skipped [source=receipt]" and measured nothing: the receipt key's
"apr version" was CARGO_PKG_VERSION, so every 0.69.0 build shared one, and a
receipt written by build A let build B skip F2 — a regressed GPU kernel
could serve behind another build's green receipt. Version + git sha would
still let a dirty tree, or another feature set, ride a clean build's receipt.

Per #3748's amended done_when 1:
- key = (model sha256, sha256 of the running executable, device); the
  version and git sha (apr sets its label at startup) are recorded beside
  the key for humans, never compared; schema 2, so every version-keyed
  receipt revalidates once;
- one receipt file per (model, executable, device): builds A, B, A never
  overwrite each other;
- both hashes cached by the file's (canonical path, size, mtime, inode) —
  a warm run reads nothing; a file rewritten mid-hash is never cached; any
  cache failure falls back to hashing;
- every F2 line names its source and the executable
  ([source=receipt exe=…] / [source=fresh exe=…]) and how each hash was
  obtained (cached | hashed); F2Outcome carries exe_sha256.

Mutants, each RED then restored GREEN: key on the version (exe comparison
dropped) → 2 falsifiers RED; key on the model only (one file per model) →
A→B→A and the per-device file RED; a stale cache after an in-place rebuild
(mtime ignored) → the identity table RED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ended key (Refs #3748)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…da and gx10 (Refs #3748)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3808 head=5c26c8ad8b5d1842202a08d4c8ff573583509a80 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift

Copy link
Copy Markdown
Contributor Author

closed by operator 1-day PR rule; reopen/rebase to resume.

No live owner session, red CI on the current head, not carried by main or any open integration PR. Linked issue: #3748 (open, milestone 0.70.0). Linked issues are not touched by this close.

Branch is NOT deleted: PMAT-3748-f2-receipt-key @ 5c26c8ad8.

@noahgift noahgift closed this Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind:code Work is a code change (derived rule, #4159) needs-owner Open work with no live owner — 0.70 batch triage; pick it up and reassign

Projects

None yet

1 participant