Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf551be
fix(keccak): constrain keccak sponge initial state (#517)
felicityin Jun 24, 2026
a516c49
fix(boolean-garble): require first gate (#518)
felicityin Jun 25, 2026
74f6f65
fix(cpu): bind zero register source operand
felicityin Jun 25, 2026
4b99be8
fix(cpu): prevent delay-slot shard split
felicityin Jun 25, 2026
58b7b6b
fix(U256x2048Mul): range check output pointers
felicityin Jun 25, 2026
88ab67c
fix(alu): require HI write for hardware multiply
felicityin Jun 25, 2026
fdd37da
fix: validate shard chip ordering
felicityin Jun 25, 2026
979b299
fix(exec): guard invalid INS encoding
felicityin Jun 25, 2026
575debe
fix(exec): reject unsupported ecrecover curve ids
felicityin Jun 25, 2026
1e09de7
fix(exec): reject undefined EXT field encoding
felicityin Jun 25, 2026
5d56599
fix(alu): reject division by zero in DivRem AIR
felicityin Jun 25, 2026
449fd40
fix(memory): harden local memory constraints
felicityin Jun 25, 2026
426e9bf
fix(memory): range-check memory words in eval_memory_access
felicityin Jun 25, 2026
074091d
fix(global): enforce on-curve global accumulation digests
felicityin Jun 25, 2026
5744713
fix(boolean-garble): restore AND gate type encoding
felicityin Jun 26, 2026
8982648
fix(poseidon2): raise KoalaBear/α=3 partial rounds to 20
felicityin Jun 26, 2026
97fdcbd
fix(boolean-garble): add coverage and constrain empty circuits
felicityin Jun 26, 2026
d023418
fix(verifier): enforce wrap completeness checks
felicityin Jun 26, 2026
65ff97d
fix(keccak): constrain keccak sponge block flags
felicityin Jun 26, 2026
e4be02d
fix(exec): guard sysmmap alignment overflow
felicityin Jun 26, 2026
b97cdc5
fix(exec): replace executor todo branches
felicityin Jun 26, 2026
2435465
fix(exec): guard sysbrk heap growth
felicityin Jun 26, 2026
68e8155
style: cargo fmt
felicityin Jun 26, 2026
bf4bd13
fix(recursion): replace unsafe zeroed init
felicityin Jun 26, 2026
33ec840
fix(recursion): handle fixed arrays explicitly
felicityin Jun 26, 2026
4186cf9
fix(machine): harden zeroed field vec init
felicityin Jun 26, 2026
7aa6fc1
fix(go-runtime): bound deserialize reads
felicityin Jun 26, 2026
845cbd7
fix(go-runtime): bound reserved input reads
felicityin Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/core/executor/src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ pub fn emit_misc_dependencies(executor: &mut Executor, event: MiscEvent) {
} else if matches!(event.opcode, Opcode::EXT) {
let lsb = event.c & 0x1f;
let msbd = event.c >> 5;
// `execute_ext` rejects encodings with `lsb + msbd >= 32`, so the `31 - lsb - msbd`
// shift amounts below cannot underflow.
debug_assert!(
lsb + msbd < 32,
"EXT with lsb + msbd >= 32 must be rejected during execution"
);
let sll_val = event.b << (31 - lsb - msbd);
let sll_event = AluEvent {
pc: UNUSED_PC,
Expand Down
26 changes: 24 additions & 2 deletions crates/core/executor/src/events/byte.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ impl ByteRecord for Vec<ByteLookupEvent> {

fn add_byte_lookup_events_from_maps(
&mut self,
_new_events: Vec<&HashMap<ByteLookupEvent, usize>>,
new_events: Vec<&HashMap<ByteLookupEvent, usize>>,
) {
todo!()
for new_blu_map in new_events {
for (blu_event, count) in new_blu_map.iter() {
self.extend(std::iter::repeat_n(*blu_event, *count));
}
}
}
}

Expand Down Expand Up @@ -185,3 +189,21 @@ impl ByteOpcode {
F::from_canonical_u8(self as u8)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_vec_add_byte_lookup_events_from_maps_expands_counts() {
let event = ByteLookupEvent::new(ByteOpcode::U8Range, 0, 0, 1, 2);
let mut map = HashMap::new();
map.insert(event, 3);

let mut events = Vec::new();
events.add_byte_lookup_events_from_maps(vec![&map]);

assert_eq!(events.len(), 3);
assert!(events.iter().all(|e| *e == event));
}
}
33 changes: 26 additions & 7 deletions crates/core/executor/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ pub enum ExecutionError {
#[error("buffer length {1} must be greater than or equal to {0}")]
BufferLengthTooSmall(usize, usize),

/// The execution failed because a hook received an unsupported elliptic curve identifier.
#[error("unsupported ecrecover curve id: {0}")]
UnsupportedEcrecoverCurveId(u8),

/// The execution failed while converting a slice to an array due to size mismatch.
#[error("failed to convert slice {0} to array")]
IntoArrayError(String),
Expand Down Expand Up @@ -1545,11 +1549,11 @@ impl<'a> Executor<'a> {
if instruction.opcode == Opcode::WSBH {
(a, b, c) = self.execute_wsbh(instruction);
} else if instruction.opcode == Opcode::EXT {
(a, b, c) = self.execute_ext(instruction);
(a, b, c) = self.execute_ext(instruction)?;
} else if instruction.opcode == Opcode::MADDU {
(hi_or_prev_a, a, b, c) = self.execute_maddu(instruction);
} else if instruction.opcode == Opcode::INS {
(hi_or_prev_a, a, b, c) = self.execute_ins(instruction);
(hi_or_prev_a, a, b, c) = self.execute_ins(instruction)?;
} else if instruction.opcode == Opcode::SEXT {
(a, b, c) = self.execute_sext(instruction);
} else if instruction.opcode == Opcode::TEQ {
Expand Down Expand Up @@ -1784,32 +1788,47 @@ impl<'a> Executor<'a> {
(a, b, 0)
}

fn execute_ext(&mut self, instruction: &Instruction) -> (u32, u32, u32) {
fn execute_ext(
&mut self,
instruction: &Instruction,
) -> Result<(u32, u32, u32), ExecutionError> {
let (rd, rt, c) =
(instruction.op_a.into(), (instruction.op_b as u8).into(), instruction.op_c);
let b = self.rr_cpu(rt, MemoryAccessPosition::B);
let msbd = c >> 5;
let lsb = c & 0x1f;
// `lsb + msbd < 32` is architecturally required (and enforced by the EXT AIR
// constraint). Otherwise the `31 - lsb - msbd` shift amount used here and in trace
// generation underflows as a `u32`. Reject the undefined encoding instead of panicking.
if msbd + lsb >= 32 {
return Err(ExecutionError::ExceptionOrTrap());
}
let mask_msb =
if msbd + lsb + 1 == 32 { 0xFFFFFFFF } else { (1u32 << (msbd + lsb + 1)) - 1 };
let a = (b & mask_msb) >> lsb;
self.rw_cpu(rd, a, MemoryAccessPosition::A);
(a, b, c)
Ok((a, b, c))
}

fn execute_ins(&mut self, instruction: &Instruction) -> (Option<u32>, u32, u32, u32) {
fn execute_ins(
&mut self,
instruction: &Instruction,
) -> Result<(Option<u32>, u32, u32, u32), ExecutionError> {
let (rd, rt, c) =
(instruction.op_a.into(), (instruction.op_b as u8).into(), instruction.op_c);
let b = self.rr_cpu(rt, MemoryAccessPosition::B);
let a = self.register(rd);
let prev_a = a;
let msb = c >> 5;
let lsb = c & 0x1f;
if msb < lsb {
return Err(ExecutionError::ExceptionOrTrap());
}
let mask = if msb - lsb + 1 == 32 { 0xFFFFFFFF } else { (1u32 << (msb - lsb + 1)) - 1 };
let mask_field = mask << lsb;
let a = (a & !mask_field) | ((b << lsb) & mask_field);
self.rw_cpu(rd, a, MemoryAccessPosition::A);
(Some(prev_a), a, b, c)
Ok((Some(prev_a), a, b, c))
}

fn execute_teq(
Expand Down Expand Up @@ -2069,7 +2088,7 @@ impl<'a> Executor<'a> {
rt
}
// Opcode::SDC1 => 0,
_ => todo!(),
_ => unreachable!("unexpected store opcode: {:?}", instruction.opcode),
};

if aligned_addr + 3 > MAX_MEMORY as u32 {
Expand Down
2 changes: 1 addition & 1 deletion crates/core/executor/src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub fn hook_ecrecover(_: HookEnv, buf: &[u8]) -> Result<Vec<Vec<u8>>, ExecutionE
match curve_id {
1 => Ok(ecrecover::handle_secp256k1(r_bytes, alpha_bytes, r_is_y_odd)),
2 => Ok(ecrecover::handle_secp256r1(r_bytes, alpha_bytes, r_is_y_odd)),
_ => unimplemented!("Unsupported curve id: {}", curve_id),
_ => Err(ExecutionError::UnsupportedEcrecoverCurveId(curve_id)),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,145 @@ impl Syscall for BooleanCircuitGarbleSyscall {
Ok(None)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{events::PrecompileEvent, Executor, Program};
use zkm_stark::ZKMCoreOpts;

const INPUT_PTR: u32 = 0x1000;
const OUTPUT_PTR: u32 = 0x2000;
const OR_GATE_ID: u32 = 7;

fn gate_info_words(gate_type: u32, delta: [u32; 4], valid: bool) -> [u32; GATE_INFO_BYTES] {
let h0 = [11, 12, 13, 14];
let h1 = [21, 22, 23, 24];
let label_b = [31, 32, 33, 34];
let mut expected = [0u32; 4];
for i in 0..4 {
expected[i] = h0[i] ^ h1[i] ^ label_b[i];
if gate_type == OR_GATE_ID {
expected[i] ^= delta[i];
}
}
if !valid {
expected[3] ^= 1;
}

let mut words = [0u32; GATE_INFO_BYTES];
words[0] = gate_type;
words[1..5].copy_from_slice(&h0);
words[5..9].copy_from_slice(&h1);
words[9..13].copy_from_slice(&label_b);
words[13..17].copy_from_slice(&expected);
words
}

fn write_input(
runtime: &mut Executor<'_>,
gate_infos: &[[u32; GATE_INFO_BYTES]],
delta: [u32; 4],
) {
let mut timestamp = 1;
let shard = 1;
runtime.mw(INPUT_PTR, gate_infos.len() as u32, shard, timestamp, None);
timestamp += 1;
for (i, value) in delta.into_iter().enumerate() {
runtime.mw(INPUT_PTR + 4 + i as u32 * 4, value, shard, timestamp, None);
timestamp += 1;
}
for (gate_idx, gate_info) in gate_infos.iter().enumerate() {
let gate_base = INPUT_PTR + 20 + gate_idx as u32 * (GATE_INFO_BYTES as u32) * 4;
for (word_idx, value) in gate_info.iter().enumerate() {
runtime.mw(gate_base + word_idx as u32 * 4, *value, shard, timestamp, None);
timestamp += 1;
}
}
runtime.mw(OUTPUT_PTR, u32::MAX, shard, timestamp, None);
}

fn run_syscall(gate_infos: Vec<[u32; GATE_INFO_BYTES]>, delta: [u32; 4]) -> Executor<'static> {
let mut runtime = Executor::new(Program::default(), ZKMCoreOpts::default());
write_input(&mut runtime, &gate_infos, delta);
runtime.state.current_shard = 2;
runtime.state.clk = 1;

let syscall = BooleanCircuitGarbleSyscall;
let mut ctx = SyscallContext::new(&mut runtime);
syscall
.execute(&mut ctx, SyscallCode::BOOLEAN_CIRCUIT_GARBLE, INPUT_PTR, OUTPUT_PTR)
.unwrap();
runtime
}

#[test]
fn basic_and_gate_verification_succeeds() {
let delta = [101, 102, 103, 104];
let mut runtime = run_syscall(vec![gate_info_words(0, delta, true)], delta);
assert_eq!(runtime.word(OUTPUT_PTR), 1);

let events = runtime.record.get_precompile_events(SyscallCode::BOOLEAN_CIRCUIT_GARBLE);
assert_eq!(events.len(), 1);
let (_, event) = &events[0];
let event = match event {
PrecompileEvent::BooleanCircuitGarble(event) => event,
_ => unreachable!(),
};
assert_eq!(event.output, 1);
assert_eq!(event.num_gates, 1);
assert_eq!(event.gates_info.len(), GATE_INFO_BYTES);
}

#[test]
fn basic_or_gate_verification_succeeds() {
let delta = [201, 202, 203, 204];
let mut runtime = run_syscall(vec![gate_info_words(OR_GATE_ID, delta, true)], delta);
assert_eq!(runtime.word(OUTPUT_PTR), 1);
}

#[test]
fn mixed_gates_with_bad_ciphertext_return_false() {
let delta = [111, 222, 333, 444];
let gate_infos = vec![
gate_info_words(0, delta, true),
gate_info_words(OR_GATE_ID, delta, true),
gate_info_words(0, delta, false),
];
let mut runtime = run_syscall(gate_infos, delta);
assert_eq!(runtime.word(OUTPUT_PTR), 0);

let events = runtime.record.get_precompile_events(SyscallCode::BOOLEAN_CIRCUIT_GARBLE);
let (_, event) = &events[0];
let event = match event {
PrecompileEvent::BooleanCircuitGarble(event) => event,
_ => unreachable!(),
};
let accessed_addrs = event
.local_mem_access
.iter()
.map(|access| access.addr)
.collect::<std::collections::BTreeSet<_>>();
assert!(accessed_addrs.contains(&INPUT_PTR));
assert!(accessed_addrs.contains(&(INPUT_PTR + 20)));
assert!(accessed_addrs.contains(&(INPUT_PTR + 20 + (GATE_INFO_BYTES as u32) * 4)));
assert!(accessed_addrs.contains(&OUTPUT_PTR));
}

#[test]
fn zero_gates_write_true() {
let delta = [1, 2, 3, 4];
let mut runtime = run_syscall(vec![], delta);
assert_eq!(runtime.word(OUTPUT_PTR), 1);

let events = runtime.record.get_precompile_events(SyscallCode::BOOLEAN_CIRCUIT_GARBLE);
let (_, event) = &events[0];
let event = match event {
PrecompileEvent::BooleanCircuitGarble(event) => event,
_ => unreachable!(),
};
assert_eq!(event.num_gates, 0);
assert!(event.gates_info.is_empty());
assert_eq!(event.output, 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
use crate::{
events::{LinuxEvent, PrecompileEvent},
program::MAX_MEMORY,
syscalls::{Syscall, SyscallCode, SyscallContext},
ExecutionError, Register,
};

pub(crate) struct SysBrkSyscall;

/// Maximum amount of heap growth allowed from the program's initial BRK value.
///
/// This is a prover-side safety bound to prevent guest-controlled `brk` values from
/// expanding memory usage without limit.
const MAX_HEAP_SIZE: u32 = 0x4000_0000;

fn max_brk(initial_brk: u32) -> Result<u32, ExecutionError> {
let limit =
initial_brk.checked_add(MAX_HEAP_SIZE).ok_or(ExecutionError::InvalidSyscallArgs())?;
Ok(limit.min(MAX_MEMORY as u32))
}

fn resolve_brk(
initial_brk: u32,
current_brk: u32,
requested_brk: u32,
) -> Result<u32, ExecutionError> {
let limit = max_brk(initial_brk)?;
let v0 = requested_brk.max(current_brk);
if v0 > limit {
return Err(ExecutionError::InvalidSyscallArgs());
}
Ok(v0)
}

impl Syscall for SysBrkSyscall {
fn num_extra_cycles(&self) -> u32 {
0
Expand All @@ -20,7 +46,8 @@ impl Syscall for SysBrkSyscall {
) -> Result<Option<u32>, ExecutionError> {
let start_clk = rt.clk;
let (record, brk) = rt.rr_traced(Register::BRK);
let v0 = if a0 > brk { a0 } else { brk };
let initial_brk = rt.rt.program.image.get(&(Register::BRK as u32)).copied().unwrap_or(brk);
let v0 = resolve_brk(initial_brk, brk, a0)?;
let a3_record = rt.rw_traced(Register::A3, 0);
let shard = rt.current_shard();
let event = PrecompileEvent::Linux(LinuxEvent {
Expand All @@ -40,3 +67,36 @@ impl Syscall for SysBrkSyscall {
Ok(Some(v0))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn resolve_brk_keeps_current_value_when_request_is_lower() {
assert_eq!(resolve_brk(0x1000, 0x2000, 0x1800).unwrap(), 0x2000);
}

#[test]
fn resolve_brk_allows_growth_within_limit() {
assert_eq!(resolve_brk(0x1000, 0x2000, 0x3000).unwrap(), 0x3000);
}

#[test]
fn resolve_brk_rejects_growth_past_limit() {
let initial_brk = 0x1000;
let limit = max_brk(initial_brk).unwrap();
assert!(matches!(
resolve_brk(initial_brk, 0x2000, limit + 4),
Err(ExecutionError::InvalidSyscallArgs())
));
}

#[test]
fn max_brk_rejects_overflowing_initial_brk() {
assert!(matches!(
max_brk(u32::MAX - MAX_HEAP_SIZE + 1),
Err(ExecutionError::InvalidSyscallArgs())
));
}
}
Loading