Zero-Knowledge Proofs Are Only As Strong As Their Jump Tables
by pr0xy · 2026-04-22
Two weeks ago, Google’s Quantum AI group published a zero-knowledge proof claiming they had optimized a quantum circuit so aggressively that first-generation quantum computers could break elliptic curve cryptography keys in nine minutes. The proof was real. The mathematics checked out. The verification key was published. Anyone could run the verifier and confirm that someone, somewhere, knew a quantum circuit that performed this computation within the stated bounds.
What nobody verified was whether the prover’s Rust simulator had been audited for memory safety bugs.
Trail of Bits did. They forged a proof that beats Google’s on every metric — 8.3 million operations versus 17 million, 1,164 qubits versus 1,175, and zero reported Toffoli gates versus 2.1 million — by exploiting two application security vulnerabilities in the SP1 zkVM guest program. No quantum breakthrough was required. No cryptanalysis. Just a malformed serialization payload and a pair of compiler-emitted jump tables that disagreed about what an invalid opcode meant.
This is not a story about broken cryptography. It is a story about how zero-knowledge proofs redistribute trust from mathematicians to compiler engineers, and how nobody is auditing the compiler engineers.
What Google Actually Proved
Google used Succinct Labs’ SP1 zkVM. The architecture is straightforward: a Rust guest program compiles to a RISC-V ELF binary, runs inside the zkVM with private inputs, and commits public outputs. The private input was a quantum circuit written in kickmix, a custom assembly language for reversible quantum operations. The guest program was a simulator that deserialized the circuit, fuzz-tested it on 9,024 random inputs to verify it correctly computed elliptic curve point addition, and counted three metrics: total operations, qubit count, and average Toffoli gates.
The public outputs were upper bounds. If the simulator ran to completion without panicking, the zkVM produced a Groth16 proof that the committed bounds were honest. Anyone could verify the proof. No one could learn the circuit. This is the standard zkVM sales pitch: trust the math, not the author.
The problem is that the math only proves the program executed correctly. It does not prove the program was correct.
Vulnerability 1: Jump Table Confusion
The first bug is in how the guest program deserialized private input. Google’s code used rkyv::access_unchecked:
let private_circuit_bytes = sp1_zkvm::io::read_vec();
let ops = unsafe {
rkyv::access_unchecked::<rkyv::Archived<Vec<Op>>>(&private_circuit_bytes)
};
The unsafe block disables all validation. This was an intentional optimization — every bounds check in the zkVM guest inflates the cycle count, and cycle count directly translates to proof generation cost. But the input is attacker-controlled. If the serialized Op structs contain malformed enum discriminants, the simulator is working with undefined behavior.
The Op type contains a kind field: an enum of 18 OperationType variants. The simulator uses this field in two separate match expressions. The first match increments gate counters:
match op.kind {
OperationType::CCZ | OperationType::CCX => {
self.stats.toffoli_gates += executed_shots;
}
OperationType::CX | OperationType::CZ | OperationType::Swap
| OperationType::R | OperationType::Hmr => {
self.stats.clifford_gates += executed_shots;
}
_ => {}
}
The second match executes the actual quantum operation:
match op.kind {
OperationType::CCX => {
let v = cond & self.qubit(op.q_control1) & self.qubit(op.q_control2);
*self.qubit_mut(op.q_target) ^= v;
}
OperationType::CX => {
let v = cond & self.qubit(op.q_control1);
*self.qubit_mut(op.q_target) ^= v;
}
// ... additional arms
}
Trail of Bits reverse-engineered the RISC-V binary in Ghidra and discovered the Rust compiler emitted two distinct jump tables for these matches. The first table maps opcode to counter logic. The second maps opcode to execution logic. Because access_unchecked never validated the enum discriminant, an out-of-range kind value could cause the first match to dereference past its jump table and land inside the second.
The result: the operation executes correctly, but the Toffoli counter never increments. A circuit with 5,980,691 actual Toffoli gates reports zero. The proof verifies. The bounds are cryptographically committed. And they are completely false.
This is a classic confused deputy problem, except the deputy is a RISC-V jump table and the attacker is a malformed serialization offset. In the 1990s we called this “trusting trust.” In 2026 we call it “zkVM optimization.”
Vulnerability 2: Register Aliasing
With the Toffoli counter bypassed, Trail of Bits had unlimited budget for the most expensive quantum operation. But they still needed to beat Google’s operation count and qubit count. The second bug gave them the qubits.
The simulator’s CCX implementation never checked whether the control qubits and target qubit were distinct:
OperationType::CCX => {
let v = cond & self.qubit(op.q_control1) & self.qubit(op.q_control2);
*self.qubit_mut(op.q_target) ^= v;
}
If all three operands reference the same qubit, the operation becomes q = q ^ (q & q), which is always zero. This immediately resets any qubit to zero, violating the reversibility requirement that makes quantum computation theoretically interesting. In a real quantum computer this would be nonsense. In a classical simulator with buggy validation, it is a primitive for arbitrary classical logic gates.
By aliasing registers, Trail of Bits could implement irreversible classical operations — AND, NAND, arbitrary bit logic — without the overhead of quantum uncomputation. Google’s circuit required sophisticated measurement-based uncomputation and Proos-Zalka register sharing to squeeze modular inversion into 2.59 field elements of storage. Trail of Bits ignored all of that. They built elliptic curve point addition using classical logic gates, treated the simulator as a NAND playground, and still passed all 9,024 fuzz tests because the simulator’s validation checked outputs, not reversibility.
The Final Numbers
The forged proof uses:
- 8,288,880 total operations (Google: 17,000,000)
- 1,164 qubits (Google’s low-qubit: 1,175)
- 5,980,691 actual Toffoli gates
- 0 reported Toffoli gates
The proof verifies against Google’s unpatched verification code with verification key 0x00ca4af6cb15dbd83ec3eaab3a0664023828d90a98e650d2d340712f5f3eb0d4. It is cryptographically indistinguishable from a legitimate proof. If Trail of Bits had not published their methodology, the cryptographic community would have no mechanism to distinguish their forgery from a genuine quantum breakthrough.
The Redistribution of Trust
The standard narrative around zero-knowledge proofs is that they eliminate trust. This incident demonstrates the opposite. ZKPs do not eliminate trust; they shift it. Before zkVMs, we trusted mathematicians and peer reviewers to validate quantum algorithm papers. Now we also trust Rust compiler jump table layouts, serialization library boundary checking, and the assumption that guest program authors understand the difference between safe and unsafe deserialization.
Google’s scientific claims are unaffected. They patched the proof, and the underlying quantum research stands. But the incident reveals a structural gap in how zkVMs are deployed. The proof system correctly proved that the simulator executed without panicking. The simulator correctly executed the attacker’s circuit. The cryptography worked perfectly. The application security failed catastrophically.
This is the same pattern we saw in the 2010s with smart contract audits: formally verified logic running on top of unaudited Solidity code. The proof guarantees execution fidelity, not semantic correctness. A zkVM that proves “this program ran to completion” does not prove “this program counts Toffoli gates correctly.” Those are implementation properties, and implementation properties require implementation review.
Trail of Bits has published their proof-of-concept at github.com/trailofbits/quantum-zk-proof-poc. The SHA-256 of their circuit is 0x7efe1f62bb14a978322ab9ed41d670fc0fe0f211331032615c910df5a540e999. You can verify it yourself.
If you are building a system that uses zero-knowledge proofs for vulnerability disclosure, electronic voting, age verification, or any other high-stakes application, remember: the zero-knowledge part is probably fine. The virtual machine part needs the same audit attention you would give to any other Rust service parsing untrusted input. Because at the end of the day, a jump table confused about an invalid opcode is just a jump table. It does not care whether it is simulating quantum cryptanalysis or validating a JWT. It will do exactly what the compiler told it to do, and if the compiler was told to skip the checks, it will skip them with mathematical certainty.