Summary
A set of independent soundness bugs were found across gnark's standard
library gadgets. The common thread is hint outputs that are consumed by the
constraint system without a binding or range constraint, letting a malicious
prover substitute forged witness values that satisfy every constraint while
encoding a false statement. Each bug is independently exploitable; several
compose into the building blocks of higher-level protocols (KZG openings,
Pedersen commitments, BLS signatures, EVM precompiles, recursive verifiers,
hash-based transcripts).
Affected consumers include std/commitments/kzg, std/commitments/pedersen,
std/signature/bls, std/signature/ecdsa, std/evmprecompiles (MODEXP,
BLS12_G1MSM, KZG point evaluation), std/recursion, and the SHA-2 / SHA-3 /
Keccak / RIPEMD-160 hash gadgets built on std/math/uints.
Fixed vulnerabilities
1. Unconstrained carry in emulated-field deferred checks — CRITICAL
Component: std/math/emulated (Mul, MulMod, AssertIsEqual, Reduce,
Div, Inverse, Sqrt, Eval, ModMul, ModAssertIsEqual), transitively all
of std/algebra/emulated and std/evmprecompiles.
The deferred multiplication / zero / evaluation checks verify
a(X)·b(X) = r(X) + k(X)·p(X) + (2^t − X)·c(X) at a random challenge γ in the
native field q. The carry polynomial c came from a hint and was never
range-checked. A malicious prover picks the quotient k and carry c so the
two sides differ by a multiple of the native modulus q; that difference
vanishes mod q at every challenge, so the Schwartz–Zippel check passes
identically for a false statement. Demonstrated end-to-end (setup → prove →
verify) on BN254, BLS12-377, BLS12-381 and BW6-761, on both Groth16 and PLONK.
Fix: per-call honest carry bounds (mulCarryBound / mvCarryBound), a
batched signed range check over all committed carry limbs inside the deferred
multicommit callback, a tightened overflow ceiling
(maxOverflowReducedResult), and a hard carryMaxBits guardrail for the
previously-silent extreme regime.
2. BLS12-377 torus PairingCheck is vacuous — CRITICAL
Component: std/algebra/native/sw_bls12377 (torus-based pairing), reached
from std/commitments/kzg, std/commitments/pedersen, std/signature/bls.
The torus scaling factor s was read from a hint and never constrained. The
single E6 verification equation is affine-linear in s, so a prover solves
for s for any P, Q, making the pairing check accept false statements.
Breaks KZG/Pedersen openings and BLS signature verification in the
BLS12-377-in-BW6-761 recursion stack.
Fix: the torus-compressed path was removed; the check uses the
over-determined E12 form in which s is fully constrained.
3. Grumpkin scalar multiplication: GLV decomposition not bound to the scalar — CRITICAL
Component: std/algebra/native/sw_grumpkin (ScalarMul, ScalarMulBase,
MultiScalarMul, joint GLV).
The GLV sub-scalars s1, s2 were tied only to an unconstrained hint value
semu (s1 − λ·s2 ≡ semu), never to the actual scalar variable s. The
circuit computed [s1 − λ·s2]Q for arbitrary prover-chosen s1, s2 — an
arbitrary multiple of Q unrelated to s. Verified: ScalarMul(P, 12345)
proven equal to [999999]P.
Fix: recompose the scalar in the native field from the canonical bit
decomposition and bind semu to s (api.ToBinary → FromBits), eliminating
the free hint value.
4. Variable-modulus remainder not reduced mod the runtime modulus — CRITICAL
Component: std/math/emulated (ModMul, ModExp, ModAssertIsEqual),
reachable via std/evmprecompiles/05-expmod.go (EIP-198).
For custom-modulus operations the remainder limbs were range-checked against the
type modulus width, not the runtime modulus m (a witness). A prover
returns r' = (a·b mod m) + m, congruent to the true result and passing every
check but non-canonical. Even the honest hint could return r + m
(malleability). Breaks RSA/CRT/verification logic that assumes a unique
representative.
Fix: Field.assertLessThanModulus performs a runtime-modulus bound check;
applied to ModExp's output and exposed as Field.ModMulCanonical.
5. BLS12-381 scalar multiplication admits a small-torsion (cofactor) offset — CRITICAL
Component: std/algebra/emulated/sw_emulated
(scalarMulGLVAndFakeGLV), reachable via std/evmprecompiles/12-blsg1msm.go
(EIP-2537) and any BLS12-381 G1 scalar mul.
The GLV+fake-GLV check verifies [V](Q − [s]P) = O on the full curve
E(Fp), whose order is h·r with h divisible by 3² for BLS12-381 G1.
V ≢ 0 mod r does not force the torsion component of Q − [s]P to vanish, so a
prover returns [s]P + T₀ for a 3-torsion point T₀ and passes all
constraints.
Fix: cofactor-clearing subgroup binding (CofactorClearing constants,
widened to all reachable primes), and routing all BLS12-381 G1 public paths
through classic GLV whose computed output is inherently in-subgroup.
6. GLV Φ-table carries the wrong sub-scalar sign — CRITICAL
Component: std/algebra/emulated/sw_emulated (scalarMulGLV and
jointScalarMulGLVUnsafe), reachable on BLS12-381 G1 (complete mode) and all
GLV curves in incomplete mode.
The Φ-table entries were built by re-signing the non-Φ table, so the [3]Φ(Q)
entry picked up the sign of the first sub-scalar s1 as a spurious factor.
Because the circuit only pins ±s1 + λ·(±s2) ≡ s (mod r) with both
|si| < 2^nbits, a prover selects a malicious lattice representation with
s1 < 0 and gets a wrong accumulator accepted. Verified end-to-end on BLS12-381
G1.
Fix: tablePhiQ[2] is now derived from triple(tablePhiQ[1]) so each Φ
entry carries only its own sub-scalar's sign.
7. AssertFinalExponentiationIsOne accepts the all-zero residue witness — HIGH
Component: std/algebra/emulated/sw_bn254 and
std/algebra/emulated/sw_bls12381 (and the classical native BLS12-377 path).
The check verifies x · cubicNonResiduePower == residueWitness^λ but never
constrains residueWitness ≠ 0. The all-zero hint satisfies 0 == 0 for any
x, so the exported method proved nothing about x.
Fix: the residue witness is constrained invertible (Inverse, asserting
w·w⁻¹ == 1).
8. Byte-wise ops (uints) result not range-checked — table-key collision — HIGH
Component: std/math/uints (twoArgFn, Not), consumed by SHA-2, SHA-3 /
Keccak, RIPEMD-160, BLS-signature and EVM-KZG gadgets.
twoArgFn packs the table key linearly as a + 2^8·b + 2^16·c and trusted that
the lookup result is in range. The log-derivative argument only constrains
membership of the packed value, which cannot distinguish an in-range c from
an out-of-range c that collides with a valid key: Xor(1,0) committed as
1/256 gives 1 + 0 + 2^16/256 = 257 = 0x000101, the valid key for
(a=1,b=0,c=1); And(1, 1/256) := 0 gives 2 = 0x000002, the valid key for
(1,0,0). Net effect: the circuit "proves" 1 & (1 ^ 0) == 0. PoC-confirmed
on Groth16 and PLONK.
Fix: range-check the result of every table query (bf.rchecker.Check(ret, 8)) before packing, in twoArgFn and Not.
9. Pairing Miller loop divides by zero when Q = 0 — HIGH
Component: std/algebra/emulated/sw_bls12381, sw_bn254, sw_bw6761, and
native sw_bls12377 pairing code.
A zero/(0,0) Q (or a Q at infinity mishandled by the affine Miller loop)
makes the line-evaluation DivUnchecked a 0/0 at every loop step, turning the
line coefficients into free prover variables and the pairing value
prover-controlled.
Fix: Q = 0 produces deterministic line coefficients; division-by-zero is
guarded.
10. Wrong exponent in BW6-761 finalExpHint — HIGH
Component: std/algebra/emulated/fields_bw6761 / sw_bw6761.
The final-exponentiation hint used an incorrect exponent (misplaced Frobenius
and flipped sign relative to the circuit relation), so the witness did not match
the value the circuit checked.
Fix: the exponent in finalExpHint was corrected to match the circuit.
11. Top recode bit unconstrained in native fixed-base scalar multiplication — MEDIUM
Component: std/algebra/native/sw_bls12377 (fixed-base comb scalar mul).
The scalar recoding pinned 2c + b0 = s + 2^n over the integers but left the
top bit of c unconstrained, permitting a native-field wrap-around. Constraining
the top bit to 1 proves 2c + b0 ≥ 2^n and excludes the wrap.
Fix: constrain the top recode bit.
Credit
Internal Consensys security audit (August 2026) and community reporter(s).
Special thanks to zkSecurity and zkao for some bug reports.
Summary
A set of independent soundness bugs were found across gnark's standard
library gadgets. The common thread is hint outputs that are consumed by the
constraint system without a binding or range constraint, letting a malicious
prover substitute forged witness values that satisfy every constraint while
encoding a false statement. Each bug is independently exploitable; several
compose into the building blocks of higher-level protocols (KZG openings,
Pedersen commitments, BLS signatures, EVM precompiles, recursive verifiers,
hash-based transcripts).
Affected consumers include
std/commitments/kzg,std/commitments/pedersen,std/signature/bls,std/signature/ecdsa,std/evmprecompiles(MODEXP,BLS12_G1MSM, KZG point evaluation),
std/recursion, and the SHA-2 / SHA-3 /Keccak / RIPEMD-160 hash gadgets built on
std/math/uints.Fixed vulnerabilities
1. Unconstrained carry in emulated-field deferred checks — CRITICAL
Component:
std/math/emulated(Mul,MulMod,AssertIsEqual,Reduce,Div,Inverse,Sqrt,Eval,ModMul,ModAssertIsEqual), transitively allof
std/algebra/emulatedandstd/evmprecompiles.The deferred multiplication / zero / evaluation checks verify
a(X)·b(X) = r(X) + k(X)·p(X) + (2^t − X)·c(X)at a random challengeγin thenative field
q. The carry polynomialccame from a hint and was neverrange-checked. A malicious prover picks the quotient
kand carrycso thetwo sides differ by a multiple of the native modulus
q; that differencevanishes mod
qat every challenge, so the Schwartz–Zippel check passesidentically for a false statement. Demonstrated end-to-end (setup → prove →
verify) on BN254, BLS12-377, BLS12-381 and BW6-761, on both Groth16 and PLONK.
Fix: per-call honest carry bounds (
mulCarryBound/mvCarryBound), abatched signed range check over all committed carry limbs inside the deferred
multicommit callback, a tightened overflow ceiling
(
maxOverflowReducedResult), and a hardcarryMaxBitsguardrail for thepreviously-silent extreme regime.
2. BLS12-377 torus
PairingCheckis vacuous — CRITICALComponent:
std/algebra/native/sw_bls12377(torus-based pairing), reachedfrom
std/commitments/kzg,std/commitments/pedersen,std/signature/bls.The torus scaling factor
swas read from a hint and never constrained. Thesingle
E6verification equation is affine-linear ins, so a prover solvesfor
sfor anyP, Q, making the pairing check accept false statements.Breaks KZG/Pedersen openings and BLS signature verification in the
BLS12-377-in-BW6-761 recursion stack.
Fix: the torus-compressed path was removed; the check uses the
over-determined
E12form in whichsis fully constrained.3. Grumpkin scalar multiplication: GLV decomposition not bound to the scalar — CRITICAL
Component:
std/algebra/native/sw_grumpkin(ScalarMul,ScalarMulBase,MultiScalarMul, joint GLV).The GLV sub-scalars
s1, s2were tied only to an unconstrained hint valuesemu(s1 − λ·s2 ≡ semu), never to the actual scalar variables. Thecircuit computed
[s1 − λ·s2]Qfor arbitrary prover-chosens1, s2— anarbitrary multiple of
Qunrelated tos. Verified:ScalarMul(P, 12345)proven equal to
[999999]P.Fix: recompose the scalar in the native field from the canonical bit
decomposition and bind
semutos(api.ToBinary→FromBits), eliminatingthe free hint value.
4. Variable-modulus remainder not reduced mod the runtime modulus — CRITICAL
Component:
std/math/emulated(ModMul,ModExp,ModAssertIsEqual),reachable via
std/evmprecompiles/05-expmod.go(EIP-198).For custom-modulus operations the remainder limbs were range-checked against the
type modulus width, not the runtime modulus
m(a witness). A proverreturns
r' = (a·b mod m) + m, congruent to the true result and passing everycheck but non-canonical. Even the honest hint could return
r + m(malleability). Breaks RSA/CRT/verification logic that assumes a unique
representative.
Fix:
Field.assertLessThanModulusperforms a runtime-modulus bound check;applied to
ModExp's output and exposed asField.ModMulCanonical.5. BLS12-381 scalar multiplication admits a small-torsion (cofactor) offset — CRITICAL
Component:
std/algebra/emulated/sw_emulated(
scalarMulGLVAndFakeGLV), reachable viastd/evmprecompiles/12-blsg1msm.go(EIP-2537) and any BLS12-381 G1 scalar mul.
The GLV+fake-GLV check verifies
[V](Q − [s]P) = Oon the full curveE(Fp), whose order ish·rwithhdivisible by3²for BLS12-381 G1.V ≢ 0 mod rdoes not force the torsion component ofQ − [s]Pto vanish, so aprover returns
[s]P + T₀for a 3-torsion pointT₀and passes allconstraints.
Fix: cofactor-clearing subgroup binding (
CofactorClearingconstants,widened to all reachable primes), and routing all BLS12-381 G1 public paths
through classic GLV whose computed output is inherently in-subgroup.
6. GLV Φ-table carries the wrong sub-scalar sign — CRITICAL
Component:
std/algebra/emulated/sw_emulated(scalarMulGLVandjointScalarMulGLVUnsafe), reachable on BLS12-381 G1 (complete mode) and allGLV curves in incomplete mode.
The Φ-table entries were built by re-signing the non-Φ table, so the
[3]Φ(Q)entry picked up the sign of the first sub-scalar
s1as a spurious factor.Because the circuit only pins
±s1 + λ·(±s2) ≡ s (mod r)with both|si| < 2^nbits, a prover selects a malicious lattice representation withs1 < 0and gets a wrong accumulator accepted. Verified end-to-end on BLS12-381G1.
Fix:
tablePhiQ[2]is now derived fromtriple(tablePhiQ[1])so each Φentry carries only its own sub-scalar's sign.
7.
AssertFinalExponentiationIsOneaccepts the all-zero residue witness — HIGHComponent:
std/algebra/emulated/sw_bn254andstd/algebra/emulated/sw_bls12381(and the classical native BLS12-377 path).The check verifies
x · cubicNonResiduePower == residueWitness^λbut neverconstrains
residueWitness ≠ 0. The all-zero hint satisfies0 == 0for anyx, so the exported method proved nothing aboutx.Fix: the residue witness is constrained invertible (
Inverse, assertingw·w⁻¹ == 1).8. Byte-wise ops (
uints) result not range-checked — table-key collision — HIGHComponent:
std/math/uints(twoArgFn,Not), consumed by SHA-2, SHA-3 /Keccak, RIPEMD-160, BLS-signature and EVM-KZG gadgets.
twoArgFnpacks the table key linearly asa + 2^8·b + 2^16·cand trusted thatthe lookup result is in range. The log-derivative argument only constrains
membership of the packed value, which cannot distinguish an in-range
cfroman out-of-range
cthat collides with a valid key:Xor(1,0)committed as1/256gives1 + 0 + 2^16/256 = 257 = 0x000101, the valid key for(a=1,b=0,c=1);And(1, 1/256) := 0gives2 = 0x000002, the valid key for(1,0,0). Net effect: the circuit "proves"1 & (1 ^ 0) == 0. PoC-confirmedon Groth16 and PLONK.
Fix: range-check the result of every table query (
bf.rchecker.Check(ret, 8)) before packing, intwoArgFnandNot.9. Pairing Miller loop divides by zero when
Q = 0— HIGHComponent:
std/algebra/emulated/sw_bls12381,sw_bn254,sw_bw6761, andnative
sw_bls12377pairing code.A zero/
(0,0)Q(or aQat infinity mishandled by the affine Miller loop)makes the line-evaluation
DivUncheckeda0/0at every loop step, turning theline coefficients into free prover variables and the pairing value
prover-controlled.
Fix:
Q = 0produces deterministic line coefficients; division-by-zero isguarded.
10. Wrong exponent in BW6-761
finalExpHint— HIGHComponent:
std/algebra/emulated/fields_bw6761/sw_bw6761.The final-exponentiation hint used an incorrect exponent (misplaced Frobenius
and flipped sign relative to the circuit relation), so the witness did not match
the value the circuit checked.
Fix: the exponent in
finalExpHintwas corrected to match the circuit.11. Top recode bit unconstrained in native fixed-base scalar multiplication — MEDIUM
Component:
std/algebra/native/sw_bls12377(fixed-base comb scalar mul).The scalar recoding pinned
2c + b0 = s + 2^nover the integers but left thetop bit of
cunconstrained, permitting a native-field wrap-around. Constrainingthe top bit to
1proves2c + b0 ≥ 2^nand excludes the wrap.Fix: constrain the top recode bit.
Credit
Internal Consensys security audit (August 2026) and community reporter(s).
Special thanks to zkSecurity and zkao for some bug reports.