Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions src/vm/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ impl<'a, T> RwasmExecutor<'a, T> {
Ok(())
}

/// Invokes a syscall by its index.
///
/// An index that the import linker can't resolve is a fatal error, not a trap: the number of
/// stack params and results is taken from the linker, so an unresolved index leaves us with no
/// way to know how many values to pop, and any guess would desynchronize the value stack.
/// zkVM proving requires the same execution trace on every run, so the caller must supply an
/// import linker that resolves every syscall the module can reach — restricting the reachable
/// set is the linker's responsibility, and [`crate::always_failing_syscall_handler`] is the way
/// to reject a syscall that is declared but must not be executed.
pub(crate) fn invoke_syscall(&mut self, sys_func_idx: SysFuncIdx) -> Result<(), TrapCode> {
let (params, result) = self
.store
Expand Down
10 changes: 2 additions & 8 deletions src/vm/executor/control_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
let func_index: u32 = self.sp.pop_as();
self.store.last_signature = Some(signature_idx);
let instr_ref: u32 = self
.store
.tables
.get(&table)
.expect("rwasm: unresolved table index")
.resolve_table(table)
.get_untyped(func_index)
.ok_or(TrapCode::TableOutOfBounds)?
.into();
Expand Down Expand Up @@ -141,10 +138,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
let func_index: u32 = self.sp.pop_as();
self.store.last_signature = Some(signature_idx);
let instr_ref = self
.store
.tables
.get(&table)
.expect("rwasm: unresolved table index")
.resolve_table(table)
.get_untyped(func_index)
.map(|v| v.as_u32())
.ok_or(TrapCode::TableOutOfBounds)?;
Expand Down
57 changes: 26 additions & 31 deletions src/vm/executor/table.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use crate::{ElementSegmentIdx, RwasmExecutor, TableEntity, TableIdx, TrapCode};

impl<'a, T> RwasmExecutor<'a, T> {
/// Resolves a table by its index, materializing an empty one if it doesn't exist yet.
///
/// Tables enter the store only when the module executes `TableGrow` (the translator emits one
/// per declared table), but a module is free to reference a table index it never grows, and
/// verification can't reject that because table indices carry no declaration.
/// Such a table is treated as a zero-length table, which matches Wasm semantics: `table.size`
/// returns 0 and every element access is bound-checked into `TrapCode::TableOutOfBounds`.
#[inline(always)]
pub(crate) fn resolve_table(&mut self, table_idx: TableIdx) -> &mut TableEntity {
self.store.tables.entry(table_idx).or_default()
}

#[inline(always)]
pub(crate) fn visit_table_size(&mut self, table_idx: TableIdx) {
let table_size = self
.store
.tables
.get(&table_idx)
.expect("rwasm: unresolved table segment")
.size();
let table_size = self.resolve_table(table_idx).size();
self.sp.push_as(table_size);
self.ip.add(1);
}
Expand All @@ -17,7 +24,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
pub(crate) fn visit_table_grow(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
let (init, delta) = self.sp.pop2();
let delta: u32 = delta.into();
let table = self.store.tables.entry(table_idx).or_default();
let table = self.resolve_table(table_idx);
let result = table.grow_untyped(delta, init);
self.sp.push_as(result);
#[cfg(feature = "tracing")]
Expand All @@ -31,10 +38,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
#[inline(always)]
pub(crate) fn visit_table_fill(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
let (i, val, n) = self.sp.pop3();
self.store
.tables
.get_mut(&table_idx)
.expect("rwasm: missing table")
self.resolve_table(table_idx)
.fill_untyped(i.into(), val, n.into())?;
self.ip.add(1);
Ok(())
Expand All @@ -44,10 +48,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
pub(crate) fn visit_table_get(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
let index = self.sp.pop();
let value = self
.store
.tables
.get_mut(&table_idx)
.expect("rwasm: missing table")
.resolve_table(table_idx)
.get_untyped(index.into())
.ok_or(TrapCode::TableOutOfBounds)?;
self.sp.push(value);
Expand All @@ -58,10 +59,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
#[inline(always)]
pub(crate) fn visit_table_set(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
let (index, value) = self.sp.pop2();
self.store
.tables
.get_mut(&table_idx)
.expect("rwasm: missing table")
self.resolve_table(table_idx)
.set_untyped(index.into(), value)
.map_err(|_| TrapCode::TableOutOfBounds)?;
#[cfg(feature = "tracing")]
Expand All @@ -84,18 +82,19 @@ impl<'a, T> RwasmExecutor<'a, T> {
let dst_index = u32::from(d);
// Query both tables and check if they are the same:
if src_table_idx != dst_table_idx {
let [src, dst] = self
self.resolve_table(src_table_idx);
self.resolve_table(dst_table_idx);
// Both tables are materialized above, so the disjoint lookup always succeeds
let [Some(src), Some(dst)] = self
.store
.tables
.get_disjoint_mut([&src_table_idx, &dst_table_idx])
.map(|v| v.expect("rwasm: unresolved table segment"));
else {
return Err(TrapCode::TableOutOfBounds);
};
TableEntity::copy(dst, dst_index, src, src_index, len)?;
} else {
let src = self
.store
.tables
.get_mut(&src_table_idx)
.expect("rwasm: unresolved table segment");
let src = self.resolve_table(src_table_idx);
src.copy_within(dst_index, src_index, len)?;
}
self.ip.add(1);
Expand Down Expand Up @@ -135,11 +134,7 @@ impl<'a, T> RwasmExecutor<'a, T> {
if is_empty_segment {
module_elements_section = &[];
}
let table = self
.store
.tables
.get_mut(&table_idx)
.expect("rwasm: missing table");
let table = self.resolve_table(table_idx);
table.init_untyped(dst_index, module_elements_section, src_index, len)?;

self.ip.add(2);
Expand Down
203 changes: 203 additions & 0 deletions tests/tables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! Tables that a module references but never grows must behave as zero-length tables instead of
//! panicking the interpreter.

use rwasm::{
always_failing_syscall_handler, instruction_set, ExecutionEngine, ImportLinker, InstructionSet,
RwasmModule, RwasmModuleBuilder, RwasmStore, TrapCode, Value,
};

/// The index of a table that no test module ever grows.
const UNGROWN_TABLE: u16 = 3;

fn verified_module(code_section: InstructionSet, elem_section: &[u32]) -> RwasmModule {
let module = RwasmModuleBuilder::new(code_section)
.with_elem_section(elem_section)
.build();
RwasmModule::new_verified_exact(&module.serialize()).expect("module must pass verification")
}

fn execute(code_section: InstructionSet, result: &mut [Value]) -> Result<(), TrapCode> {
let module = verified_module(code_section, &[]);
let engine = ExecutionEngine::new();
let mut store = RwasmStore::new(
ImportLinker::default().into(),
(),
always_failing_syscall_handler,
None,
None,
);
engine.execute(&mut store, &module, &[], result)
}

fn execute_and_trap(code_section: InstructionSet) -> TrapCode {
execute(code_section, &mut []).expect_err("execution must trap")
}

#[test]
fn test_table_size_of_ungrown_table_is_zero() {
let mut result = [Value::I32(-1)];
execute(
instruction_set! {
TableSize(UNGROWN_TABLE)
Return
},
&mut result,
)
.unwrap();
assert_eq!(result[0].i32(), Some(0));
}

#[test]
fn test_table_get_of_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0)
TableGet(UNGROWN_TABLE)
Drop
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_table_set_of_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // index
I32Const(1) // value
TableSet(UNGROWN_TABLE)
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_table_fill_of_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // d
I32Const(0) // val
I32Const(1) // n
TableFill(UNGROWN_TABLE)
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_empty_table_fill_of_ungrown_table_succeeds() {
execute(
instruction_set! {
I32Const(0) // d
I32Const(0) // val
I32Const(0) // n
TableFill(UNGROWN_TABLE)
Return
},
&mut [],
)
.unwrap();
}

#[test]
fn test_table_copy_of_ungrown_tables_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // d
I32Const(0) // s
I32Const(1) // n
.op_table_copy(UNGROWN_TABLE, UNGROWN_TABLE + 1)
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_empty_table_copy_of_ungrown_tables_succeeds() {
execute(
instruction_set! {
I32Const(0) // d
I32Const(0) // s
I32Const(0) // n
.op_table_copy(UNGROWN_TABLE, UNGROWN_TABLE + 1)
Return
},
&mut [],
)
.unwrap();
}

#[test]
fn test_table_copy_within_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // d
I32Const(0) // s
I32Const(1) // n
.op_table_copy(UNGROWN_TABLE, UNGROWN_TABLE)
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_table_init_into_ungrown_table_traps() {
let module = verified_module(
instruction_set! {
I32Const(0) // d
I32Const(0) // s
I32Const(1) // n
TableInit(1)
TableGet(UNGROWN_TABLE) // table index payload
Return
},
&[0],
);
let engine = ExecutionEngine::new();
let mut store = RwasmStore::new(
ImportLinker::default().into(),
(),
always_failing_syscall_handler,
None,
None,
);
let trap_code = engine
.execute(&mut store, &module, &[], &mut [])
.expect_err("execution must trap");
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_call_indirect_through_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // func index
CallIndirect(0)
TableGet(UNGROWN_TABLE) // table index payload
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_return_call_indirect_through_ungrown_table_traps() {
let trap_code = execute_and_trap(instruction_set! {
I32Const(0) // func index
ReturnCallIndirect(0)
TableGet(UNGROWN_TABLE) // table index payload
Return
});
assert_eq!(trap_code, TrapCode::TableOutOfBounds);
}

#[test]
fn test_grown_table_still_reports_its_size() {
let mut result = [Value::I32(-1)];
execute(
instruction_set! {
I32Const(0) // init
I32Const(2) // delta
TableGrow(UNGROWN_TABLE)
Drop
TableSize(UNGROWN_TABLE)
Return
},
&mut result,
)
.unwrap();
assert_eq!(result[0].i32(), Some(2));
}
Loading