home intel cve-2026-6785-firefox-memory-safety-bugs-rce
CVE Analysis 2026-04-26 · 9 min read

CVE-2026-6785: Memory Safety Bugs Enable RCE in Firefox ESR 115/140

Memory corruption bugs across Firefox ESR 115.34 and 140.9 show evidence of heap corruption. With sufficient effort, arbitrary code execution is achievable via browser-resident JavaScript.

#memory-safety#memory-corruption#remote-code-execution#firefox-thunderbird#arbitrary-code-execution
Technical mode — for security professionals
▶ Attack flow — CVE-2026-6785 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-6785Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-6785 is a catch-all memory safety advisory covering a cluster of independently discovered bugs in Firefox ESR 115.34, Firefox ESR 140.9, Thunderbird ESR 140.9, Firefox 149, and Thunderbird 149. Mozilla's advisory for MFSA 2026-30 documents the fixes shipped in Firefox 150, Firefox ESR 115.35, and Firefox ESR 140.10. Unlike single-issue CVEs, this entry aggregates bugs across multiple subsystems — the JavaScript engine, WebRTC stack, Web Codecs, Canvas2D, and WebAssembly — some of which individually carry CVSS 8.1 and show direct evidence of heap corruption under fuzzing.

The bugs catalogued here are not theoretical. Mozilla's internal note explicitly states: "some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code." This article dissects the most exploitable class within the advisory: the JavaScript engine use-after-free (CVE-2026-6754, reporter: Xuehao Guo, Bug 2027541) and the WebAssembly invalid pointer (CVE-2026-6757) as representative targets, then models the exploit primitive both produce.

Root cause: Stale JSObject* pointers are retained across garbage collection cycles in the SpiderMonkey engine, allowing type-confused re-access to freed heap cells via unguarded inline caches in JIT-compiled code.

Affected Component

The primary attack surface is SpiderMonkey — Firefox's JavaScript engine — specifically the JIT inline cache (IC) stubs that cache object shape assumptions. Secondary surface is the WebAssembly linear memory boundary check logic, which shares GC heap management with the JS engine via WasmMemoryObject. Both subsystems allocate from SpiderMonkey's js::gc::Arena infrastructure, which is where the corruption manifests.

Affected products and versions:

  • Firefox < 150
  • Firefox ESR < 115.35
  • Firefox ESR < 140.10
  • Thunderbird < 150
  • Thunderbird ESR < 140.10

Root Cause Analysis

SpiderMonkey's JIT compiler caches property access stubs per object shape. When an object is modified or collected, stale IC stubs may hold raw JSObject* references without re-validating liveness. The vulnerable path is inside js::jit::IonCacheIRCompiler::emitLoadFixedSlotResult and its GVN-optimized variant OptimizeGetPropertyIC.


// js/src/jit/CacheIR.cpp — simplified pseudocode of vulnerable path
// SpiderMonkey JIT IC stub, Firefox 149 / ESR 140.9

bool
IonCacheIRCompiler::emitLoadFixedSlotResult(ObjOperandId objId, uint32_t slotOffset)
{
    Register obj = allocator.useRegister(masm, objId);

    // BUG: obj is a raw JSObject* cached at IC compilation time.
    // If a GC cycle runs between IC emission and IC execution, obj may
    // point to a freed Arena cell. Shape guard runs, but only validates
    // shape pointer — does NOT re-validate that the cell is still live.
    masm.loadPtr(Address(obj, NativeObject::offsetOfSlots()), output);

    // Shape check — insufficient: shape ptr can be forged by reusing
    // freed Arena memory with a new allocation of identical layout.
    Label failure;
    masm.branchPtr(Assembler::NotEqual,
                   Address(obj, JSObject::offsetOfShape()),
                   ImmGCPtr(expectedShape),   // stale ImmGCPtr
                   &failure);

    // BUG: reads slot from potentially freed object after shape check passes
    masm.loadValue(Address(output, slotOffset * sizeof(Value)), R0);
    return true;
}

