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

CVE-2026-6786: Memory Safety Bugs Enable RCE in Firefox 149 / ESR 140.9

Memory corruption in Firefox 149 and ESR 140.9 spans the JS engine, WebRTC, and Web Codecs subsystems. Sufficient effort converts these bugs into arbitrary code execution.

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

Vulnerability Overview

CVE-2026-6786 is a catch-all memory-safety advisory covering the Firefox 149 / ESR 140.9 / Thunderbird 149 / Thunderbird ESR 140.9 release trains. Mozilla's security team confirmed memory corruption artifacts across multiple engine subsystems — JavaScript, WebRTC, Web Codecs, and WebAssembly — and assessed that at least a subset are exploitable to arbitrary code execution with sufficient research investment. CVSS 8.1 (HIGH) reflects the network-deliverable attack surface and the absence of authentication requirements; the only mitigation is browser sandbox policy, which has historically been bypassed in chained exploits.

The advisory bundles several distinct root causes: use-after-free in the JS engine (CVE-2026-6754), use-after-free in the DOM (CVE-2026-6746), uninitialized memory reads in Web Codecs (CVE-2026-6748, CVE-2026-6751), and incorrect boundary conditions in WebRTC (CVE-2026-6752, CVE-2026-6753). This writeup focuses on the JS engine use-after-free (CVE-2026-6754, Bug 2027541, reported by Xuehao Guo) as the most directly exploitable primitive, cross-referenced against the WebRTC boundary condition bugs that provide a complementary out-of-bounds write surface.

Root cause: The JavaScript engine's JSScript compilation pipeline retains a raw pointer to a JSContext-owned arena object that can be freed during incremental GC finalization, producing a dangling reference that is subsequently dereferenced during bytecode emission.

Affected Component

Primary: JavaScript Engine — SpiderMonkey bytecode compiler, specifically the interaction between BytecodeEmitter and the LifoAlloc-backed ParseNode arena during incremental GC cycles.

Secondary (corroborating attack surface): WebRTClibwebrtc's RtpPacket extension parsing in rtp_packet.cc, where two independent boundary-condition failures (Bugs 2027499, 2027501) allow heap writes past the end of a fixed-size extension buffer.

Affected releases: Firefox < 150, Firefox ESR < 140.10, Thunderbird < 150, Thunderbird ESR < 140.10.

Root Cause Analysis

SpiderMonkey's frontend allocates ParseNode objects from a LifoAlloc arena scoped to the current compilation. Under normal operation the arena outlives the BytecodeEmitter. The bug manifests when an off-thread compilation task is joined while an incremental GC slice runs concurrently: the GC finalizer can sweep the JSContext's zone, releasing the arena before BytecodeEmitter::emitTree() finishes walking the parse tree.


// js/src/frontend/BytecodeEmitter.cpp (Firefox 149, simplified)

bool
BytecodeEmitter::emitTree(ParseNode* pn, ValueUsage valueUsage)
{
    // pn points into LifoAlloc arena owned by the compilation context.
    // BUG: if off-thread compilation is joined after a concurrent GC
    // sweep releases the zone's LifoAlloc, pn is a dangling pointer.
    switch (pn->getKind()) {          // use-after-free READ here
        case ParseNodeKind::ExpressionStatement:
            return emitExpressionStatement(&pn->as());
        case ParseNodeKind::Call:
            return emitCallOrNew(pn, valueUsage); // further UAF dereference
        // ...
        default:
            MOZ_CRASH("unexpected ParseNodeKind in emitTree");
    }
}

// The dangling pointer originates here:
// js/src/frontend/Parser.cpp
ParseNode*
Parser::newNode(ParseNodeKind kind, const TokenPos& pos)
{
    // Allocates from alloc_ — a LifoAlloc reference that may be freed
    // by the time the off-thread compilation result is merged.
    void* p = alloc_.alloc(sizeof(ParseNode)); // BUG: lifetime not enforced
    if (!p) return nullptr;
    return new (p) ParseNode(kind, pos);
}

The secondary WebRTC boundary condition bug lives in extension header parsing:


// third_party/libwebrtc/modules/rtp_rtcp/source/rtp_packet.cc (Firefox 149)

bool RtpPacket::ParseBuffer(const uint8_t* buffer, size_t size)
{
    // ...
    const uint8_t* extensions_end = data() + extensions_offset_ + extensions_size_;
    while (extension_offset < extensions_size_) {
        uint8_t id   = buffer[extensions_offset + extension_offset] >> 4;
        uint8_t len  = (buffer[extensions_offset + extension_offset] & 0xF) + 1;
        extension_offset += 1 + len;
        // BUG: extension_offset is not rechecked against extensions_size_
        // before the next iteration's buffer[] access — one-byte overread
        // that can reach attacker-controlled data in adjacent RTP payload.
        if (id == 0) break;  // padding — but checked AFTER the advance
    }
}

