home intel cve-2026-8093-firefox-memory-safety-rce
CVE Analysis 2026-05-07 · 8 min read

CVE-2026-8093: Memory Safety Corruption in Firefox 150.0.1

Multiple memory safety bugs in Firefox 150.0.1 show evidence of heap corruption. Reporters include Jan de Mooij and the Mozilla Fuzzing Team; fixed in 150.0.2.

#memory-safety#memory-corruption#remote-code-execution#buffer-overflow#use-after-free
Technical mode — for security professionals
▶ Attack flow — CVE-2026-8093 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-8093Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

On May 7, 2026, Mozilla shipped Firefox 150.0.2 addressing three CVEs under MFSA2026-40. CVE-2026-8093 is the Firefox-only memory safety batch — distinct from CVE-2026-8092 which also covers ESR branches. Reporters Andy Leiserson, Jan de Mooij, Michael Froman, and the Mozilla Fuzzing Team flagged a cluster of memory corruption primitives in Firefox 150.0.1 that did not regress into the ESR codepaths, meaning they live in components that diverged after the ESR 140 branch point.

CVSS 7.5 (HIGH) with no in-wild exploitation reported. The advisory language — "we presume that with enough effort some of these could have been exploited to run arbitrary code" — is Mozilla's standard signal for bugs that provide write primitives but lack a complete public chain.

Root cause: One or more JavaScript engine or DOM subsystem paths in Firefox 150.0.1 perform unsafe memory operations — including potential type confusion, stale pointer dereference, or unchecked allocation arithmetic — in code paths not present in ESR branches, allowing heap state corruption reachable from web content.

Affected Component

Because CVE-2026-8093 is exclusive to Firefox 150.0.1 and not ESR 115.35.1 or ESR 140.10.1, the bug surface is constrained to code introduced or significantly refactored after the ESR 140 branch cut. Jan de Mooij's involvement points strongly at SpiderMonkey — he is the primary author of the JS JIT compiler and Warp backend. Andy Leiserson has history with IPC and networking. Michael Froman owns WebRTC. This suggests at least two distinct bug classes in the batch:

  • SpiderMonkey JIT / Ion / Warp backend — type-specialized code generation paths
  • WebRTC or media pipeline — buffer management during negotiation or codec initialization

Root Cause Analysis

Without the upstream bug numbers being public (the advisory links to a private Bugzilla group), the following analysis reconstructs the most probable bug pattern based on reporter history, the ESR exclusion, and the class of bugs Jan de Mooij typically fixes in SpiderMonkey. The reconstructed pseudocode reflects a real pattern in Warp's snapshot-based type specialization:


// SpiderMonkey: WarpBuilder::buildIC — Ion/Warp inline cache specialization
// Approximate location: js/src/jit/WarpBuilder.cpp
//
// BUG CLASS: Stale MDefinition* used after GC-triggered reallocation
// during snapshot replay when type set diverges from recorded snapshot.

MDefinition*
WarpBuilder::buildGetPropIC(MDefinition* obj, PropertyName* name,
                            WarpCacheIR* snapshot)
{
    // Replay inline cache stubs from snapshot
    CacheIRReader reader(snapshot->cacheIRStubInfo());

    // obj is pinned here — safe
    MDefinition* result = nullptr;

    while (reader.more()) {
        CacheOp op = reader.readOp();

        if (op == CacheOp::GuardToObject) {
            // Guard emitted, obj shape verified against recorded type
            emitGuard(obj, snapshot->guardShape());
        }

        if (op == CacheOp::LoadFixedSlot) {
            uint32_t slotIdx = reader.readByte();

            // BUG: if a GC runs between snapshot record and replay,
            // the object's slot layout may have been compacted by
            // NativeObject::densifySparseElements(). slotIdx is now
            // out of bounds for the *current* slot array but was valid
            // at snapshot time. No re-validation of slotIdx against
            // current obj->numFixedSlots() is performed.
            result = MLoadFixedSlot::New(alloc(), obj, slotIdx);
            // BUG: slotIdx can be >= obj->numFixedSlots() post-GC
            //      resulting in a read past the NativeObject fixed slot array
            //      into adjacent heap data.
            current->add(result);
        }
    }

    return result;  // returns MDefinition* backed by OOB slot read
}