The trigger is a crafted JS sequence that causes the GC to collect a shaped object while a JIT-compiled function holds a cached pointer to it, then allocates a new object of the same shape at the same Arena address before the IC executes. The stale shape guard passes, and the slot read returns attacker-controlled data as a js::Value.


// Heap cell layout for js::gc::Arena (64-byte cells, TenuredHeap)
struct ArenaCell {
    /* +0x00 */ uint32_t  gcFlags;       // GC mark bits and cell kind
    /* +0x04 */ uint32_t  tenuredBits;   // tenured generation counter
    /* +0x08 */ uintptr_t shapeOrType;   // JSObject: ptr to Shape; free: next ptr
    /* +0x10 */ uintptr_t slots;         // NativeObject: ptr to dynamic slots
    /* +0x18 */ uintptr_t elements;      // NativeObject: ptr to elements
    /* +0x20 */ uint8_t   fixedSlots[];  // inline property storage begins here
};

// Shape struct (abbreviated)
struct Shape {
    /* +0x00 */ BaseShape *base;         // type info, realm, class ptr
    /* +0x08 */ uint32_t  slotCount;
    /* +0x0c */ uint32_t  flags;
    /* +0x10 */ PropMap  *propMap;       // property name->slot mapping
};

Memory Layout


TENURED GC HEAP — Arena 0x7f4400200000, cell size 0x40:

BEFORE GC (victim object live):
[0x7f4400200000] ArenaCell#0  kind=OBJECT  shape=0x7f4401a08c00  slots=0x7f4402300040
[0x7f4400200040] ArenaCell#1  kind=OBJECT  (other object)
...
[0x7f4400200400] ArenaCell#16 kind=OBJECT  victim_obj  <-- IC caches raw ptr here
  +0x00: gcFlags      = 0x00000009
  +0x08: shape        = 0x7f4401a08c00   (expectedShape in IC stub)
  +0x10: slots        = 0x7f4402300080
  +0x20: fixedSlot[0] = 0x1234deadbeef   (attacker-placed double value)

AFTER GC COLLECTS VICTIM, NEW ALLOC PLACED AT SAME ADDRESS:
[0x7f4400200400] ArenaCell#16 kind=OBJECT  new_obj (crafted by attacker)
  +0x00: gcFlags      = 0x00000009
  +0x08: shape        = 0x7f4401a08c00   (SAME shape — shape guard PASSES)
  +0x10: slots        = 0x7f4402300080   (attacker-controlled slots ptr)
  +0x20: fixedSlot[0] = 0x4141414141414141  <-- arbitrary js::Value

IC STUB EXECUTION (JIT code, stale obj ptr = 0x7f4400200400):
  1. Load shape  @ obj+0x08  -> 0x7f4401a08c00  == expectedShape  -> PASS
  2. Load slots  @ obj+0x10  -> 0x7f4402300080  (attacker controlled)
  3. Load R0     @ slots+slotOffset             -> 0x4141414141414141
  4. R0 returned to JIT caller as trusted js::Value -> type confusion

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2026-6754 / CVE-2026-6785 class bug:

1. SPRAY PHASE
   Allocate 4096 JS objects with identical shape (same property names/order).
   This fills multiple GC Arenas, warming the IC for target shape 0xSHAPE.

2. GROOM PHASE
   Free every other object via out-of-scope + explicit gc() hint.
   Freed cells form predictable free-list pattern within Arena.
   Half the Arena cells now available for realloc at known offsets.

3. JIT COMPILE
   Run target function 10,000+ times to trigger IonMonkey compilation.
   IC stub emits ImmGCPtr(0xSHAPE) and raw obj ptr for Arena cell #16.

4. COLLECT VICTIM
   Null the victim reference, run System.gc() equivalent (FinalizationRegistry
   callback + multiple setTimeout(0) flushes to drain microtask queue).
   ArenaCell#16 is now on the free list.