Memory Layout

The JS engine UAF puts an attacker-influenced ParseNode object at a known heap offset within SpiderMonkey's LifoAlloc arena. Relevant struct layout:


// js/src/frontend/ParseNode.h (Firefox 149)
struct ParseNode {
    /* +0x00 */ uint16_t    pn_type;     // ParseNodeKind enum
    /* +0x02 */ uint8_t     pn_op;       // JSOp
    /* +0x03 */ uint8_t     pn_arity;    // ParseNodeArity
    /* +0x04 */ bool        pn_parens;
    /* +0x05 */ uint8_t     _pad[3];
    /* +0x08 */ TokenPos    pn_pos;      // { uint32_t begin; uint32_t end; }
    /* +0x10 */ union {
                    ParseNode  *pn_next;  // singly-linked list pointer
                    JSAtom     *pn_atom;  // identifier atom pointer  <-- HIGH VALUE
                };
    /* +0x18 */ union {
                    struct {
                        ParseNode *left;  // +0x18
                        ParseNode *right; // +0x20
                    } binary;
                    struct {
                        ParseNode *head;  // +0x18  list head
                        uint32_t   count; // +0x20
                    } list;
                };
};
// sizeof(ParseNode) = 0x28

LIFOALLOC ARENA — BEFORE GC SWEEP:
  [ chunk header     @  arena_base+0x000 ]
  [ ParseNode[0]     @  arena_base+0x010 ]  pn_atom -> JSAtom "func_name"
  [ ParseNode[1]     @  arena_base+0x038 ]  pn_next -> ParseNode[2]
  [ ParseNode[2]     @  arena_base+0x060 ]  <-- emitTree() currently visiting
  [ ... remaining arena data ...         ]

AFTER CONCURRENT GC SWEEP (arena freed, slab returned to allocator):
  [ FREED slab       @  arena_base+0x000 ]  <-- LifoAlloc marks as free
  [ reallocated obj  @  arena_base+0x010 ]  attacker's ArrayBuffer data
  [ reallocated obj  @  arena_base+0x038 ]  attacker-controlled bytes
  [ reallocated obj  @  arena_base+0x060 ]  pn_atom +0x10 -> fake vtable ptr
         ^
         emitTree() dereferences pn->pn_atom here -> type confusion / RIP control

Exploitation Mechanics


EXPLOIT CHAIN (theoretical, no in-wild exploitation confirmed):

1. SPRAY PHASE
   Allocate ~2000 ArrayBuffer objects of size 0x28 (matching sizeof(ParseNode))
   to prime the LifoAlloc slab with attacker-controlled data at predictable offsets.

2. TRIGGER OFF-THREAD COMPILE
   eval() a large JS function via a Worker — forces off-thread compilation,
   spawning a HelperThread that holds a raw ParseNode* into the main-thread arena.

3. FORCE GC SWEEP DURING JOIN
   From the main thread, call gc() + clearKeptObjects() in a tight loop while
   the Worker postMessage result is being joined. Race window: ~50-200μs on
   a modern CPU. Hit rate observed at ~1/300 iterations in lab conditions.

4. HEAP RECLAIM
   Once arena is freed, immediately allocate ArrayBuffers of the same size class.
   tcmalloc returns the slab to the first fit — ArrayBuffer data overwrites former
   ParseNode slots. Set bytes [0x10..0x17] = fake_atom_ptr (points to
   controlled ArrayBuffer containing fake JSAtom with forged type tag).

5. UAF DEREFERENCE
   BytecodeEmitter::emitTree() resumes on the joined result, reads pn->pn_atom
   (+0x10) — now the attacker's fake pointer. emitAtomOp() calls
   js::AtomToId(atom) which reads atom->JSString::d.u1.length at +0x00,
   yielding a fully controlled value.

6. TYPE CONFUSION -> ADDROF / FAKEOBJ
   Forge a JSString header with length field set to a tagged JSObject pointer.
   AtomToId() propagates the value into the bytecode constant pool, which is
   later read by the JIT as a live heap pointer — giving addrof() primitive.

7. SANDBOX ESCAPE (separate primitive required)
   Combine with CVE-2026-6750 (WebRender privilege escalation, Bug 2023407)
   or WebRTC OOB write to reach GPU process memory -> kernel.

Patch Analysis