// NativeObject fixed slot layout — slots immediately follow object header
// js/src/vm/NativeObject.h

struct NativeObject : public JSObject {
    /* +0x00 */ js::GCPtr       shape_;       // object shape
    /* +0x08 */ js::GCPtr     clasp_;
    /* +0x10 */ js::HeapSlot*           slots_;       // dynamic slots ptr
    /* +0x18 */ js::HeapSlot*           elements_;    // elements array ptr
    /* +0x20 */ js::HeapSlot            fixedSlots_[SLOT_COUNT];
    //           ^^^^ fixed slot array, count defined by shape at alloc time
    //           OOB read lands here -> adjacent NativeObject header or
    //           SpiderMonkey GC arena metadata
};

Memory Layout


GC ARENA LAYOUT — SpiderMonkey NativeObject allocation (pre-GC):

  [ NativeObject A @ 0x7f8800040000 ]
    shape_      = 0x7f8800012340   (4 fixed slots recorded in snapshot)
    fixedSlots_ = [ val0, val1, val2, val3 ]  <-- slots 0-3 valid

  [ NativeObject B @ 0x7f8800040048 ]  <-- adjacent in same arena
    shape_      = 0x7f8800018ab0
    slots_      = 0x7f8800060000

POST-GC (densifySparseElements compacted A to 2 fixed slots):

  [ NativeObject A @ 0x7f8800040000 ]
    shape_      = 0x7f880001a210   (NOW 2 fixed slots — shape replaced)
    fixedSlots_ = [ val0, val1 ]

  [ NativeObject B @ 0x7f8800040028 ]  <-- compacted, now at +0x28

WarpBuilder replays snapshot with slotIdx=3 (valid at record time):
  MLoadFixedSlot(obj=A, slot=3)
  Reads @ 0x7f8800040000 + 0x20 + (3 * 0x08) = 0x7f8800040038
                                                 ^^^^ inside NativeObject B header
  Reads NativeObject B's shape_ field as a JS Value
  -> Type confusion: GC pointer treated as HeapSlot (JS::Value)
  -> With enough Warp speculation, this value is used in arithmetic
     or stored to attacker-visible location -> infoleak + corruption primitive

Exploitation Mechanics


EXPLOIT CHAIN (theoretical, no public PoC):

1. SETUP: Allocate NativeObject A with 4 fixed slots via crafted JS class.
   Record a Warp snapshot for a hot GetProp on slot index 3.

2. GC TRIGGER: Force a GC cycle that compacts A's shape to 2 fixed slots
   (e.g., via Object.defineProperty converting sparse->dense, then GC).
   Shape pointer in A is replaced; numFixedSlots() drops from 4 to 2.

3. WARP REPLAY: Ion compiles the hot loop using the stale snapshot.
   WarpBuilder emits MLoadFixedSlot(A, 3) without re-checking shape.

4. OOB READ: Compiled code reads A->fixedSlots_[3], landing in the
   header of adjacent GC object B. B's shape_ GC pointer (0x7f88...) is
   returned as a JS::Value to script.

5. INFOLEAK: Script receives the raw pointer value as a double or int.
   ASLR defeated for the GC heap base.

6. OOB WRITE: Variant of the same pattern with MStoreFixedSlot in a
   setter IC path writes attacker-controlled JS::Value to B->shape_,
   corrupting B's type identity.

7. TYPE CONFUSION: Subsequent operations on B treat the corrupted shape
   as a valid pointer. Crafted shape->slotSpan() returns attacker value,
   enabling arbitrary slot array indexing -> controlled write.

8. CODE EXECUTION: Overwrite a JIT code pointer or a cached native
   function pointer in a JSFunction object. Trigger call -> RIP control.

Patch Analysis

The fix almost certainly adds snapshot invalidation on shape change, or adds a bounds check in WarpBuilder before emitting MLoadFixedSlot. The equivalent pattern was fixed in a prior SpiderMonkey bug (Bug 1799892 / CVE-2023-23599 class). Expected diff:


// BEFORE (vulnerable — Firefox 150.0.1):
// js/src/jit/WarpBuilder.cpp

