CVE-2026-12289
Overview
Background
- WebRender
- Gecko’s GPU-accelerated compositor that runs in the privileged GPU/compositor process and renders content submitted over IPC by child processes.
- `ExternalImageId`
- a 64-bit identifier for a shared graphics surface whose high 32 bits encode the owning compositor namespace and low 32 bits the resource handle.
- Namespace
- a per-
CompositorManagerParent32-bit tag (mNamespace) that scopes which parent-side bridge legitimately owns a given image or pipeline id. - `PipelineId`
- a WebRender identifier for an async image pipeline that likewise carries an owning namespace in its high 32 bits.
Root Cause Analysis
The parent-side IPC handlers ContentCompositorBridgeParent::AllocPTextureParent, ImageBridgeParent::AllocPTextureParent, WebRenderBridgeParent::AddSharedExternalImage, and the TOpAddPipelineIdForCompositable / TOpRemovePipelineIdForCompositable / TOpUpdateAsyncImagePipeline / TOpUpdatedAsyncImagePipeline command cases in WebRenderBridgeParent::ProcessWebRenderParentCommands accepted an ExternalImageId or PipelineId supplied by a child process and acted on it without confirming that the id’s embedded namespace matched the receiving bridge’s own namespace. The invariant violated is that a compositor bridge may only reference surfaces and pipelines belonging to its own namespace; the prior ImageBridgeParent code only rejected the sentinel namespace 0 rather than checking actual ownership, so a compromised content process could forge an id encoding another bridge’s namespace and reach cross-namespace resources.
The fix introduces an OwnsExternalImageId predicate (on CompositorBridgeParentBase, ImageBridgeParent, and WebRenderBridgeParent) that compares the id’s high 32 bits against the owner’s namespace/content id, and each handler now bails out (nullptr, return false, or success = false; continue;) when the check fails. This restores the ownership invariant by validating the namespace at every entry point where an externally-controlled id is consumed, closing the confusion that enabled privilege escalation.
ExternalImageId and PipelineId values instead of verifying it against the receiving parent’s own namespace. The fix adds explicit ownership checks (OwnsExternalImageId and direct >> 32 namespace comparisons) at each handler so a forged cross-namespace id is rejected before use.Attack Path
- Compromise a content process An attacker gains code execution in a sandboxed child process that speaks the compositor/image-bridge IPC protocol.
- Forge a cross-namespace id
The attacker crafts an
ExternalImageIdorPipelineIdwhose high 32 bits encode a namespace owned by a different compositor bridge rather than its own. - Submit it over IPC
The forged id is sent via
AllocPTextureParent,AddSharedExternalImage, or aPipelineIdcompositable command to the privileged compositor process. - Bypass the missing ownership check The parent, lacking a namespace validation, binds or manipulates a surface or async image pipeline belonging to another namespace.
- Achieve privilege escalation Cross-namespace resource confusion in the compositor process yields unauthorized influence over another context’s graphics state and escalation beyond the content sandbox.
Impact Assessment
ExternalImageId / PipelineId values over the WebRender/ImageBridge IPC channels.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/ipc/ImageBridgeParent.cpp |
modified |
Files Changed
gfx/layers/ipc/CompositorBridgeParent.cppgfx/layers/ipc/CompositorBridgeParent.hgfx/layers/ipc/CompositorManagerParent.hgfx/layers/ipc/ContentCompositorBridgeParent.cppgfx/layers/ipc/ImageBridgeParent.cppgfx/layers/ipc/ImageBridgeParent.hgfx/layers/wr/WebRenderBridgeParent.cppgfx/layers/wr/WebRenderBridgeParent.h
Audit Directions
- Namespace-encoded idsAudit every parent-side IPC handler that consumes an id carrying an embedded namespace (
ExternalImageId,PipelineId, font/image keys) and confirm it validates ownership before use. - Sentinel-only validationFlag checks that merely reject a sentinel value (e.g.
ns == 0) as a proxy for ownership; these do not prove the resource belongs to the caller and should be replaced with explicit owner comparisons. - Cross-process resource lookupsReview compositor/ImageBridge code paths that resolve child-supplied ids into surfaces or pipelines shared across processes, ensuring the lookup is scoped to the caller’s namespace or content id.
Patch
diff --git a/gfx/layers/ipc/CompositorBridgeParent.cpp b/gfx/layers/ipc/CompositorBridgeParent.cpp
index 2afe2cb373c..bdb031b8854 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -149,6 +149,13 @@ bool CompositorBridgeParentBase::DeallocShmem(ipc::Shmem& aShmem) {
return PCompositorBridgeParent::DeallocShmem(aShmem);
}
+bool CompositorBridgeParentBase::OwnsExternalImageId(
+ const wr::ExternalImageId& aId) const {
+ return mCompositorManager &&
+ (static_cast<uint32_t>(wr::AsUint64(aId) >> 32) ==
+ mCompositorManager->GetNamespace());
+}
+
CompositorBridgeParent::LayerTreeState::LayerTreeState()
: mApzcTreeManagerParent(nullptr),
mApzInputBridgeParent(nullptr),
diff --git a/gfx/layers/ipc/CompositorBridgeParent.h b/gfx/layers/ipc/CompositorBridgeParent.h
index 00f46b06f5b..0530c9e8eba 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -153,6 +153,8 @@ class CompositorBridgeParentBase : public PCompositorBridgeParent,
// from the Compositor thread.
virtual void EnsureWebRenderBridgeParentInitialized() = 0;
+ bool OwnsExternalImageId(const wr::ExternalImageId& aId) const;
+
protected:
virtual ~CompositorBridgeParentBase();
diff --git a/gfx/layers/ipc/CompositorManagerParent.h b/gfx/layers/ipc/CompositorManagerParent.h
index ea5f0198ff9..c1a1210088a 100644
--- a/gfx/layers/ipc/CompositorManagerParent.h
+++ b/gfx/layers/ipc/CompositorManagerParent.h
@@ -78,6 +78,8 @@ class CompositorManagerParent final : public PCompositorManagerParent {
return mNamespace == static_cast<uint32_t>(wr::AsUint64(aId) >> 32);
}
+ uint32_t GetNamespace() const { return mNamespace; }
+
private:
static StaticMonitor sMonitor;
static StaticRefPtr<CompositorManagerParent> sInstance
diff --git a/gfx/layers/ipc/ContentCompositorBridgeParent.cpp b/gfx/layers/ipc/ContentCompositorBridgeParent.cpp
index f806b1ccf93..bf23dea49cb 100644
--- a/gfx/layers/ipc/ContentCompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/ContentCompositorBridgeParent.cpp
@@ -423,6 +423,12 @@ PTextureParent* ContentCompositorBridgeParent::AllocPTextureParent(
const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
const LayersId& aId, const uint64_t& aSerial,
const wr::MaybeExternalImageId& aExternalImageId) {
+ if (aExternalImageId.isSome() &&
+ !OwnsExternalImageId(aExternalImageId.ref())) {
+ NS_ERROR("We do not own this external image id.");
+ return nullptr;
+ }
+
CompositorBridgeParent::LayerTreeState* state = nullptr;
StaticMonitorAutoLock lock(CompositorBridgeParent::sIndirectLayerTreesLock);
diff --git a/gfx/layers/ipc/ImageBridgeParent.cpp b/gfx/layers/ipc/ImageBridgeParent.cpp
index 1926260319b..2dcf9b6bc2b 100644
--- a/gfx/layers/ipc/ImageBridgeParent.cpp
+++ b/gfx/layers/ipc/ImageBridgeParent.cpp
@@ -317,8 +317,9 @@ PTextureParent* ImageBridgeParent::AllocPTextureParent(
const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
const uint64_t& aSerial, const wr::MaybeExternalImageId& aExternalImageId) {
if (aExternalImageId.isSome()) {
- uint32_t ns = static_cast<uint32_t>(wr::AsUint64(*aExternalImageId) >> 32);
- if (ns == 0) {
+ if (aExternalImageId.isSome() &&
+ !OwnsExternalImageId(aExternalImageId.ref())) {
+ NS_ERROR("We do not own this external image id.");
return nullptr;
}
}
@@ -408,6 +409,12 @@ already_AddRefed<ImageBridgeParent> ImageBridgeParent::GetInstance(
return bridge.forget();
}
+bool ImageBridgeParent::OwnsExternalImageId(
+ const wr::ExternalImageId& aId) const {
+ return (static_cast<uint32_t>(wr::AsUint64(aId) >> 32) ==
+ static_cast<uint32_t>(static_cast<uint64_t>(mContentId) >> 32));
+}
+
bool ImageBridgeParent::AllocShmem(size_t aSize, ipc::Shmem* aShmem) {
if (mClosed) {
return false;
diff --git a/gfx/layers/ipc/ImageBridgeParent.h b/gfx/layers/ipc/ImageBridgeParent.h
index 672a8efa0e3..f300f8edce6 100644
--- a/gfx/layers/ipc/ImageBridgeParent.h
+++ b/gfx/layers/ipc/ImageBridgeParent.h
@@ -126,6 +126,9 @@ class ImageBridgeParent final : public PImageBridgeParent,
static void ShutdownInternal();
void DeferredDestroy();
+
+ bool OwnsExternalImageId(const wr::ExternalImageId& aId) const;
+
nsCOMPtr<nsISerialEventTarget> mThread;
dom::ContentParentId mContentId;
diff --git a/gfx/layers/wr/WebRenderBridgeParent.cpp b/gfx/layers/wr/WebRenderBridgeParent.cpp
index ef4b5b085da..adcf06a7266 100644
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -884,6 +884,11 @@ bool WebRenderBridgeParent::AddSharedExternalImage(
return true;
}
+ if (!GetCompositorBridge()->OwnsExternalImageId(aExtId)) {
+ gfxCriticalNote << "We do not own extId:" << wr::AsUint64(aExtId);
+ return false;
+ }
+
auto key = wr::AsUint64(aKey);
auto it = mSharedSurfaceIds.find(key);
if (it != mSharedSurfaceIds.end()) {
@@ -1660,6 +1665,12 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
case WebRenderParentCommand::TOpAddPipelineIdForCompositable: {
const OpAddPipelineIdForCompositable& op =
cmd.get_OpAddPipelineIdForCompositable();
+ if (wr::AsUint64(op.pipelineId()) >> 32 !=
+ mLateInit->mIdNamespace.mHandle) {
+ success = false;
+ continue;
+ }
+
AddPipelineIdForCompositable(op.pipelineId(), op.handle(), op.owner(),
aTxn, txnForImageBridge);
break;
@@ -1667,6 +1678,12 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
case WebRenderParentCommand::TOpRemovePipelineIdForCompositable: {
const OpRemovePipelineIdForCompositable& op =
cmd.get_OpRemovePipelineIdForCompositable();
+ if (wr::AsUint64(op.pipelineId()) >> 32 !=
+ mLateInit->mIdNamespace.mHandle) {
+ success = false;
+ continue;
+ }
+
auto* pendingOps =
mLateInit->mApi->GetPendingAsyncImagePipelineOps(aTxn);
@@ -1681,6 +1698,11 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
case WebRenderParentCommand::TOpUpdateAsyncImagePipeline: {
const OpUpdateAsyncImagePipeline& op =
cmd.get_OpUpdateAsyncImagePipeline();
+ if (wr::AsUint64(op.pipelineId()) >> 32 !=
+ mLateInit->mIdNamespace.mHandle) {
+ success = false;
+ continue;
+ }
auto* pendingOps =
mLateInit->mApi->GetPendingAsyncImagePipelineOps(aTxn);
@@ -1699,6 +1721,12 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
case WebRenderParentCommand::TOpUpdatedAsyncImagePipeline: {
const OpUpdatedAsyncImagePipeline& op =
cmd.get_OpUpdatedAsyncImagePipeline();
+ if (wr::AsUint64(op.pipelineId()) >> 32 !=
+ mLateInit->mIdNamespace.mHandle) {
+ success = false;
+ continue;
+ }
+
aTxn.InvalidateRenderedFrame(wr::RenderReasons::ASYNC_IMAGE);
auto* pendingOps =
diff --git a/gfx/layers/wr/WebRenderBridgeParent.h b/gfx/layers/wr/WebRenderBridgeParent.h
index 698943773a0..babfe604ae9 100644
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -260,6 +260,11 @@ class WebRenderBridgeParent final : public PWebRenderBridgeParent,
return aFontKey.mNamespace == mLateInit->mIdNamespace;
}
+ bool OwnsExternalImageId(const wr::ExternalImageId& aId) const {
+ return static_cast<uint32_t>(wr::AsUint64(aId) >> 32) ==
+ mLateInit->mIdNamespace.mHandle;
+ }
+
void FlushRendering(wr::RenderReasons aReasons, bool aBlocking);
/**