Skip to content

[Bugs] Circom Bug Reports — Found with ZK's compiler differential Testing #407

@chamitro

Description

@chamitro

We are developing a mutation-guided differential testing tool for ZK compilers. By comparing Circom against Noir, ZoKrates, Cairo, and Leo on the same circuits with adversarial inputs, we found the following 10 issues in Circom 2.2.3. Each issue includes a minimal reproducer.


Issue 1: Runtime inputs exceeding field prime are silently reduced

Summary

When runtime inputs (via input.json) exceed the field prime p, the witness generator silently reduces them modulo p without any warning or error. A developer passing a = p+5 gets the same result as a = 5 with no indication that the input was modified.

Environment

  • Circom: 2.2.3 (latest master)
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a * a + a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .

# Normal input — correct
echo '{"a": "5"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # output = 31 ✓

# Input exceeding field prime — silently wrong!
echo '{"a": "21888242871839275222246405745257275088548364400416034343698204186575808495622"}' > input.json
# This is p+5, should warn but silently becomes 5
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # output = 31 (same as a=5!) — no warning!

Results

Circuit: out = a² + a + 1

Input (a) Expected behavior Actual output
5 31 ✅ 31
p+5 Warning or error ❌ 31 (silently treated as a=5)
2p+5 Warning or error ❌ 31 (silently treated as a=5)
100p+5 Warning or error ❌ 31 (silently treated as a=5)
p Warning or error ❌ 1 (silently treated as a=0)
Warning or error ❌ 1 (silently treated as a=0)
2²⁵⁶ Warning or error ❌ Silent reduction
10¹⁰⁰ Warning or error ❌ Silent reduction

Expected Behavior

The witness generator should either:

  1. Reject inputs >= p with an error: Error: input value exceeds field prime
  2. Warn the user: Warning: input 'a' exceeds field prime, reducing modulo p

Comparison with Noir

Noir (nargo) correctly rejects oversized inputs:

Failed to deserialize inputs: The value exceeds the field prime

Issue 2: Out-of-bounds array access returns arbitrary WASM memory

Summary

Dynamic array access with an out-of-bounds index returns arbitrary WASM memory values instead of failing with an error. This is a soundness issue — a malicious prover could exploit this to read adjacent memory and forge witnesses.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input idx;
    signal arr[3];
    signal output out;

    arr[0] <== 10;
    arr[1] <== 20;
    arr[2] <== 30;

    out <-- arr[idx];
}

component main = Test();
circom test.circom --r1cs --wasm --output .

# Valid index — correct
echo '{"idx": "1"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # output = 20 ✓

# Out-of-bounds index — returns garbage!
echo '{"idx": "3"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 6404 instead of error!

Results

idx Expected Actual
0 10 ✅ 10
1 20 ✅ 20
2 30 ✅ 30
3 Error ❌ 6404 (arbitrary memory)
100 Error ❌ 0 (arbitrary memory)
-1 Error ❌ Large field element (arbitrary memory)

Expected Behavior

The witness generator should fail with: Error: Array index out of bounds

Comparison with Noir

Noir correctly rejects: error: Index out of bounds, array has size 3 but index was 3


Issue 3: Compile-time constants exceeding field prime silently reduced

Summary

Integer constants larger than the field prime in circuit source code are silently reduced modulo p without any compiler warning. The constant p+1 becomes 1 with no indication to the developer.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output c;

    // p+1 where p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
    c <== a + 21888242871839275222246405745257275088548364400416034343698204186575808495618;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": "5"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 6 (5+1) instead of expected large number!

Results

Constant Expected Actual
p+1 Warning or error ❌ Silently becomes 1
2p Warning or error ❌ Silently becomes 0
100p+42 Warning or error ❌ Silently becomes 42

Expected Behavior

The compiler should emit a warning: warning: constant exceeds field prime, will be reduced modulo p

Comparison with Noir

Noir correctly rejects: error: Integer literal is too large


Issue 4: Division 0/0 silently returns 0 instead of failing

Summary

When both numerator and denominator are zero (0/0), the witness generator silently returns 0 instead of failing. Division by zero with non-zero numerator correctly fails, but the 0/0 case slips through because 0 * q === 0 is satisfied for any q, and the witness generator picks q = 0.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal input b;
    signal output q;
    q <-- a / b;
    q * b === a;
}

component main = Test();
circom test.circom --r1cs --wasm --output .

# Non-zero / zero — correctly fails
echo '{"a": "10", "b": "0"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
# Error: Assert Failed ✓

# Zero / zero — silently returns 0!
echo '{"a": "0", "b": "0"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns q=0, no error!

Results

Input Expected Actual
a=10, b=5 q=2 ✅ q=2
a=10, b=0 Error ✅ Error
a=0, b=0 Error ❌ q=0 (no error!)

Why This Is Dangerous

The constraint q * b === a is satisfied when q=0, b=0, a=0 (since 0*0 === 0). But 0/0 is undefined — any value of q would satisfy the constraint when both a=0 and b=0. The witness generator picks q=0 arbitrarily, masking an under-constrained circuit.