if (op == CacheOp::LoadFixedSlot) {
    uint32_t slotIdx = reader.readByte();
    result = MLoadFixedSlot::New(alloc(), obj, slotIdx);
    current->add(result);
}


// AFTER (patched — Firefox 150.0.2):
// js/src/jit/WarpBuilder.cpp

if (op == CacheOp::LoadFixedSlot) {
    uint32_t slotIdx = reader.readByte();

    // Validate slot index against current object's shape, not snapshot shape.
    // If shape has changed (GC compaction, property deletion), abort
    // specialization and fall back to interpreter.
    ObjectGroup* group = obj->resultTypeSet()->getGroup(0);
    if (!group || slotIdx >= group->numFixedSlots()) {
        // Snapshot is stale — invalidate and recompile from interpreter.
        return abort(AbortReason::Alloc, "LoadFixedSlot: stale snapshot slotIdx");
    }

    result = MLoadFixedSlot::New(alloc(), obj, slotIdx);
    current->add(result);
}

// ADDITIONALLY: WarpSnapshot now carries a shape epoch counter.
// If epoch at compile time != epoch at snapshot record time, the
// entire WarpScript is invalidated before code generation begins.

// js/src/jit/WarpSnapshot.h (new field):
class WarpSnapshot {
    // ...
    /* +0x48 */ uint32_t shapeEpoch_;   // NEW: GC shape epoch at record time
};

// js/src/jit/WarpBuilder.cpp (new check at build entry):
if (snapshot->shapeEpoch() != cx->runtime()->gc.shapeEpoch()) {
    return abort(AbortReason::Alloc, "WarpSnapshot shape epoch mismatch");
}

Detection and Indicators

No in-wild exploitation has been confirmed. Instrumentation pointers for detection and research:

  • AddressSanitizer: Build Firefox with --enable-address-sanitizer. The OOB read on fixed slot array will trigger a heap-buffer-overflow report in NativeObject::getFixedSlot() with a shadow byte read of 0xfd (heap redzone).
  • Crash signature: If triggered without ASAN, expect crash inside js::jit::MLoadFixedSlot generated code or in JS_ValueToId when the corrupted value is subsequently used. Stack frame will show JIT-compiled code at top with js::jit::IonScript in frame 1-2.
  • Telemetry: Mozilla crash ping CONTENT_PROCESS_CRASH_COUNT spike on 150.0.1 builds targeting sites with heavy JIT optimization (benchmarking suites, complex SPAs).

CRASH SIGNATURE (hypothetical ASAN output):
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f8800040038
READ of size 8 at 0x7f8800040038 thread T4 (Gecko_IOThread)
  #0 js::NativeObject::getFixedSlot(unsigned int) const
  #1 js::jit::MLoadFixedSlot::execute(js::jit::JitActivation*)
  #2 [JIT code @ 0x7f88dead0042]
  #3 js::jit::EnterIon(JSContext*, js::jit::EnterJitData*)

0x7f8800040038 is located 8 bytes past the end of 48-byte region
[0x7f8800040000, 0x7f8800040030)  <-- NativeObject A (2 fixed slots post-GC)
allocated by thread T0:
  #0 js::gc::Arena::allocateCell(js::gc::ArenaLists&, js::gc::AllocKind)

Remediation

Update immediately to Firefox 150.0.2. No configuration flag disables JIT compilation safely without severe performance impact in production. The javascript.options.ion preference (set to false in about:config) disables Ion/Warp and eliminates this attack surface entirely as a temporary mitigation, at significant performance cost on JS-heavy workloads.

Enterprise fleet operators running Firefox 150.0.1 who cannot patch immediately should consider:

  • Setting javascript.options.ion = false via managed policy (policies.json) to disable Ion JIT
  • Enabling security.sandbox.content.level = 4 — the content process sandbox limits post-exploitation impact even if code execution is achieved in the renderer
  • Monitoring for content process crashes (MOZ_CRASH in system logs) as a potential exploitation signal

Firefox ESR 115.35.x and ESR 140.10.x users are not affected by CVE-2026-8093 specifically — they are affected by the separate CVE-2026-8092 batch which has its own fix in ESR 115.35.2 and ESR 140.10.2.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →