CVE-2026-54496: halo2_gadgets Variable-Base Scalar Multiplication Under-Constrained Base Point
A missing copy constraint in Zcash's halo2_gadgets ECC gadget let a malicious prover substitute an arbitrary base point into the Orchard Action circuit, bypassing the integrity check that ties a shiel
The problem
The variable-base scalar multiplication gadget in `halo2_gadgets/src/ecc/chip/mul/incomplete.rs` wrote the base point coordinates `(x_p, y_p)` into the circuit using `assign_advice()` at the first iteration of the double-and-add loop (L309-310, commit `32a87582`).
That call places a witness value in the constraint system but does not tie it to any previously committed cell.
The existing `q_mul_2` selector kept `x_p`/`y_p` uniform across loop rows, but it anchored them only to the first row's unconstrained witness. A malicious prover could therefore set `(x_p, y_p)` to any point they chose. The gadget enforces the Orchard diversified-address-integrity condition `pk_d = [ivk] g_d`; with a free base, an attacker could pick a fake nullifier key `nk`, derive a wrong `ivk` from it, then solve for the base point that makes `[ivk] g_d` equal the target `pk_d`, defeating the check entirely.
The result was undetectable double-spending or theft of funds protected only by an incoming viewing key.
Proof of concept
A working proof-of-concept for CVE-2026-54496 in zebrad, with the exact payload below.
// Vulnerable path: assign_advice does NOT create a copy constraint.
// Attacker controls (x_p, y_p) freely in the first loop iteration.
//
// incomplete.rs (pre-fix, ~L309-310)
region.assign_advice(
|| "x_p",
self.double_and_add.x_p,
row + offset, // first iteration row
|| x_p, // attacker sets this to a crafted point
)?;
region.assign_advice(
|| "y_p",
self.y_p,
row + offset,
|| y_p, // attacker sets this to a crafted point
)?;
// Exploit algebra (pseudocode):
// 1. Pick any fake nk (not the note's real nullifier key).
// 2. Derive wrong_ivk = Commit_ivk(Extract(ak), nk) -- circuit forces this honestly.
// 3. Solve for crafted_base such that [wrong_ivk] crafted_base == pk_d.
// (Feasible because ECDLP is over a known-order group; prover knows wrong_ivk.)
// 4. Set x_p = crafted_base.x, y_p = crafted_base.y in the witness.
// 5. q_mul_2 propagates the chosen base across all loop rows -> proof verifies.
// 6. nf = Nullifier(nk, ...) produces a fresh, valid-looking nullifier for the
// same note, enabling a repeat spend.CWE-345 (Insufficient Verification of Data Authenticity). In halo2's PLONKish constraint system, `assign_advice()` places a witness value into a cell but creates no permutation constraint linking that cell to any other. The `q_mul_2` custom gate constrained consistency across loop iterations but had no anchor to the actual base point passed in by the caller.
Replacing the two `assign_advice()` calls with `copy_advice()` inserts a copy constraint (a permutation argument) that forces the first-row cell to equal the cell holding the real base, which `q_mul_2` then propagates transitively to every row of the loop. Because changing the circuit changes the verifying key, the fix required a hard fork (NU6.2) to rotate the pinned key on-chain.
The fix
Upgrade to `halo2_gadgets` >= 0.5.0, `orchard` >= 0.14.0, `zcash_primitives` >= 0.28.0, `zebrad` >= 5.0.0 (NU6.2, activated block 3,364,600 on 2026-06-03), or `zcashd` >= 6.20.0. An interim soft-fork mitigation that rejected all Orchard-containing transactions shipped in `zebrad` 4.5.3 (block 3,363,426).
Downstream projects using `assign_advice()` to supply a value that must equal an already-committed cell should audit those sites and replace them with `copy_advice()` or an explicit equality constraint.
Reported by Taylor Hornby (independent, contracted by Shielded Labs).
Related research
- high · 7.1CVE-2026-35341CVE-2026-35341: uu_mkfifo Unauthorized Permission Overwrite on Existing Files
- high · 7.3CVE-2026-35338CVE-2026-35338: uu_chmod --preserve-root Bypass via Unresolved Path
- high · 8.1SurrealDB HTTP RPC Session Race Condition Allows Privilege Escalation
- high · 8.6CVE-2026-55441CVE-2026-55441: mise Tera exec() Injection via Untrusted Task-Include Files