diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index cf9a7375..6b5a8bf9 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -159,6 +159,10 @@ fn execute_one(data: &[u8]) -> Result<()> { gen_cfg.max_tables = 1; // Keep wasm-smith table limits inside rwasm runtime hard cap. gen_cfg.max_table_elements = rwasm::N_MAX_TABLE_SIZE as u64; + // Force several segments so `data.drop`/`elem.drop` sequences with descending indices + // are reachable, they used to truncate the dropped-segment bitset. + gen_cfg.min_data_segments = 4; + gen_cfg.min_element_segments = 4; gen_cfg.export_everything = true; STATS.wasm_smith_modules.fetch_add(1, SeqCst); diff --git a/src/vm/executor/memory.rs b/src/vm/executor/memory.rs index 657d5620..cf21a345 100644 --- a/src/vm/executor/memory.rs +++ b/src/vm/executor/memory.rs @@ -157,8 +157,13 @@ impl<'a, T> RwasmExecutor<'a, T> { #[inline(always)] pub(crate) fn visit_data_drop(&mut self, data_segment_idx: DataSegmentIdx) { let empty_data_segments = &mut self.store.empty_data_segments; - empty_data_segments.resize(data_segment_idx as usize + 1, false); - empty_data_segments.set(data_segment_idx as usize, true); + // grow only, `resize` would truncate the bitset and forget previously dropped segments + // with a higher index + let idx = data_segment_idx as usize; + if idx >= empty_data_segments.len() { + empty_data_segments.resize(idx + 1, false); + } + empty_data_segments.set(idx, true); self.ip.add(1); } } diff --git a/src/vm/executor/table.rs b/src/vm/executor/table.rs index bc839db2..93028ee3 100644 --- a/src/vm/executor/table.rs +++ b/src/vm/executor/table.rs @@ -149,8 +149,13 @@ impl<'a, T> RwasmExecutor<'a, T> { #[inline(always)] pub(crate) fn visit_element_drop(&mut self, element_segment_idx: ElementSegmentIdx) { let empty_elem_segments = &mut self.store.empty_elem_segments; - empty_elem_segments.resize(element_segment_idx as usize + 1, false); - empty_elem_segments.set(element_segment_idx as usize, true); + // grow only, `resize` would truncate the bitset and forget previously dropped segments + // with a higher index + let idx = element_segment_idx as usize; + if idx >= empty_elem_segments.len() { + empty_elem_segments.resize(idx + 1, false); + } + empty_elem_segments.set(idx, true); self.ip.add(1); } } diff --git a/tests/segment_drop.rs b/tests/segment_drop.rs new file mode 100644 index 00000000..d629442f --- /dev/null +++ b/tests/segment_drop.rs @@ -0,0 +1,92 @@ +use rwasm::{ + always_failing_syscall_handler, instruction_set, ExecutionEngine, ImportLinker, InstructionSet, + RwasmModuleBuilder, RwasmStore, TrapCode, +}; + +fn execute(code_section: InstructionSet, data: &[u8], elem: &[u32]) -> Result<(), TrapCode> { + let module = RwasmModuleBuilder::new(code_section) + .with_data_section(data) + .with_elem_section(elem) + .build(); + let engine = ExecutionEngine::new(); + let mut store = RwasmStore::new( + ImportLinker::default().into(), + (), + always_failing_syscall_handler, + None, + None, + ); + engine + .execute(&mut store, &module, &[], &mut []) + .map(|_| ()) +} + +fn data_drop_code(dropped: &[u32]) -> InstructionSet { + let mut code = instruction_set! { + I32Const(1) + MemoryGrow + Drop + }; + for segment in dropped { + code.op_data_drop(*segment); + } + code.op_i32_const(0); // d + code.op_i32_const(0); // s + code.op_i32_const(1); // n + code.op_memory_init(5); + code.op_return(); + code +} + +fn elem_drop_code(dropped: &[u32]) -> InstructionSet { + let mut code = instruction_set! { + I32Const(0) // init + I32Const(1) // delta + TableGrow(0) + Drop + }; + for segment in dropped { + code.op_elem_drop(*segment); + } + code.op_i32_const(0); // d + code.op_i32_const(0); // s + code.op_i32_const(1); // n + code.op_table_init(5); + code.op_table_get(0); // table index payload of `table.init` + code.op_return(); + code +} + +#[test] +fn test_data_drop_keeps_higher_dropped_segments() { + let data = &[0xaa, 0xbb, 0xcc, 0xdd]; + // not dropped at all, `memory.init` reads the real data section + assert_eq!(execute(data_drop_code(&[]), data, &[]), Ok(())); + // dropped, `memory.init` must observe a zero-length segment and trap + assert_eq!( + execute(data_drop_code(&[5]), data, &[]), + Err(TrapCode::MemoryOutOfBounds), + ); + // dropping a lower-indexed segment afterward must not resurrect segment 5 + assert_eq!( + execute(data_drop_code(&[5, 2]), data, &[]), + Err(TrapCode::MemoryOutOfBounds), + ); +} + +#[test] +fn test_elem_drop_keeps_higher_dropped_segments() { + let elem = &[1u32, 2, 3, 4]; + // not dropped at all, `table.init` reads the real element section + assert_eq!(execute(elem_drop_code(&[]), &[], elem), Ok(())); + // dropped, `table.init` must observe a zero-length segment and trap + assert_eq!( + execute(elem_drop_code(&[5]), &[], elem), + Err(TrapCode::TableOutOfBounds), + ); + // dropping a lower-indexed segment afterward must not resurrect segment 5 + assert_eq!( + execute(elem_drop_code(&[5, 2]), &[], elem), + Err(TrapCode::TableOutOfBounds), + ); +}