
CVE-2026-74970, Fission site isolation bypass in Firefox WebRender
This directory holds an honest reproduction scaffold for a Fission site-isolation authorization bypass in Firefox WebRender. Under Fission each content process owns a distinct PipelineId namespace, but the GPU-process code that registers a compositable PipelineId never checked that the PipelineId's namespace belonged to the sending process. A compromised content process can therefore register a PipelineId in another process's namespace and overwrite that origin's async image pipeline in the shared manager.
Everything here targets a locally built Firefox tree with synthetic content only. It never attacks, contacts, probes, or references any live or third party system. It is not remote code execution and not memory corruption; the threat model is an already-compromised content process.
Matching PoC repository: https://github.com/defineid/Trespasser
Writeup: writeup-webrender-pipelineid.html in this portfolio.
| Field | Value |
|---|
| CVE | CVE-2026-74970 |
| Component | Core / Graphics: WebRender |
| Class | Site-Isolation Bypass |
| CWE | 862 (Missing Authorization) · 284 (Improper Access Control) |
| Fixed in | Firefox 154 · ESR 153.1 |
| Bug | https://bugzilla.mozilla.org/show_bug.cgi?id=2056558 |
| PoC repo | https://github.com/defineid/Trespasser |
Under Fission, WebRender assigns each content process its own PipelineId namespace. The GPU-process bridge is expected to reject any resource whose namespace does not belong to the process that sent it, so that one content process cannot address or overwrite another's rendering resources.
WebRenderBridgeParent::AddPipelineIdForCompositable, which runs in the GPU
process, validated the incoming pipeline against the root/WebRender guard and
against this bridge's own set of registered pipelines, but it never called
MatchesNamespace on the PipelineId. The image-key, font-key, and blob-key
paths in the same file all perform that MatchesNamespace ownership check;
the PipelineId path simply did not. RemovePipelineIdForCompositable is
likewise unchecked, so the same namespace-ownership hole exists on removal.
Downstream, AsyncImagePipelineManager::AddAsyncImagePipeline guards the
uniqueness of a pipeline only with MOZ_ASSERT, which compiles to a no-op in
release builds. So when a forged, cross-namespace PipelineId reaches the shared
AsyncImagePipelineManager, the manager does not reject the duplicate: it
silently overwrites the victim origin's existing async image pipeline entry.
Chained together, a compromised content process A can forge a PipelineId whose namespace belongs to content process B (a different origin, in a separate process under Fission), submit it on the compositable-registration path, and have the GPU process accept it and clobber B's image pipeline in the shared manager.
An already-compromised content process under Fission. The attacker is assumed to already have code execution in one content process (for example through a separate content-process bug) and to be able to send WebRender IPC messages with attacker-chosen fields. This defect is the missing authorization that lets that process reach across the site-isolation boundary into another origin's rendering resources.
This is a Fission site-isolation authorization bypass, not memory corruption. The impact is cross-origin rendering confusion and cross-tab denial of service. Downstream consumers are null-guarded and reference-counted, so it does not yield code execution. The PoC must not and does not claim code execution.
README.md — this file.forge-namespace.patch — a research patch for a local Firefox tree. On
the content side, guarded by XRE_IsContentProcess() and the environment
variable MOZ_POC_FORGE_WR_NAMESPACE, it forges the outgoing PipelineId's
namespace. On the GPU-process parent side it prints an ACCEPTED diagnostic
when a cross-namespace PipelineId is registered, demonstrating that the
missing check let it through.poc.html — a bare content-side test case: a WebGL canvas (so an async image
pipeline is created) plus a cross-origin out-of-process iframe (so Fission
places it in a separate content process). On a stock build it renders normally
and does nothing observable.victim.html — the synthetic second origin the iframe loads. It is a trivial,
script-free page whose only job is to be a live cross-origin document, so that
under Fission it lands in its own content process with its own PipelineId
namespace (the namespace a forged registration would target).poc-render.png — a screenshot of poc.html loaded in the local stock
Firefox 140.11.0esr. It shows the test page rendering normally: the WebGL
canvas draws its frame and the out-of-process iframe loads victim.html. It
shows only benign rendering. The bypass itself requires the research patch and
a vulnerable build and was not triggered here (see Verification status).These steps are meant to be performed by the author against a local Firefox checkout. Nothing here was executed against Firefox in this environment (see Verification status below).
Check out a vulnerable Firefox source tree (before 154 / before ESR
153.1) and confirm Fission is enabled (fission.autostart = true, the
default on recent builds).
Apply the research patch from the top of the source tree:
patch -p1 < forge-namespace.patch
The patch touches gfx/layers/wr/WebRenderBridgeChild.cpp (content side,
forges the namespace) and gfx/layers/wr/WebRenderBridgeParent.cpp
(GPU-process side, prints the ACCEPTED diagnostic). Both changes are inert
unless the guard conditions are met.
Build normally (./mach build).
Serve this directory so that poc.html's out-of-process iframe loads the
companion victim.html from a genuinely different registrable domain, so
Fission puts the iframe in its own content process. For example,
python3 -m http.server 8000 --bind :: and load the iframe from
http://victim.localtest.me:8000/victim.html. See the inline comment in
poc.html.
Launch the build with the forge variable set and load the test page:
MOZ_POC_FORGE_WR_NAMESPACE=999999 ./mach run --temp-profile poc.html
999999 is an arbitrary namespace value chosen so it belongs to no real
process; it makes the forged, cross-namespace registration obvious in the log.
On a vulnerable build with the patch applied, the GPU-process parent accepts and registers a PipelineId whose namespace belongs to no such process, and prints (this is the exact shape shown in the writeup):
[POC-2056558] ACCEPTED cross-namespace PipelineId:
sender-namespace=4 pipeline-namespace=999999 (should have been rejected)
sender-namespace is the namespace legitimately owned by the sending content
process; pipeline-namespace is the forged value. The two differing values,
plus the fact that registration was accepted, is the whole point: the ownership
check that would have rejected it is missing.
On a fixed build (154 / ESR 153.1 and later), the same message is rejected
with an IPC_FAIL and no such ACCEPTED line is printed, because the PipelineId
path now enforces MatchesNamespace.
Enforce, on the PipelineId path, the same namespace-ownership check the resource-key paths already use:
if (!MatchesNamespace(aPipelineId)) {
return IPC_FAIL(this, "PipelineId namespace does not belong to the sending process");
}
Applied in AddPipelineIdForCompositable (and symmetrically on
RemovePipelineIdForCompositable), this rejects any PipelineId whose namespace
does not belong to the sending process, closing the bypass.
poc.html was then loaded once in the local stock Firefox 140.11.0esr and
captured as poc-render.png; that screenshot shows only the test page
rendering normally (the WebGL canvas draws a frame, the out-of-process iframe
loads victim.html). Because the local Firefox is a stock, non-patched build,
the screenshot contains no forged registration and no [POC-2056558]
log line. To capture the accepted-cross-namespace output, apply the patch to a
local vulnerable build and run it yourself as described above.This scaffold contains no exploitation primitive beyond demonstrating that the missing authorization check lets a cross-namespace PipelineId through. It makes no attempt at code execution and produces no memory corruption.
This targets a locally built Firefox tree with synthetic, self-authored content, for a writeup of an already-reported, already-patched CVE. The research patch is guarded so it only affects a local developer build and encodes the compromised-content-process assumption the writeup describes; it is not a remote exploit. Nothing here attacks, contacts, probes, or references attacking any live or third party service. The out-of-process iframe in the test case is inherent to reproducing the Fission process split and is documented as such; the top page itself has no other network dependency.