How to Fix: Bound check in the instruction `memory.init`
The memory.init crash is a classic bounds-checking bug: the engine validates part of the operation, but misses a critical range condition when copying bytes from a passive data segment into linear memory. In affected builds of Wasmtime, a malformed or specially crafted WebAssembly module can trigger incorrect behavior when memory.init is executed with offsets or lengths that exceed valid source or destination limits.
Understanding the Root Cause
The WebAssembly instruction memory.init copies a byte range from a passive data segment into linear memory. Conceptually, it behaves like this:
memory.init data_index dst_offset src_offset len
Before the copy happens, the runtime must guarantee all of the following:
- Destination range is valid:
dst_offset + len <= memory_size - Source range is valid:
src_offset + len <= data_segment_length - Arithmetic does not overflow while computing end offsets
This issue appears when one or more of those checks are incomplete, performed in the wrong order, or done using arithmetic that can wrap. For example, if the implementation checks len <= memory_size but does not correctly verify dst_offset + len, then a large destination offset can bypass validation and produce an out-of-bounds access. The same logic applies to the passive data segment: a valid length alone is not enough if src_offset is already near the end of the segment.
In practice, the bug is usually caused by one of these implementation mistakes:
- Checking individual operands, but not the full range
- Using unchecked integer addition for end-offset calculation
- Performing validation after partial state changes
- Assuming decoded immediates or JIT-lowered values are already safe
For Wasmtime, the fix is to ensure the engine traps before any copy occurs whenever the source or destination span is invalid. That aligns behavior with the WebAssembly specification, where memory.init must trap on out-of-bounds access rather than reading or writing invalid memory.
Step-by-Step Solution
The safest fix is to centralize validation and reject the operation unless both source and destination ranges are provably in bounds.
- Locate the runtime path that executes or lowers
memory.init. - Add explicit checked arithmetic for both source and destination end offsets.
- Trap immediately if either computed range exceeds its backing buffer.
- Apply the same logic in both interpreter and compiled/JIT paths if the engine supports both.
- Add regression tests using the provided reproducer and additional overflow-focused cases.
A robust validation pattern looks like this:
fn validate_memory_init( memory_size: usize, data_len: usize, dst: usize, src: usize, len: usize,) -> Result<(), Trap> { let dst_end = dst.checked_add(len).ok_or(Trap::MemoryOutOfBounds)?; let src_end = src.checked_add(len).ok_or(Trap::MemoryOutOfBounds)?; if dst_end > memory_size { return Err(Trap::MemoryOutOfBounds); } if src_end > data_len { return Err(Trap::MemoryOutOfBounds); } Ok(())}
Then perform the copy only after validation succeeds:
validate_memory_init(memory_size, data_len, dst, src, len)?;memory[dst..dst + len].copy_from_slice(&data[src..src + len]);
If you are patching Wasmtime source, the implementation details depend on the version, but the workflow is generally:
git clone <repository>cd wasmtimegit checkout <affected-version-or-branch>
# Search for memory.init handlinggrep -R "memory.init" -n .grep -R "MemoryInit" -n .grep -R "data segment" -n crates/
Once you identify the lowering or runtime helper, update it so the trap is raised before any write occurs. In Rust, prefer checked_add over manual addition when offsets are untrusted.
After patching, rebuild and test:
cargo build --releasecargo test
If the issue report includes the attached reproducer archive, unpack it and run it against both the vulnerable and patched binaries to confirm the behavioral difference:
unzip memory_init_0_9_0.zipcd memory_init_0_9_0# Run using your local Wasmtime binary./run.sh
If there is no helper script, invoke the module directly with the local binary used in your environment:
/path/to/wasmtime <testcase.wasm>
Expected result after the fix:
- The module should trap cleanly on invalid
memory.initranges. - It should not crash the runtime.
- It should not perform partial memory writes before trapping.
For a stronger regression suite, add tests covering both normal and failing paths:
#[test]fn memory_init_traps_on_oob_destination() { // dst + len exceeds memory size // expect trap}#[test]fn memory_init_traps_on_oob_source() { // src + len exceeds passive data segment // expect trap}#[test]fn memory_init_traps_on_integer_overflow() { // dst or src near usize::MAX // expect trap}#[test]fn memory_init_succeeds_on_exact_boundary() { // dst + len == memory_size // src + len == data_len // expect success}
Common Edge Cases
- Exact-boundary copies:
dst + len == memory_sizeis valid, butdst + len > memory_sizemust trap. - Zero-length copies:
len == 0should succeed as long as the implementation follows spec semantics and does not perform unnecessary invalid slicing. - Integer overflow: even if values look unsigned and harmless,
dst + lenorsrc + lencan wrap without checked arithmetic. - Dropped data segments: if
data.drophas already invalidated a passive segment, latermemory.initbehavior must still follow the runtime’s data-segment state rules. - Multiple memories: newer proposals or internal abstractions may make it easy to validate against the wrong memory instance.
- JIT vs interpreter mismatch: one execution path may trap correctly while another still permits out-of-bounds access.
- Partial-copy bugs: some flawed implementations begin copying before all checks complete, which can corrupt memory even if a trap is eventually raised.
FAQ
1. Why does this bug happen specifically with memory.init?
Because memory.init reads from one bounded object and writes to another. That means it needs two independent bounds checks plus overflow-safe end-offset calculation. Missing either side creates an out-of-bounds condition.
2. Is checking len alone enough to make the operation safe?
No. A small len can still be unsafe if src_offset or dst_offset is already near the end of the valid range. The correct check is on the entire span, not the length in isolation.
3. What should the runtime do when the range is invalid?
It should raise a WebAssembly trap before modifying memory. Crashing the process, performing a partial write, or reading invalid bytes from the data segment indicates incorrect runtime behavior.
The key takeaway is simple: fix memory.init by validating source range, destination range, and overflow safety before any copy occurs. Once that logic is centralized and covered by regression tests, this class of Wasm memory bug becomes much harder to reintroduce.