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 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 |
| // Division by zero is architecturally undefined: the executor traps on it | ||
| // (`ExecutionError::ExceptionOrTrap`, see `execute_alu`), so an honest trace never | ||
| // contains a divrem event with c == 0. Enforce the same here so the AIR rejects | ||
| // div-by-zero rows rather than accepting them with a forced quotient. This keeps the |
There was a problem hiding this comment.
where is the spec to state this? or we just want to reject div-by-zero?
There was a problem hiding this comment.
fn execute_alu(
&mut self,
instruction: &Instruction,
) -> Result<(Option<u32>, u32, u32, u32), ExecutionError> {
let (rd, b, c) = self.alu_rr(instruction);
if matches!(instruction.opcode, Opcode::DIV | Opcode::DIVU | Opcode::MOD | Opcode::MODU)
&& c == 0
{
return Err(ExecutionError::ExceptionOrTrap());
}
There was a problem hiding this comment.
There isn't a spec citation for “trap on div-by-zero” here. The reason for this constraint is that our current executor already rejects DIV/DIVU/MOD/MODU when c == 0, so the AIR should enforce the same behavior instead of accepting impossible rows.
| // If the last real row is the last row, verify the public value's next pc. | ||
| builder.when_last_row().when(local.is_real).assert_eq(public_values.next_pc, local.next_pc); | ||
|
|
||
| // A branch or jump row carries its post-delay-slot target in `next_next_pc`. |
There was a problem hiding this comment.
should update our docs with the latest constraints.
|
|
||
| /// The bit decomposition of `addr`, used to range check that `addr` is a valid KoalaBear | ||
| /// field element (i.e. strictly less than the modulus `0x7F000001`). | ||
| pub addr_bits: KoalaBearBitDecomposition<T>, |
There was a problem hiding this comment.
Since we have memory range check in GlobalMemoryChip, local memory should be calculated from GlobalMemory, plz check if we already have constraints for the addressing, if we have, we don't need to do range check again.
There was a problem hiding this comment.
This is to fix this issue: 041
Medium - local.rs / MemoryLocalChip - No 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:
addr (memory address) - no range check, no KoalaBear modulus bound
initial_shard, final_shard - no range check against 24-bit bound
initial_clk, final_clk - no range check against 24-bit bound
initial_value[0..3], final_value[0..3] - no byte range check (values should be in [0, 255])
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:
Range-checks addr via KoalaBearBitDecomposition
Constrains value via 32 boolean bit decompositions
Enforces address ordering via AssertLtColsBits
Constrains timestamp = 1 for Initialize mode
| // (num_gates + delta read), never a gate row. | ||
| let single_row_phase = builder.is_first_row() * builder.is_last_row(); | ||
| builder.when(single_row_phase).assert_one(local.is_first_row); | ||
| builder.when(single_row_phase.clone()).assert_one(local.is_first_row); |
There was a problem hiding this comment.
check we if we remove boolean circuit garble with Vangher.
There was a problem hiding this comment.
It can be removed by Vangher in the next PR.
There was a problem hiding this comment.
I will make the PR for this.
There was a problem hiding this comment.
#520
This PR remove the boolean circuit garble precompile
@eigmax @felicityin
Fix issues found by Diligence: https://tintinweb.github.io/minr/#/p/ProjectZKM/Ziren/88e0521b82d3ed4c28005fbe220066c8ec76b31a