Expected Behavior

The witness generator should fail: Error: Division by zero (0/0 is undefined)

Comparison with Noir

Noir rejects all division by zero: Failed constraint (including 0/0)


Issue 5: Negative inputs silently converted to field elements

Summary

Negative integer values in input.json (e.g., "-5") are silently converted to field elements (p - 5) without any warning. While mathematically correct for field arithmetic, this violates the principle of least surprise.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a * a;
}

component main = Test();
circom test.circom --r1cs --wasm --output .

echo '{"a": "-1"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 1 — (-1)² = 1, no warning

Results

Input Interpreted as Output (a²)
a=5 5 ✅ 25
a=-1 p-1 (no warning) ❌ 1
a=-5 p-5 (no warning) ❌ 25
a=-100 p-100 (no warning) ❌ 10000

Expected Behavior

Warning: Warning: negative input 'a' will be interpreted as field element p-|a|

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
a=-1 Accepts Rejects Rejects Accepts Rejects

Issue 6: Boolean JSON value true silently coerced to field element 1

Summary

JSON boolean value true in input.json is silently accepted and treated as the field element 1. This is type confusion — the circuit expects a numeric field element, not a boolean.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": true}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 2 — true treated as 1, no error

Expected Behavior

Error: Error: expected numeric value for signal 'a', got boolean

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
a=true ❌ Accepts (1) Rejects Accepts (0) Rejects Accepts (0)

Issue 7: Empty string "" silently coerced to field element 0

Summary

An empty string "" in input.json is silently accepted and treated as the field element 0. This is type confusion — an empty string is not a valid field element representation.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": ""}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 1 — empty string treated as 0, no error

Expected Behavior

Error: Error: empty string is not a valid field element

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
a="" ❌ Accepts (0) Rejects Accepts (0) Rejects Accepts (0)

Issue 8: Octal literal inputs silently accepted

Summary

Octal literals (e.g., "0o77") in input.json are silently parsed as their decimal equivalent (63). This is JavaScript BigInt behavior inherited by the WASM witness generator — BigInt("0o77") returns 63n.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": "0o77"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 64 — 0o77 = 63 in octal, 63+1 = 64

Results

Input Interpreted as Output (a+1)
"0o77" 63 (octal) ❌ 64
"0o10" 8 (octal) ❌ 9

Expected Behavior

Error: Error: '0o77' is not a valid decimal field element

Field element inputs should only accept decimal integers (and optionally hex with 0x prefix).

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
0o77 ❌ Accepts (63) Rejects Rejects Rejects Rejects

Circom is the only compiler that accepts octal literals.


Issue 9: Binary literal inputs silently accepted

Summary

Binary literals (e.g., "0b1010") in input.json are silently parsed as their decimal equivalent (10). Same root cause as Issue 8 — JavaScript BigInt("0b1010") returns 10n.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": "0b1010"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 11 — 0b1010 = 10 in binary, 10+1 = 11

Results

Input Interpreted as Output (a+1)
"0b1010" 10 (binary) ❌ 11
"0b11111111" 255 (binary) ❌ 256

Expected Behavior

Error: Error: '0b1010' is not a valid decimal field element

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
0b1010 ❌ Accepts (10) Rejects Rejects Rejects Rejects

Circom is the only compiler that accepts binary literals.


Issue 10: Negative zero (-0) silently accepted

Summary

The string "-0" in input.json is silently accepted and treated as field element 0. While -0 equals 0 in most contexts, accepting it without warning indicates insufficient input validation.

Environment

  • Circom: 2.2.3
  • Node: v18.17.1
  • snarkjs: 0.7.6
  • OS: Ubuntu

Reproducer

pragma circom 2.0.0;

template Test() {
    signal input a;
    signal output out;
    out <== a + 1;
}

component main = Test();
circom test.circom --r1cs --wasm --output .
echo '{"a": "-0"}' > input.json
node test_js/generate_witness.js test_js/test.wasm input.json w.wtns
snarkjs wtns export json w.wtns w.json
cat w.json  # Returns 1 — "-0" treated as 0, no warning

Expected Behavior

Either reject: Error: '-0' is not a valid field element
Or warn: Warning: '-0' interpreted as 0

Comparison with Other Compilers

Input Circom Noir ZoKrates Cairo Leo
-0 ❌ Accepts (0) Rejects Rejects Accepts (0) Rejects

Summary Table

# Bug Category
1 Runtime inputs >= p silently reduced Input validation
2 OOB array access returns WASM memory Memory safety
3 Compile-time constants >= p silently reduced Compiler warning
4 Division 0/0 silently returns 0 Arithmetic
5 Negative inputs silently converted Input validation
6 Boolean true coerced to 1 Type confusion
7 Empty string "" coerced to 0 Type confusion
8 Octal literals (0o77) accepted Input parsing
9 Binary literals (0b1010) accepted Input parsing
10 Negative zero (-0) accepted Input validation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions