fix(verifier): reject unexpected precomputed commitment for normal AIRs - #906
fix(verifier): reject unexpected precomputed commitment for normal AIRs#906diegokingston wants to merge 1 commit into
Conversation
A proof for a non-preprocessed AIR could include an optional precomputed trace commitment. The verifier neither rejected nor absorbed that root before sampling Fiat-Shamir challenges, yet later authenticated and used the corresponding openings in verify_trace_openings and the DEEP composition reconstruction. A malicious prover could therefore choose security-critical trace columns (e.g. bitwise) after learning the challenges. The normal-table branch of the round-1 replay now rejects any proof carrying a precomputed commitment. Honest provers never set it for non-preprocessed AIRs, so rejection is safe. The fix lives in multi_verify_views, the single implementation shared by multi_verify and multi_verify_archived (recursion guest).
|
Reviewed adversarially — a prosecutor and a defense working the premise independently, then reproduced end to end. The issue is real, the fix is correct and load-bearing, and the regression test is vacuous and needs replacing. Details and a ready-made replacement below. The issue is real — confirmed by executionA forged proof is accepted by the stock Controls discriminate, so the harness is not simply accepting everything: The only non-test change is attacker-side: a The mechanism is a re-partition, not extra columns — worth correcting in the PR bodyExtra columns are impossible, and there are two real guards:
But nothing pins The exploit then follows from what the composition polynomial actually proves — that a random linear combination of constraints vanishes, not that each does:
Every individual constraint is violated while the combination vanishes identically, so the quotient really is low-degree and FRI/DEEP/OOD all pass honestly. The PR body's framing ("choose security-critical trace columns after learning the challenges") is right, but it undersells it: this is arbitrary constraint forgery for any non-preprocessed table, not merely unbound extra columns. Scope note (inferred from reading shared code paths, not executed at VM level): all VM tables except
|
| Is the issue real? | Yes — forged proof accepted by stock main, reproduced independently |
| Is the fix correct? | Yes — rejects the forgery; correctly placed in the shared multi_verify_views |
| Is the test adequate? | No — passes on unpatched main, proves nothing |
| Severity | High — arbitrary constraint forgery for any non-preprocessed AIR |
Method: adversarial prosecutor/defense on the premise, then the PoC re-run by me in a separate clean worktree with verifier.rs verified stock before running. My first run showed rejection because the prosecutor's worktree still had the fix applied — worth mentioning so nobody repeats that and concludes the opposite.
|
Corrections and an upgrade to my review above, after an independent adjudication pass re-ran everything. One item I posted was factually wrong — correcting it publicly. The verdict on the PR is unchanged: the issue is real, the fix is correct, the test needs replacing. Upgrade: severity is Critical, and it is now executed rather than arguedMy review demonstrated an invalid witness accepted. That is weaker than it sounds, because for It is closed now, by execution on a stock The verifier accepts "there is a valid 2-col Fibonacci execution of length 16 with a0=1, a1=1 and public output 999" when the only such execution outputs 2178309. Both polarities controlled; with this PR's hunk applied it flips to Why it generalises, and why this is not a Fibonacci artefact: ❌ Correction: my scope note was wrongI wrote that all VM tables except
So the preprocessed set is {bitwise, decode, keccak_rc, register, page-except-private-input}. Still vulnerable: CPU, CPU32, LT, SHIFT, MEMW, MEMW_ALIGNED, MEMW_REGISTER, LOAD, STORE, MUL, DVRM, BRANCH, EQ, BYTEWISE, HALT, COMMIT (which carries the public output), KECCAK, KECCAK_RND, ECSM, ECDAS, private-input PAGEs, and in continuations L2G ( The conclusion is unchanged — the execution core is in scope — but the sentence as I wrote it was wrong. Also, for VM AIRs Entry points, corrected and extended: ❌ Correction: my wording on the vacuous test was rebuttableI wrote "the new check never executes in this test". That is imprecise — on a patched build the new check does fire and is the reason for rejection. The accurate and load-bearing claim is:
The finding stands (it is not a regression test), but state it that way; as I first wrote it, the rebuttal would have been correct. One practical note on replacing the testWorth saying up front, because it is the main friction: any behavioural regression test here needs a small The cheapest discriminating test is not the forgery at all: it is an honest trace under a split declaration ( One thing that survives the fix, worth its own lookThe same defect class exists on the main↔aux split and this PR does not close it. Constraint enforcement survives (aux is bound before num_precomputed == if air.is_preprocessed() { air.num_precomputed_columns() } else { 0 }…checking both For the record, that also resolves a question I raised above: the preprocessed branch does not need an analogous check, but not because the hardcoded root pins the leaf width — it doesn't. It is because on that branch both roots are absorbed at Everything else in my review re-derived cleanly: the re-partition mechanism, both guard citations, |
|
Superseded by #909, which closes this defect class generally rather than this one shape. #909 pins all three trace-opening widths to the AIR (precomputed, main, aux — in both the regular and symmetric slot), once per table, before any opening is read. Relative to this PR:
The diagnosis in this PR was correct and the fix works — I verified it rejects the forgery. The only reason to close rather than merge both is that #909 makes this guard redundant, and one authoritative check reads better than two partial ones. One thing worth carrying over regardless of what happens here: this PR's regression test passes on unpatched |
A proof for a non-preprocessed AIR could include an optional precomputed trace commitment. The verifier neither rejected nor absorbed that root before sampling Fiat-Shamir challenges, yet later authenticated and used the corresponding openings in verify_trace_openings and the DEEP composition reconstruction. A malicious prover could therefore choose security-critical trace columns (e.g. bitwise) after learning the challenges.
The normal-table branch of the round-1 replay now rejects any proof carrying a precomputed commitment. Honest provers never set it for non-preprocessed AIRs, so rejection is safe. The fix lives in multi_verify_views, the single implementation shared by multi_verify and multi_verify_archived (recursion guest).