High firefox UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the JavaScript: WebAssembly component
ComponentSpiderMonkey
Bug ClassUAF
Tracker2052688
Fix commit9823b5c86ffa (firefox) +18/-0
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-08-18

Background

`JitActivation`
a SpiderMonkey object representing a span of JIT/Wasm execution on the stack, holding trap and exit-frame bookkeeping for that activation.
`wasm::Code`
a reference-counted Wasm code container owning the executable machine-code segment(s) that trap stubs and compiled functions run inside.
Wasm trap
a runtime fault (such as an indirect-call signature mismatch) that transfers control to a trap stub, which unwinds and raises a JavaScript RuntimeError.
Tail call
a Wasm call that reuses/unwinds the caller’s frame, so the calling instance’s frame is gone by the time the callee traps.

Root Cause Analysis

When a Wasm trap fired, JitActivation::startWasmTrap looked up the trapping wasm::Code via wasm::LookupCode(pc) but retained no owning reference to it; it only recorded the effective-instance frame pointer through setWasmExitFP(fp). The implicit invariant was that some live wasm::Frame on the stack keeps the trapping code segment alive for the duration of trap handling, but for an IndirectCallBadSig trap reached through a tail call, the trapping code belongs to a different instance whose frame has already been unwound, so nothing on the stack keeps that code alive. A garbage collection during trap handling (for example while allocating the RuntimeError) could then free the wasm::Code segment that the trap stub was still executing in, producing a use-after-free.

The fix adds a RefPtr<const wasm::Code> wasmTrapCode_ member that startWasmTrap populates with wasm::LookupCode(state.pc) and asserts non-null, holding a strong reference across the GC; finishWasmTrap deliberately leaves it set because execution is still returning into that code, releasing it only on the next trap or when the activation is destroyed.

Key insight
The core mistake was relying on a stack frame to keep the trapping Wasm code alive during trap handling, an assumption that breaks when a tail call has already unwound that frame; the fix takes an explicit owning RefPtr to the trapping wasm::Code so it survives any GC until the trap finishes.

Attack Path

  1. Craft the modules An attacker supplies Wasm that sets up a cross-instance tail call, where the callee lives in a different wasm::Code than the effective instance.
  2. Trigger the trap The tail-called function performs an indirect call with a mismatched signature, raising an IndirectCallBadSig trap after the caller’s frame has been unwound.
  3. Force a collection Trap handling allocates a RuntimeError, which can drive a garbage collection while no stack frame references the trapping code segment.
  4. Free the code The GC reclaims the wasm::Code segment that the trap stub is still executing in, leaving a dangling executable region.
  5. Exploit the dangling code Continued execution runs inside freed (and potentially reallocated) memory, yielding a use-after-free that can corrupt state or redirect control flow.

Impact Assessment

An attacker who can run WebAssembly (any web page) gains a use-after-free on a freshly freed executable Wasm code segment inside the content process, a high-severity primitive that can lead to memory corruption and potentially code execution. Preconditions are only that the victim executes the malicious Wasm and that a GC occurs during the narrow trap-handling window; no special privileges are required.

Files Changed

  • js/src/vm/JitActivation.cpp
  • js/src/vm/JitActivation.h

Audit Directions

  • Lifetime tied to stack frames
    Audit trap, exit, and unwinding paths that assume a wasm::Frame keeps a wasm::Code/instance alive; tail calls and other frame-eliding optimizations can invalidate that assumption.
  • GC across native handling windows
    Look for allocations (like RuntimeError construction) reachable during low-level handlers where currently-executing code or data is not explicitly rooted by a RefPtr or GC root.
  • `LookupCode` result usage
    Review call sites that use wasm::LookupCode(pc) results transiently without taking an owning reference, especially where the looked-up code differs from the effective instance.
diff --git a/js/src/vm/JitActivation.cpp b/js/src/vm/JitActivation.cpp
index 5fff795afc9..4c98a2ec424 100644
--- a/js/src/vm/JitActivation.cpp
+++ b/js/src/vm/JitActivation.cpp
@@ -244,6 +244,14 @@ void js::jit::JitActivation::startWasmTrap(wasm::Trap trap,
   const wasm::Code& code = wasm::GetNearestEffectiveInstance(fp)->code();
   MOZ_RELEASE_ASSERT(&code == wasm::LookupCode(pc));
 
+  // Keep the trapping code alive across a GC. For an IndirectCallBadSig trap
+  // reached through a tail call, this code can belong to a different instance
+  // than the effective one above, whose frame has already been unwound. Nothing
+  // else then keeps alive the code segment we are about to run the trap stub
+  // in.
+  wasmTrapCode_ = wasm::LookupCode(state.pc);
+  MOZ_RELEASE_ASSERT(wasmTrapCode_);
+
   setWasmExitFP(fp);
   wasmTrapData_.emplace();
   wasmTrapData_->resumePC =
@@ -271,6 +279,9 @@ void js::jit::JitActivation::finishWasmTrap() {
   MOZ_ASSERT(hasWasmExitFP());
   MOZ_ASSERT(isWasmTrapping());
   wasmTrapData_.reset();
+  // Keep wasmTrapCode_ alive: we are still executing inside this code and are
+  // about to return into it, so releasing it now could free it. It is released
+  // on the next trap or when the activation is destroyed.
   packedExitFP_ = nullptr;
   MOZ_ASSERT(!isWasmTrapping());
 }
diff --git a/js/src/vm/JitActivation.h b/js/src/vm/JitActivation.h
index 4f1b2d4c248..884ff99bf4b 100644
--- a/js/src/vm/JitActivation.h
+++ b/js/src/vm/JitActivation.h
@@ -8,6 +8,7 @@
 #include "mozilla/Assertions.h"  // MOZ_ASSERT
 #include "mozilla/Atomics.h"     // mozilla::Atomic, mozilla::Relaxed
 #include "mozilla/Maybe.h"       // mozilla::Maybe
+#include "mozilla/RefPtr.h"      // RefPtr
 
 #include <stddef.h>  // size_t
 #include <stdint.h>  // uint8_t, uint32_t, uintptr_t
@@ -94,6 +95,12 @@ class JitActivation : public Activation {
   // purposes. Wasm code can't trap reentrantly.
   mozilla::Maybe<wasm::TrapData> wasmTrapData_;
 
+  // Keeps the trapping code alive while the trap is handled. With tail calls
+  // the trapping frame may already be unwound, so no wasm::Frame keeps it alive
+  // and a GC (e.g. while building the RuntimeError) could otherwise free the
+  // code segment we are still executing in.
+  RefPtr<const wasm::Code> wasmTrapCode_;
+
 #ifdef CHECK_OSIPOINT_REGISTERS
  protected:
   // Used to verify that live registers don't change between a VM call and
Loading diff…