Closed Bug 2042728 Opened 11 days ago Closed 8 days ago

Missing ensureBallast() in wasm Ion JSPI emitters (emitResume/emitStoreSuspendParams) trips infallible-LifoAlloc assertion [@ FunctionCompiler::emitResume]

Categories

(Core :: JavaScript: WebAssembly, defect, P3)

defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox151 --- wontfix
firefox152 --- wontfix
firefox153 --- fixed

People

(Reporter: bugmon, Assigned: yury)

References

(Regression)

Details

(4 keywords)

Attachments

(4 files)

The WebAssembly JSPI / stack-switching MIR emitters in WasmIonCompile.cpp allocate an attacker-controlled number of MIR nodes inside per-parameter loops without ever calling mirGen().ensureBallast(). The wasm Ion TempAllocator is created infallible-by-default, so MIR nodes are allocated via an infallible LifoAlloc path; the engine-wide convention (15 call sites in this same file) is to call mirGen().ensureBallast() at the top of every loop that emits a module-controlled number of nodes, which pre-reserves a 16 KiB ballast chunk inside a temporary fallible scope. The two JSPI emitters FunctionCompiler::emitStoreSuspendParams (loop creating one MWasmStoreStackResult per suspend-tag param) and FunctionCompiler::emitResume (per-handler/per-param loops creating MWasmStackResultArea slots and MWasmStackResult nodes) omit this call entirely.

As a result, a single wasm 'suspend' or 'resume' op whose tag carries a large number of parameters causes ~N MIR nodes to be allocated within one op's infallible-allocation window. This overflows the current LifoAlloc chunk past the reserved ballast; the next node forces an infallible new-chunk allocation, which trips MOZ_ASSERT(fallibleScope_, "[OOM] Cannot allocate a new chunk in an infallible scope.") in LifoAlloc::newChunkWithCapacity (js/src/ds/LifoAlloc.cpp:164) and aborts the process. This is reachable from untrusted wasm: JSPI/StackSwitching is gated with 'flag fuzz enable = true' in js/public/WasmFeatures.h, so it is available under --fuzzing-safe and can be enabled purely from script (setPrefValue('wasm_stack_switching', true)); reaching the Ion path additionally requires the optimizing tier (setJitCompilerOption('wasm.baseline', 0), since baseline rejects the stack-switching opcodes).

This is a violated allocator invariant in the wasm Ion compiler, but it is a debug-build/assertion finding: on release and ASAN builds the MOZ_ASSERT is compiled out and LifoAlloc::newChunkWithCapacity simply allocates the new chunk, so there is no out-of-bounds access, use-after-free, or memory corruption in shipping builds. It is a correctness/robustness (DoS-class) defect, fixed by adding mirGen().ensureBallast() to the per-parameter loops, matching the rest of the file.

Build Info

Affected Code

File: js/src/wasm/WasmIonCompile.cpp, line 9657-9675

    // The rest are suspendTagParams, again in reverse order.
    for (uint32_t i = 0; i < suspendTagParams.length(); i++) {
      size_t reverseIndex = suspendTagParams.length() - i - 1;
      ValType handlerParam = suspendTagParams[reverseIndex];
      MWasmStackResultArea::StackResult loc(paramsAreaOffset,
                                            handlerParam.toMIRType());
      MDefinition* param = suspendParams[reverseIndex];
      MWasmStoreStackResult* store = MWasmStoreStackResult::New(
          alloc(), paramsArea, paramsAreaOffset, param);   // <-- no ensureBallast() in this loop
      if (!store) {
        return false;
      }
      curBlock_->add(store);
      paramsAreaOffset = loc.endOffset();
    }
    return true;
  }

File: js/src/wasm/WasmIonCompile.cpp, line 9895-9910

    DefVector resultDefs;
    size_t suspendLabelParams = suspendTagParams.length() + 1;
    for (uint32_t i = 0; i < suspendLabelParams; i++) {
      size_t stackResultIndex = currentResultsAreaIndex - i - 1;
      MWasmStackResult* stackResult =
          MWasmStackResult::New(alloc(), handlersResultArea, stackResultIndex); // line 9901: no ensureBallast()
      if (!stackResult || !resultDefs.append(stackResult)) {
        return false;
      }
      curBlock_->add(stackResult);
    }
    if (!br(handler.labelDepth(), resultDefs)) {
      return false;
    }

File: js/src/wasm/WasmIonCompile.cpp, line 9108-9119

  // Convention used everywhere else in this file: ensureBallast() at the top
  // of every loop that emits a module-controlled number of MIR nodes.
  for (uint32_t i = 0; i < numElements; i++) {
    if (!mirGen().ensureBallast()) {
      return false;
    }
    // ... allocate MIR nodes ...
  }

The JSPI emitters allocate a module-controlled number of MIR nodes per iteration without ensureBallast(), unlike every other variable-count allocating loop in the file.

