fix: issues found by Diligence#519
Conversation
Issue 009
The bug is that Ziren keeps two versions of operand A:
When op_a is register $zero, the CPU AIR forced only op_a_access.value() to be zero. It did not also force op_a_value to zero for read-source instructions. That left op_a_value free whenever an instruction read $zero as operand A. Branch instructions and TEQ consume this free op_a_value, so a malicious prover could fake comparisons involving $zero. Example: beq $0, $0, label should always branch. But the prover could keep the real register access value as 0 while setting the free op_a_value to 1, making the branch chip believe the operands are unequal and accept a fallthrough path. So this is a soundness bug: the verifier could accept a proof of an execution path that does not match real MIPS semantics. FixAdd a CPU AIR constraint for the vulnerable case: This binds the free instruction-tuple operand op_a_value to zero when op_a is $zero and the instruction treats A as an immutable read source, covering branches, stores, and TEQ. It deliberately does not constrain ordinary writes to $zero, where a nonzero computed result is allowed to be discarded. |
Issue 213Critical — Branch/Jump delay-slot target ( MIPS branches and jumps use a delay slot, so Ziren tracks: pc For a jump at B to target T: pc = B Inside one shard, next_next_pc is correctly propagated to the delay-slot row. But across shard boundaries, public values only carry next_pc, not next_next_pc. So a malicious prover could end a shard on a branch/jump row. The shard exports only B + 4, the delay-slot address. The next shard starts at B + 4 and re-derives next_pc = B + 8, silently dropping the true target T. Result: a taken branch or unconditional jump can be forged as fallthrough, letting the prover create an execution trace that violates the real program control flow while still verifying. FixAdd verifier-side AIR constraints that forbid a shard from ending on a branch or jump row: Why this fixes it: branch/jump rows are real, non-halt, non-sequential rows. They carry the post-delay-slot target in next_next_pc, but shard public values only export next_pc. By requiring the last real CPU row to be either sequential or halt, the AIR now enforces the executor’s intended invariant that branch/jump and their delay slot cannot be split across a shard boundary. That prevents next_next_pc from being dropped. |
Issue 016High - syscall/precompiles/u256x2048_mul/air.rs - U256x2048Mul lo_ptr/hi_ptr constrained only via reduce() The chip reads lo_ptr and hi_ptr from memory, then constrains them only by comparing against: But reduce() maps a 32-bit word into the KoalaBear field, whose modulus is smaller than 2^32. So two different 32-bit words can reduce to the same field element. Because lo_ptr and hi_ptr are later used as base addresses for output memory writes, a non-canonical pointer word could collide with another address after reduction. That lets the proof write the multiplication result to a different memory address than the raw pointer word represents. The fix is to range-check the raw pointer words with KoalaBearWordRangeChecker before reducing them, making the reduction injective for those values. FixAdd local KoalaBearWordRangeChecker columns for both pointer reads: Trace generation now populates them from the raw memory-read pointer words, and the AIR range-checks both values before the existing reduce() binding: Same for hi_ptr_memory. This makes the pointer word canonical before reduction, so lo_ptr_memory.value().reduce() and hi_ptr_memory.value().reduce() cannot collide with a different 32-bit word. |
Issue 020MULT/MULTU HI-register write is skippable, forging the value read by MFHI. The MUL chip allowed real MULT/MULTU instructions to skip writing the MIPS HI register. The HI write was gated by hi_record_is_real, but that column was not forced to be 1 for hardware MULT/MULTU rows. A malicious prover could set it to 0, causing the product high word to be computed but not written to register 33. Then a later MFHI would read the old/stale HI value instead of the true multiply high word. This lets the prover forge computations that depend on 64-bit multiplication results, while still satisfying the memory argument because the skipped write simply never enters the memory multiset. FixAdd a MUL AIR constraint that forces hardware MULT/MULTU rows to write HI, while preserving dependency-only multiply rows used by DivRem and MADD/MSUB: Meaning:
Delegated multiply rows use pc = UNUSED_PC, so they can still keep hi_record_is_real = 0 and avoid writing register 33. This closes the attack because a real MULT/MULTU row can no longer match a MUL-chip row that skips the HI memory write. Since hi_record_is_real is also the is_check_memory slot in the instruction tuple, this also forces the CPU side to send the real shard/clk for hardware multiplies. |
Issue 036Medium - Verifier trusts proof-supplied chip_ordering indices without bounds validation. The shard proof contains a prover-supplied chip_ordering map from chip name to opened-values index. The verifier used this map directly to index opened_values.chips and order the chip list, but did not validate that the indices were in bounds, unique, or a valid 0..n permutation. A malicious proof could therefore provide malformed ordering metadata, causing verifier panics through out-of-bounds indexing, or making multiple chips reuse the same opened values. That can misalign chip constraints with the data being opened. The fix is to validate chip_ordering before use and reject malformed proofs with a verification error instead of trusting the prover-supplied map. FixFixed the native shard verifier path.
|
Issue 037INS instruction integer underflow when msb < lsb. The The AIR constraints for INS (in A malicious guest program can craft an INS instruction with Fix
|
Issue 038Reachable unimplemented!() panic in ecrecover hook on guest-controlled curve ID. The Hooks are invoked during execution via the The hook return values are used as "hints" and are not directly constrained by the AIR -- the precompile circuit re-verifies the computation. However, the panic occurs before any return value is produced, so it is a pure DoS vector against the prover. FixI replaced the reachable
I also added the new executor error variant in crates/core/executor/src/executor.rs:193 so the failure is explicit and non-panicking. |
Issue 039EXT instruction shift underflow when msbd + lsb > 31. The The AIR constraints for EXT ( Both the executor ( FixAdd an explicit check in
Because the guard fires during execution (before event emission), it also protects the identical subtraction in |
Issue 040Division-by-zero AIR constraint forces quotient=0xFFFFFFFF but executor rejects div-by-zero. Executor/AIR divergence on division by zero. The executor traps on FixMake the AIR reject division by zero, matching the executor. The existing This proves |
4f79b32 to
ca43f4c
Compare
793c72c to
449fd40
Compare
Issue 041No Independent Range Checking on Bridge Chip Columns. The MemoryLocalChip is the critical bridge between within-shard memory operations (local Memory lookups from eval_memory_access) and cross-shard memory state (global bus connecting Init/Finalize chips). Despite being foundational to memory soundness, its AIR constraints consist exclusively of four lookup send/receive operations with zero structural constraints on any of its 21 witness columns per entry. Specifically, the following columns are entirely unconstrained beyond lookup matching:
The system relies entirely on transitive constraint propagation: the MemoryGlobalInit chip range-checks values via 32 boolean bits, and this propagates through the lookup chain. However, this creates a fragile dependency where any implementation bug in the logUp/grand-product argument could allow arbitrary field elements to flow through the memory system undetected. This stands in sharp contrast with the MemoryGlobalChip, which independently:
FixAdd defense-in-depth constraints within the MemoryLocalChip AIR:
|
Issue 042Memory Value Bytes Never Range-Checked Within Memory Subsystem. Throughout the entire memory subsystem (instruction chip, consistency columns, local chip, and The memory value flow works as follows:
The memory instruction chip DOES perform a For reads (LB/LBU/LH/LHU/LW/LWL/LWR/LL), the memory instruction chip constrains The soundness assumption is that the CPU chip and ALU chip properly range-check all register values to be valid 4-byte Words with each byte in FixAdd byte range checks on memory values at the boundary where they enter the memory subsystem. The most effective location is within // Within eval_memory_access, add:
self.slice_range_check_u8(&memory_access.value().0, do_check.clone());
self.slice_range_check_u8(&memory_access.prev_value().0, do_check.clone());This would provide defense-in-depth against any upstream chip failing to properly constrain register/memory values. |
Issue 214Poseidon2-KoalaBear uses 13 partial rounds where the Plonky3 reference prescribes 20 for the degree-3 S-box. Ziren instantiates Poseidon2 over KoalaBear (31-bit prime, width 16) with S-box degree α=3 (cube — the smallest exponent coprime to FixRaised the partial-round count to 20 across all five KoalaBear/α=3 instantiations — the original recommendation listed four, but the core-machine precompile chip also had to change, since the
All consume the existing 30-row RC_16_30 table (8+20=28 ≤ 30); no new constants. The BN254 outer perm (outer_perm, R_P=56) is a different field/width and was correctly left alone. A structural bug the naive fix would have missed The I also added a regression test in primitives pinning R_F/R_P to Plonky3's reference, so a future field/S-box switch can't silently drift again. |
Issue 033Medium - BooleanCircuitGarbleChip - Untested Ziren-Specific Precompile The boolean circuit garble precompile is a Ziren-specific addition (not present in SP1) that verifies garbled boolean circuit gates. Neither the executor syscall implementation ( The precompile reads
FixAdded unit tests for the boolean circuit garble precompile on both sides. In the executor, I covered:
In the machine crate, I added trace-level and AIR-level tests that:
|
Issue 072Low - wrap.rs - Wrap verifier omits is_complete assertion and assert_complete checks Ziren's The wrap verifier is the final recursion stage before the proof is handed to the Gnark prover for Groth16/PLONK wrapping. It verifies a root proof and re-commits its public values. Without the completeness assertions, a wrap proof could be generated from an incomplete root proof -- one that does not represent a fully verified program execution. This means:
All of these boundary conditions, normally enforced by While the root verifier ( FixAdded completeness enforcement to the wrap verifier. ZKMWrapVerifier::verify now:
This matches the defense-in-depth behavior used by the root/compress verifiers and prevents a wrap proof from being built on an incomplete root proof. |
Issue 076Low - syscall/precompiles/keccak_sponge/air.rs - Keccak sponge block position flags are one-way selectors (Veridise Bug #1/9 cross-chip variant) Extends ZR-021 (boolean concern) with Veridise Bug #1/9 cross-reference: these flags are not just missing boolean checks -- they are one-way selectors that are never derived from the actual block position, matching the same vulnerability pattern found in SysLinux. The The builder.assert_eq(first_block * first_step * local.is_real, local.receive_syscall);
builder.assert_eq(final_block * final_step * local.is_real, local.write_output);
builder.assert_eq(local.is_absorbed, final_step * not_final_block * local.is_real);These constraints link
Note: FixAdded completeness-style block flag constraints for the Keccak sponge AIR. In crates/core/machine/src/syscall/precompiles/keccak_sponge/air.rs, I:
In crates/core/machine/src/syscall/precompiles/keccak_sponge/trace.rs, I added a regression test that builds a 2-block sponge event and checks the trace flags match the expected block and round positions. |
Issue 081Low - SysMmapSyscall - Integer Overflow in mmap Size Alignment The Specifically, the computation This is a prover-side issue in the executor, not a soundness issue in the constraint system. It could cause incorrect execution traces that either fail proof generation or produce proofs of incorrect computations. FixFixed sysmmap so page-size alignment no longer uses wrapping_add. The syscall now:
I also added unit tests covering:
|
Issue 083Low - Executor - todo!() Panic in Store Instruction Handler The This pattern also exists in FixReplaced the executor-side todo!() fallbacks with real behavior and clearer failure diagnostics. In
In
|
Issue 085Low - SysBrkSyscall - Unbounded Heap Growth Without Upper Bound The While the BRK register value itself is just a u32, downstream memory allocation when the guest actually writes to high addresses could exhaust prover memory. FixAdded an upper bound to sysbrk in |
Issue 086Low - recursion/circuit/machine/ - MaybeUninit::zeroed().assume_init() on Circuit Variables The recursion circuit's In the circuit context, these variables are always overwritten in the first loop iteration (i == 0) before being used in constraints. However, if the code were refactored to allow empty proof batches past the FixReplaced the recursion circuit’s unsafe zero-initialization in |
4186cf9 to
33ec840
Compare
Issue 087Low - recursion/compiler/ir/collections.rs - todo!() on Fixed Array Operations May Cause Panics Five While the primary circuit code paths use FixFixed the recursion compiler’s |
Issue 088Low - The The transmute of zero values is sound for KoalaBear (the production field type) because Montgomery-form zero is represented as FixHardened zeroed_f_vec in |
Issue 089Low - deserialize.go - Deserialization performs no bounds checking on input data The
FixAdded explicit bounds checks throughout the Go runtime deserializer in |
Issue 090Low - runtime.go - RESERVED_INPUT_PTR advances without bounds checking The Go runtime Additionally, the This is comparable to the Rust entrypoint ( FixAdded an explicit bounds check to the Go runtime’s reserved input allocator in |
Fix issues found by Diligence: https://tintinweb.github.io/minr/#/p/ProjectZKM/Ziren/88e0521b82d3ed4c28005fbe220066c8ec76b31a