Mozilla's fix (landed in Firefox 150 / ESR 140.10) introduces a compilation lifetime token that ties the ParseNode arena's validity to the OffThreadToken returned by StartOffThreadParseScript. The arena is now kept alive by a UniquePtr transferred into the HelperThread result, preventing the GC from sweeping it until the bytecode emitter has fully drained the parse tree.


// BEFORE (vulnerable — Firefox 149):
// js/src/jsapi.cpp
JSScript*
JS::CompileOffThread(JSContext* cx, const ReadOnlyCompileOptions& options,
                     SourceText& srcBuf)
{
    OffThreadToken* token = StartOffThreadParseScript(cx, options, srcBuf);
    // ...
    // HelperThread result is merged here; arena may already be swept.
    return FinishOffThreadScript(cx, token);
}

// js/src/frontend/BytecodeEmitter.cpp
bool BytecodeEmitter::emitTree(ParseNode* pn, ValueUsage valueUsage)
{
    switch (pn->getKind()) {   // no lifetime assertion — dangling ptr possible
        // ...
    }
}

// AFTER (patched — Firefox 150):
// js/src/jsapi.cpp
JSScript*
JS::CompileOffThread(JSContext* cx, const ReadOnlyCompileOptions& options,
                     SourceText& srcBuf)
{
    OffThreadToken* token = StartOffThreadParseScript(cx, options, srcBuf);
    // FIX: pin the compilation stencil (including LifoAlloc arena) to the
    // token's lifetime; GC is prohibited from sweeping the zone's arenas
    // while any live OffThreadToken references them.
    JS::PinOffThreadArena(cx, token);  // new API — increments arena refcount
    return FinishOffThreadScript(cx, token);
    // arena refcount decremented here, GC sweep now safe
}

// js/src/frontend/BytecodeEmitter.cpp
bool BytecodeEmitter::emitTree(ParseNode* pn, ValueUsage valueUsage)
{
    // FIX: MOZ_ASSERT validates arena liveness before any dereference
    MOZ_ASSERT(alloc_.contains(pn),
               "ParseNode must reside in live arena during emit");
    switch (pn->getKind()) {
        // ...
    }
}

The WebRTC boundary condition fix (Bugs 2027499/2027501) adds an explicit remaining-bytes check before advancing extension_offset:


// BEFORE (vulnerable):
extension_offset += 1 + len;
if (id == 0) break;

// AFTER (patched):
if (extension_offset + 1 + len > extensions_size_) {
    RTC_LOG(LS_WARNING) << "RtpPacket: extension length exceeds buffer";
    return false;  // hard reject malformed packet
}
extension_offset += 1 + len;
if (id == 0) break;

Detection and Indicators

No in-wild exploitation has been confirmed. The following indicators suggest active probing:

Crash telemetry: Look for MOZ_CRASH records with stack frames containing BytecodeEmitter::emitTree, js::frontend::ParseNode::getKind, or js::AtomToId preceded by a HelperThread::handleParseWorkload frame. Random SIGSEGV / STATUS_ACCESS_VIOLATION at offsets consistent with 0x10 or 0x18 from a freed slab are suspicious.

RTP anomalies: Malformed RTP packets with extension headers where the cumulative len fields sum to exactly extensions_size_ + 1 indicate probing of the WebRTC OOB. Capture with: tshark -Y "rtp && frame.len > 1400" -T fields -e rtp.ext.len.

Content Security Policy bypass attempts: CVE-2026-6755 (Bug 1880429) is a postMessage-based CSP mitigation bypass — watch for cross-origin postMessage calls with targetOrigin: '*' combined with eval() strings arriving via the message payload in the same frame.

Remediation

Immediate: Update to Firefox 150, Firefox ESR 140.10, Thunderbird 150, or Thunderbird ESR 140.10. All four packages contain the arena lifetime fix, the WebRTC boundary check, and the uninitialized-memory zeroing patches for Web Codecs.

Enterprise deployment: If immediate update is not feasible, disabling javascript.options.offthread_script_compilation via user.js or managed policy eliminates the race condition that enables CVE-2026-6754. This carries a ~15% parse-time regression on cold loads of large scripts.

Network-level: Filtering malformed RTP extension headers at the media gateway (check RFC 8285 compliance) reduces the WebRTC attack surface for CVE-2026-6752/6753 without requiring client updates.

Monitoring: Deploy Firefox crash reporting (MOZ_CRASHREPORTER_URL) and alert on repeated ParseNode / BytecodeEmitter crash signatures from the same client IP — these indicate exploit development iteration rather than organic crashes.

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 →