Exploit Chain

  1. Craft a wasm module that uses the JSPI / stack-switching proposal: define a cont type (cont $ft) and a tag whose parameter list is large (e.g. 500 i64 params).
  2. Define a function $f that pushes the tag's params and executes 'suspend $tag', and an exported function that does 'cont.new' + 'resume $ct (on $tag L)'.
  3. From JavaScript, enable the feature with setPrefValue('wasm_stack_switching', true) (permitted under --fuzzing-safe) and force the optimizing tier with setJitCompilerOption('wasm.baseline', 0).
  4. Construct the module with new WebAssembly.Module(bytes); this synchronously invokes the Ion wasm compiler (IonCompileFunctions -> RootCompiler::generate -> emitBodyExprs).
  5. Compiling the 'suspend'/'resume' op enters emitStoreSuspendParams / emitResume, which allocate ~500 MIR nodes in a per-parameter loop without calling ensureBallast().
  6. The infallible LifoAlloc ballast (16 KiB) is exhausted mid-loop; the next infallible allocation requests a new chunk, hitting MOZ_ASSERT(fallibleScope_) and aborting the process (debug/fuzzing builds).

Steps to Reproduce

  1. Use the debug JS shell: /firefox/obj-js-debug/dist/bin/js (assertions enabled).
  2. Run the testcase jspi_ensureballast.js (it self-enables stack switching and the Ion tier in-script, so the evaluator's default --fuzzing-safe is sufficient; no extra flags needed).
  3. Observe: Assertion failure: fallibleScope_ ([OOM] Cannot allocate a new chunk in an infallible scope.) at js/src/ds/LifoAlloc.cpp:164, with the crash stack landing in FunctionCompiler::emitResume (WasmIonCompile.cpp:9901) / emitStoreSuspendParams (WasmIonCompile.cpp:9666).
  4. Note: the ASAN/release build (/firefox/obj-firefox-asan/dist/bin/js) compiles the module without crashing because the MOZ_ASSERT is compiled out and the infallible allocation simply grows the LifoAlloc.

Security Impact

  • Severity: Low
  • Attacker capability: Untrusted wasm (e.g. from a web page) that reaches the optimizing wasm tier can crash the JS engine's debug/fuzzing build by submitting a module whose JSPI suspend/resume tag carries a large parameter list, aborting on a violated allocator invariant in the wasm Ion compiler. On release/ASAN builds the assertion is compiled out and the allocation succeeds, so there is no memory corruption, OOB access, or use-after-free in shipping builds; impact is limited to a correctness/robustness (DoS-class) defect and to crashing assertion-enabled builds.
  • Preconditions: JSPI / stack-switching must be enabled (gated by the wasm_stack_switching pref; carries 'flag fuzz enable = true' so it is available under --fuzzing-safe) and the optimizing (Ion) wasm compiler must be used (baseline rejects the stack-switching opcodes). No memory-corruption primitive is obtained; the MOZ_ASSERT is debug-only.

ASAN Report

Assertion failure: fallibleScope_ ([OOM] Cannot allocate a new chunk in an infallible scope.), at js/src/ds/LifoAlloc.cpp:164
    #1 js::LifoAlloc::newChunkWithCapacity(unsigned long, bool) js/src/ds/LifoAlloc.cpp:163
    #2 js::LifoAlloc::getOrCreateChunk(unsigned long) js/src/ds/LifoAlloc.cpp:219
    #3 js::LifoAlloc::allocImplColdPath(unsigned long) js/src/ds/LifoAlloc.cpp:229
    #5 js::LifoAlloc::allocInfallible(unsigned long) js/src/ds/LifoAlloc.h:897
    #6 js::jit::TempAllocator::allocateInfallible(unsigned long) js/src/jit/JitAllocPolicy.h:42
    #8 js::jit::MInstruction::operator new(unsigned long, js::jit::TempAllocator&) js/src/jit/MIR.h:1047
    #9 js::jit::MWasmStackResult::New<...>(...) js/src/jit/MIR-wasm.h:1947
    #10 (anonymous namespace)::FunctionCompiler::emitResume() js/src/wasm/WasmIonCompile.cpp:9901
    #11 (anonymous namespace)::FunctionCompiler::emitBodyExprs() js/src/wasm/WasmIonCompile.cpp:10513
    #12 (anonymous namespace)::RootCompiler::generate() js/src/wasm/WasmIonCompile.cpp:11320
    #13 js::wasm::IonCompileFunctions(...) js/src/wasm/WasmIonCompile.cpp:11475
    #14 ExecuteCompileTask(...) js/src/wasm/WasmGenerator.cpp:627
    #16 js::wasm::ModuleGenerator::finishFuncDefs() js/src/wasm/WasmGenerator.cpp:860
    #17 DecodeCodeSection<...>(...) js/src/wasm/WasmCompile.cpp:976
    #19 js::WasmModuleObject::construct(JSContext*, unsigned int, JS::Value*) js/src/wasm/WasmJS.cpp:1968
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 (deliberate crash from the failed MOZ_ASSERT)
SUMMARY: UndefinedBehaviorSanitizer: SEGV in MOZ_CrashSequence
Attached file jspi_ensureballast.js
Attached patch fix.patchSplinter Review
Attached file crash_stack.txt
Group: core-security → javascript-core-security

Might be the same issue as bug 2042726.

Duplicate of this bug: 2042726

This is an issue for stack-switching, not JS-PI. JS-PI uses this code but only ever uses a single suspend param and can't trigger this. JS-PI is enabled in nightly but stack switching is not.

Severity: -- → S3
Priority: -- → P3
Assignee: nobody → ydelendik

Bug 2042765 is a similar issue elsewhere. Maybe worth fixing both at the same time.

Group: javascript-core-security
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Keywords: sec-other
See Also: → 2042765
Status: ASSIGNED → RESOLVED
Closed: 8 days ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: