critical · 9.3CVE-2026-54496Jul 6, 2026

CVE-2026-54496: halo2_gadgets Variable-Base Scalar Multiplication Under-Constrained Base Point

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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…

Packagezebrad
Ecosystemrust
Affected<= 4.5.1
Fixed in5.0.0

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.

rust
// 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).

References: [1][2]

Related research