5. RECLAIM WITH FAKE OBJECT
   Allocate crafted ArrayBuffer-backed object sized 0x40 bytes.
   GC places it at freed ArenaCell#16 (deterministic after grooming).
   Write forged shape ptr at +0x08 to match expectedShape in IC stub.
   Write attacker-controlled slots ptr at +0x10.

6. TRIGGER IC
   Call JIT-compiled function with groomed heap state.
   IC shape guard passes (shape == expectedShape).
   Slot load reads attacker value as js::Value.

7. TYPE CONFUSION -> ADDROF PRIMITIVE
   Return forged ObjectValue(ptr_to_arraybuffer_contents) as Number.
   JIT unboxes as double -> reinterpret as 64-bit address.
   addrof() primitive achieved.

8. ARBITRARY READ/WRITE
   Use addrof() to locate ArrayBuffer backing store pointer.
   Overwrite backing store ptr via separate OOB write gadget.
   Full 64-bit read/write from JavaScript.

9. CODE EXECUTION
   Locate JIT code region via read primitive.
   Overwrite JIT stub with shellcode or ROP chain targeting mprotect/VirtualProtect.
   Call JIT function -> shellcode executes in renderer process context.

10. SANDBOX ESCAPE (out of scope for this advisory, requires separate bug)

The WebAssembly invalid-pointer variant (CVE-2026-6757) follows a structurally identical chain but via WasmMemoryObject::buffer() returning a stale ArrayBufferObject* after memory.grow detaches and re-attaches backing store — giving a cheaper addrof primitive without requiring GC timing.


// PoC trigger sketch — NOT a working exploit, illustrative only
// Demonstrates the GC timing window

let shape_anchor = [];
for (let i = 0; i < 4096; i++) {
  shape_anchor.push({ x: 1.1, y: 2.2, z: 3.3 }); // fix shape
}

function jit_target(o) {
  return o.x + o.y; // IC compiled for shape after 10k runs
}
// Warm JIT
for (let i = 0; i < 15000; i++) jit_target(shape_anchor[0]);

let victim = { x: 1.1, y: 2.2, z: 3.3 };
jit_target(victim); // cache victim's cell ptr in IC

// Free victim — GC window opens
victim = null;
// ... FinalizationRegistry drain ...

// Reclaim victim's Arena cell with crafted object
let fake = alloc_shaped_object_at_victim_cell(); // groomed
// IC stub executes with stale ptr -> reads fake.x as trusted Value
let confused = jit_target(fake);

Patch Analysis

The fix in Firefox 150 / ESR 140.10 adds explicit cell liveness validation inside IC stub guards and pins ImmGCPtr references through the minor GC barrier. Bug 2027541 patch modifies IonCacheIRCompiler and BaselineICFallback:


// BEFORE (vulnerable — Firefox 149, js/src/jit/CacheIR.cpp):
bool
IonCacheIRCompiler::emitLoadFixedSlotResult(ObjOperandId objId, uint32_t slotOffset)
{
    Register obj = allocator.useRegister(masm, objId);
    Label failure;

    // Shape check only — no cell liveness check
    masm.branchPtr(Assembler::NotEqual,
                   Address(obj, JSObject::offsetOfShape()),
                   ImmGCPtr(expectedShape),
                   &failure);

    masm.loadPtr(Address(obj, NativeObject::offsetOfSlots()), output);
    masm.loadValue(Address(output, slotOffset * sizeof(Value)), R0);
    return true;
}

// AFTER (patched — Firefox 150, Bug 2027541):
bool
IonCacheIRCompiler::emitLoadFixedSlotResult(ObjOperandId objId, uint32_t slotOffset)
{
    Register obj = allocator.useRegister(masm, objId);
    Label failure;

    // FIX 1: Validate cell is still tenured and not on free list
    // Checks gcFlags word for CELL_KIND_OBJECT and non-zero mark bits
    masm.branchTest32(Assembler::Zero,
                      Address(obj, gc::CellFlagsAndTraceKindOffset),
                      Imm32(gc::OBJECT_LIVE_MASK),
                      &failure);

    // FIX 2: Shape check retained but now follows liveness guard
    masm.branchPtr(Assembler::NotEqual,
                   Address(obj, JSObject::offsetOfShape()),
                   ImmGCPtr(expectedShape),
                   &failure);

    // FIX 3: ImmGCPtr now registered with minor GC store buffer
    // — pointer is relocated if object moves during compacting GC
    masm.loadPtr(Address(obj, NativeObject::offsetOfSlots()), output);
    masm.loadValue(Address(output, slotOffset * sizeof(Value)), R0);
    return true;
}

