Differential behavior: memory.init skips destination bounds check for dropped/active data segments
Categories
(Core :: JavaScript: WebAssembly, defect)
Tracking
()
People
(Reporter: decoder, Unassigned)
Details
(Keywords: ai-involved, sec-audit, testcase)
Attachments
(1 file)
|
2.33 KB,
application/javascript
|
Details |
Summary
memory.init on a dropped or active (implicitly dropped at instantiation) data segment silently accepts an out-of-bounds destination offset instead of trapping, diverging from the WebAssembly specification and V8.
Root Cause
js/src/wasm/WasmInstance.cpp, function MemoryInit. When the referenced data segment has been dropped (maybeSeg == nullptr), the function takes a fast path:
if (!maybeSeg) {
if (len == 0 && srcOffset == 0) {
return 0; // BUG: returns success without checking dstOffset against memLen
}
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS);
return -1;
}
A dropped segment behaves as a zero-length segment. Per the WebAssembly spec, memory.init must still trap when dstOffset + len > memLen. With len == 0 this reduces to dstOffset > memLen. The fast path only validates the source side (srcOffset == 0 && len == 0) but never checks the destination offset against the memory length.
For a non-null (live) segment, control reaches BoundsCheckInit which correctly validates both source and destination bounds. This creates an internal inconsistency: a live zero-length segment traps on out-of-bounds dstOffset, but a dropped segment with the same offsets does not.
Reproduction
Tested with mozilla-central revision 20260521-b754e06486cf (ASan debug build).
Run poc.js with the SpiderMonkey JS shell:
$ m-c-asan-debug/dist/bin/js --fuzzing-safe poc.js
NO TRAP: memory.init accepted out-of-bounds destination on a dropped segment
Compare with V8:
$ d8 poc.js
RuntimeError: memory access out of bounds
poc.js constructs a binary wasm module with:
- 1 page of memory (65536 bytes)
- An active data segment (dropped at instantiation)
- An exported function that calls
memory.init 0with the three parameters
The test invokes init(65537, 0, 0) — destination offset 65537 exceeds the 65536-byte memory. SpiderMonkey returns normally; V8 correctly traps.
Suggested Fix
In MemoryInit, route the null/dropped case through the destination bounds check instead of unconditionally returning success:
if (!maybeSeg) {
WasmMemoryObject* mem = instance->memory(memoryIndex);
const size_t memLen = mem->volatileMemoryLength();
if (BoundsCheckInit(dstOffset, srcOffset, len, memLen, /*segLen=*/0)) {
ReportTrapError(cx, JSMSG_WASM_OUT_OF_BOUNDS);
return -1;
}
return 0;
}
| Reporter | ||
Comment 1•5 months ago
|
||
Updated•5 months ago
|
Description
•