Closed Bug 2042748 Opened 5 months ago Closed 5 months ago

Heap-buffer-overflow READ in [@ AutoMessageArgs::init] via non-NUL-terminated wasm import name (DecodeName/CompileForESM)

Categories

(Core :: JavaScript: WebAssembly, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2040428

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

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

  1. 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).
  2. 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.
  3. Load the module as a source-phase ES module (import.source) so JS::CompileWasmModuleAsSource -> js::wasm::CompileForESM runs over the decoded imports.
  4. CompileForESM detects the "wasm-js:" reserved prefix and calls JS_ReportErrorNumberUTF8(..., JSMSG_WASM_ESM_RESERVED_MODULE_NAME, moduleName.data()).
  5. 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

  1. Build the ASAN JS shell (already at /firefox/obj-firefox-asan/dist/bin/js; ENABLE_SOURCE_PHASE_IMPORTS is compiled in).
  2. mkdir -p /tmp/esm
  3. 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).
  4. 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
  5. 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
Attached file poc.js —
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file build.js —
Attached file RUN.txt —
Group: core-security → javascript-core-security

NI Dan because this involves Wasm ESM integration.

Flags: needinfo?(dminor)

This is another dup of Bug 2040428.

Status: UNCONFIRMED → RESOLVED
Closed: 5 months ago
Duplicate of bug: 2040428
Flags: needinfo?(dminor)
Resolution: --- → DUPLICATE
Group: javascript-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: