Heap-buffer-overflow READ in [@ AutoMessageArgs::init] via non-NUL-terminated wasm import name (DecodeName/CompileForESM)
Categories
(Core :: JavaScript: WebAssembly, defect)
Tracking
()
People
(Reporter: bugmon, Unassigned)
Details
(5 keywords)
Attachments
(5 files)
The WebAssembly binary decoder reads import/export names through js/src/wasm/WasmBinary.cpp's Decoder::readBytes, and DecodeName (js/src/wasm/WasmValidate.cpp) stores the result into a UTF8Bytes/CacheableName buffer using resizeUninitialized(numBytes) + memcpy — producing a buffer of exactly numBytes with NO trailing NUL terminator. In this tree the upstream length guard if (numBytes > MaxStringBytes) return false; has been removed from DecodeName (the symbol MaxStringBytes is absent everywhere under js/src/wasm/), so import names may be arbitrarily large.
When such a module is loaded as a source-phase ECMAScript module (the WebAssembly ESM integration), js::wasm::CompileForESM (js/src/wasm/WasmJS.cpp) iterates the imports and, when a module name begins with the reserved prefix "wasm-js:", calls JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_ESM_RESERVED_MODULE_NAME, moduleName.data()). The message format (js/public/friend/ErrorNumbers.msg:514, "module import name '{0}' is reserved") consumes the {0} argument as a NUL-terminated C string, so AutoMessageArgs::init (js/src/vm/ErrorReporting.cpp:269) executes strlen() on the non-NUL-terminated CacheableName buffer and reads off the end of the heap allocation.
The read length is attacker-controlled by the import-name size; it leaks adjacent heap bytes into the rendered LinkError string and/or crashes the process. ASAN confirms a heap-buffer-overflow READ 0 bytes after the import-name allocation made by DecodeName.
Build Info
- Branch: main
- Revision: 1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1
- Timestamp: 2026-05-21T16:50:48+00:00
Affected Code
File: js/src/wasm/WasmValidate.cpp, line 2903-2926
static bool DecodeName(Decoder& d, CacheableName* name) {
uint32_t numBytes;
if (!d.readVarU32(&numBytes)) {
return false;
}
// NOTE: upstream has `if (numBytes > MaxStringBytes) return false;` here.
// That bound is REMOVED in this tree (MaxStringBytes is undefined).
const uint8_t* bytes;
if (!d.readBytes(numBytes, &bytes)) { // Decoder::readBytes (WasmBinary.cpp)
return false;
}
if (!IsUtf8(AsChars(Span(bytes, numBytes)))) {
return false;
}
UTF8Bytes utf8Bytes;
if (!utf8Bytes.resizeUninitialized(numBytes)) { // <-- exactly numBytes, NO NUL terminator
return false;
}
memcpy(utf8Bytes.begin(), bytes, numBytes);
*name = CacheableName(std::move(utf8Bytes));
return true;
}
File: js/src/wasm/WasmJS.cpp, line 684-693
Span<const char> moduleName = import.module.utf8Bytes(); // non-NUL-terminated bytes
Span<const char> name = import.field.utf8Bytes();
if (CharsStartsWith(moduleName, MakeStringSpan("wasm-js:"))) {
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
JSMSG_WASM_ESM_RESERVED_MODULE_NAME,
moduleName.data()); // passed to a {0}/%s C-string format
return false;
}
File: js/src/vm/ErrorReporting.cpp, line 269
// JSMSG_WASM_ESM_RESERVED_MODULE_NAME == "module import name '{0}' is reserved"
// {0} is consumed as a NUL-terminated C string:
args_[i] = ...; // = moduleName.data() (no NUL terminator)
size_t len = strlen(args_[i]); // <-- out-of-bounds heap READ past the name allocation
Decode side stores a non-NUL-terminated, length-unbounded name; the ESM error path then treats it as a C string.
Exploit Chain
- Craft a WebAssembly module with an import whose MODULE name begins with the reserved prefix "wasm-js:" (e.g. "wasm-js:" followed by 130000 'A's, 130008 bytes total — larger than the removed MaxStringBytes=100000 bound).
- Decoding reaches DecodeImportSection -> DecodeImportGroup -> DecodeName (WasmValidate.cpp), which uses WasmBinary.cpp's Decoder::readBytes to obtain the name and stores it in a CacheableName/UTF8Bytes buffer of exactly numBytes via resizeUninitialized + memcpy — with no NUL terminator and no length bound.
- Load the module as a source-phase ES module (import.source) so JS::CompileWasmModuleAsSource -> js::wasm::CompileForESM runs over the decoded imports.
- CompileForESM detects the "wasm-js:" reserved prefix and calls JS_ReportErrorNumberUTF8(..., JSMSG_WASM_ESM_RESERVED_MODULE_NAME, moduleName.data()).
- The {0} substitution treats moduleName.data() as a C string; AutoMessageArgs::init runs strlen() on the non-NUL-terminated buffer, reading past the end of the heap allocation (OOB read of attacker-controlled length), leaking adjacent heap memory into the error message and crashing under ASAN.
Steps to Reproduce
- Build the ASAN JS shell (already at /firefox/obj-firefox-asan/dist/bin/js; ENABLE_SOURCE_PHASE_IMPORTS is compiled in).
- mkdir -p /tmp/esm
- Create bad.wasm (run WITHOUT --fuzzing-safe so os.file is available): /firefox/obj-firefox-asan/dist/bin/js /tmp/esm/build.js (build.js writes a module whose import module name is 'wasm-js:' + 'A'*130000).
- Run the trigger with --fuzzing-safe plus the source-phase-imports flags: /firefox/obj-firefox-asan/dist/bin/js --fuzzing-safe --enable-source-phase-imports --setpref=experimental.wasm_esm_integration=true /tmp/esm/poc.js
- Observe AddressSanitizer: heap-buffer-overflow READ in strlen (AutoMessageArgs::init, ErrorReporting.cpp:269) called from js::wasm::CompileForESM (WasmJS.cpp); the overrun buffer was allocated in DecodeName (WasmValidate.cpp:2919). Confirmed via mcp__clauditor__js_shell_evaluator (content=poc.js, flags=[--enable-source-phase-imports, --setpref=experimental.wasm_esm_integration=true]).
Security Impact
- Severity: Moderate
- Attacker capability: A crafted WebAssembly module triggers an out-of-bounds heap READ of attacker-controlled length (strlen past the import-name allocation). The scanned bytes feed UTF-8 validation/copy and the rendered LinkError message string, enabling disclosure of adjacent heap contents and/or a controlled crash (DoS). No OOB write, but it is a clear JS-reachable memory-safety violation.
- Preconditions: Requires the experimental source-phase WebAssembly ESM integration feature to be active (--enable-source-phase-imports plus experimental.wasm_esm_integration; ENABLE_SOURCE_PHASE_IMPORTS compiled in) and loading an attacker-controlled .wasm module as an ES module source whose import module name begins with 'wasm-js:'. The removed MaxStringBytes bound in DecodeName lets the name be arbitrarily large; the non-NUL-terminated storage means even a short 'wasm-js:' name triggers the same strlen OOB.
ASAN Report
==6201==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7a7178c91800
READ of size 131073 at 0x7a7178c91800 thread T0
#0 strlen
#1 AutoMessageArgs::init js/src/vm/ErrorReporting.cpp:269:25
#6 JS_ReportErrorNumberUTF8 js/src/jsapi.cpp:3950:3
#7 js::wasm::CompileForESM js/src/wasm/WasmJS.cpp
#8 JS::CompileWasmModuleAsSource js/src/vm/Modules.cpp:350:8
#9 js::shell::ModuleLoader::loadAndParse js/src/shell/ModuleLoader.cpp:685:14
0x7a7178c91800 is located 0 bytes after 131072-byte region [0x7a7178c71800,0x7a7178c91800)
allocated by thread T0 here:
#8 mozilla::Vector<char,0,SystemAllocPolicy>::resizeUninitialized mozilla/Vector.h:1232
#9 DecodeName js/src/wasm/WasmValidate.cpp:2919:18
#10 DecodeImportGroup js/src/wasm/WasmValidate.cpp:3380:8
#11 DecodeImportSection js/src/wasm/WasmValidate.cpp:3596:10
SUMMARY: AddressSanitizer: heap-buffer-overflow js/src/vm/ErrorReporting.cpp:269:25 in AutoMessageArgs::init
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
| Reporter | ||
Comment 4•5 months ago
|
||
| Reporter | ||
Comment 5•5 months ago
|
||
Updated•5 months ago
|
Comment 7•5 months ago
|
||
NI Dan because this involves Wasm ESM integration.
Comment 8•5 months ago
|
||
This is another dup of Bug 2040428.
Updated•1 month ago
|
Description
•