High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionPrivilege escalation in the Graphics: WebRender component
ComponentGraphics
Bug ClassLogic Error
Tracker2023443
Fix commit0517b85e48a4 (firefox) +62/-2
CISA KEVNot listed
Creditedchoeseyeong
Disclosed2026-06-16

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-CompositorManagerParent 32-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.

Key insight
The single core mistake was trusting the namespace field embedded in child-supplied 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

  1. Compromise a content process An attacker gains code execution in a sandboxed child process that speaks the compositor/image-bridge IPC protocol.
  2. Forge a cross-namespace id The attacker crafts an ExternalImageId or PipelineId whose high 32 bits encode a namespace owned by a different compositor bridge rather than its own.
  3. Submit it over IPC The forged id is sent via AllocPTextureParent, AddSharedExternalImage, or a PipelineId compositable command to the privileged compositor process.
  4. Bypass the missing ownership check The parent, lacking a namespace validation, binds or manipulates a surface or async image pipeline belonging to another namespace.
  5. 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

An attacker who already controls a content process gains the ability to reference and manipulate graphics resources (external images and async image pipelines) that belong to a different namespace, executing in the context of the privileged GPU/compositor process. This constitutes a high-severity privilege escalation out of the content sandbox. The precondition is prior code execution in a child process able to send crafted ExternalImageId / PipelineId values over the WebRender/ImageBridge IPC channels.

Changed Functions

FunctionChangeNotes
if
gfx/layers/ipc/ImageBridgeParent.cpp
modified

Files Changed

  • gfx/layers/ipc/CompositorBridgeParent.cpp
  • gfx/layers/ipc/CompositorBridgeParent.h
  • gfx/layers/ipc/CompositorManagerParent.h
  • gfx/layers/ipc/ContentCompositorBridgeParent.cpp
  • gfx/layers/ipc/ImageBridgeParent.cpp
  • gfx/layers/ipc/ImageBridgeParent.h
  • gfx/layers/wr/WebRenderBridgeParent.cpp
  • gfx/layers/wr/WebRenderBridgeParent.h

Audit Directions

  • Namespace-encoded ids
    Audit 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 validation
    Flag 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 lookups
    Review 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.
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);
 
   /**
Loading diff…