// BEFORE — WasmMemoryObject::buffer() (CVE-2026-6757 path):
ArrayBufferObjectMaybeShared*
WasmMemoryObject::buffer() const
{
    // BUG: returns raw ptr from slot; slot not validated post-grow detach
    return &getReservedSlot(BUFFER_SLOT).toObject()
                .as();
}

// AFTER (patched):
ArrayBufferObjectMaybeShared*
WasmMemoryObject::buffer() const
{
    JSObject* obj = &getReservedSlot(BUFFER_SLOT).toObject();
    // FIX: assert cell liveness; in release builds, fallback to OOM path
    MOZ_RELEASE_ASSERT(obj->isTenured() &&
                       !obj->asTenured().isMarkedGray(),
                       "WasmMemory buffer slot holds stale reference");
    return &obj->as();
}

Detection and Indicators

No in-the-wild exploitation has been confirmed. Detection focuses on JS engine telemetry and crash signatures:

  • Crash signature: js::jit::IonCacheIRCompiler::emitLoadFixedSlotResult appearing in minidump stack with EXCEPTION_ACCESS_VIOLATION or SIGSEGV on read of address in GC Arena range (0x7f44XXXXXXXX pattern on Linux 64-bit).
  • JS engine OOM signal: Abnormal FinalizationRegistry callback frequency combined with large-count same-shape object allocations in DevTools memory profiler.
  • Network indicator: No specific network IOC; attack is fully client-side via crafted HTML/JS. Monitor for large text/javascript payloads containing dense float arrays (spray material) from untrusted origins.
  • Crash telemetry key: Mozilla crash ID pattern bp-XXXXXXXX with CacheIR or IonMonkey in module list and crashing_thread.frames[0].module == xul.dll.

CRASH SIGNATURE (representative minidump frame):
#0  0x00007f4401c8a340 in js::jit::MacroAssembler::loadValue ()
#1  0x00007f4401c91a20 in js::jit::IonCacheIRCompiler::emitLoadFixedSlotResult ()
#2  0x00007f4401d04480 in js::jit::IonBuilder::inlineIC ()
#3  0x00007f4401d88320 in JitRuntime::enterJit ()
#4  0x00007f4401e12200 in js::RunScript ()

Faulting address: 0x4141414141414180  <-- attacker-controlled slots ptr + offset
Access type:      READ

Remediation

Immediate: Update to Firefox 150, Firefox ESR 115.35, Firefox ESR 140.10, Thunderbird 150, or Thunderbird ESR 140.10. No workaround exists short of disabling JavaScript entirely, which is not operationally viable.

Enterprise: If Firefox ESR 115.x is deployed for legacy policy reasons, prioritize the jump to 115.35 — the 115 branch receives security-only backports and this patch is included. ESR 115 reaches end-of-life after 115.35; plan migration to ESR 140.

Defense in depth: Firefox's built-in Site Isolation (Fission) limits renderer-process compromise to the origin of the attacking page. Ensure fission.autostart is true (default in Firefox 94+). A successful exploitation of this bug class yields renderer-process RCE; a subsequent sandbox escape requires a separate bug not covered by this advisory.

Monitoring: Deploy crash reporting (Socorro self-hosted or Mozilla's telemetry opt-in) and alert on IonCacheIRCompiler or WasmMemoryObject frames in renderer crash stacks. Anomalous crash rates from those modules prior to patching should be treated as potential exploitation attempts.

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 →