Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSite isolation issue in the Graphics component
ComponentGraphics
Bug ClassLogic Error
Tracker2056558
Fix commit076f33aea8c3 (firefox) +23/-0
CISA KEVNot listed
CreditedAbdulaziz Alasaiqah
Disclosed2026-08-18

Background

WebRender
Gecko’s GPU-accelerated compositor whose parent-side commands are processed in the trusted compositor process.
`wr::PipelineId`
an identifier for a WebRender rendering pipeline that carries an mNamespace field scoping it to a particular owner.
Namespace
a per-CompositorBridgeParent numeric tag (mNamespace) used to distinguish which child/content endpoint owns a given graphics resource.
Async image pipeline
a WebRender compositable pipeline whose image contents are updated asynchronously via parent commands like OpUpdateAsyncImagePipeline.

Root Cause Analysis

The vulnerable path was WebRenderBridgeParent::ProcessWebRenderParentCommands, which handled the TOpAddPipelineIdForCompositable, TOpRemovePipelineIdForCompositable, TOpUpdateAsyncImagePipeline, and TOpUpdatedAsyncImagePipeline cases using a wr::PipelineId supplied directly by the child endpoint. The invariant that a child may only operate on pipelines belonging to its own namespace was never checked, so the parent trusted an attacker-controllable op.pipelineId() without confirming ownership. This let a command reference a PipelineId scoped to a different CompositorBridgeParent namespace, i.e. a resource owned by another content context.

The fix introduces CompositorBridgeParentBase::OwnsPipelineId, which compares mNamespace against aPipelineId.mNamespace, and gates each of the four command handlers on it, breaking out (and asserting unreachable) when the check fails. Because processing now stops before AddPipelineIdForCompositable or the async-image update logic runs, a cross-namespace pipeline reference can no longer mutate another owner’s compositable state.

Key insight
The single core mistake was processing child-supplied PipelineId values in the compositor parent without verifying they belonged to the sending endpoint’s namespace; the fix adds an explicit OwnsPipelineId ownership check on every affected command path.

Attack Path

  1. Compromised child endpoint An attacker gains control of a content/child process that speaks the PCompositorBridge/WebRender IPC protocol.
  2. Forge a foreign PipelineId The attacker crafts a parent command (e.g. OpUpdateAsyncImagePipeline) whose pipelineId() carries an mNamespace belonging to a different CompositorBridgeParent.
  3. Bypass absent ownership check Before the fix, ProcessWebRenderParentCommands accepted the command without confirming the pipeline was owned by the sender.
  4. Cross-namespace manipulation The parent adds, removes, or updates async image compositable state for a pipeline owned by another context, violating site isolation between graphics resources.

Impact Assessment

An attacker controlling one child endpoint gains the ability to add, remove, or update async-image compositable pipelines that belong to a different namespace, executed in the trusted compositor/parent process. This is a site-isolation boundary violation letting one context tamper with another’s graphics pipeline state; the precondition is a cooperating or compromised child able to send WebRender parent commands. The advisory rates the impact as moderate/medium with no memory-safety primitive established in the diff.

Files Changed

  • gfx/layers/ipc/CompositorBridgeParent.cpp
  • gfx/layers/ipc/CompositorBridgeParent.h
  • gfx/layers/wr/WebRenderBridgeParent.cpp

Audit Directions

  • Unvalidated IPC identifiers
    Audit every parent-side command handler that consumes a child-supplied PipelineId, ExternalImageId, or handle for a matching Owns* ownership check before use.
  • Namespace-scoped resources
    Verify that any resource keyed by mNamespace is only mutated by the endpoint whose namespace matches, not merely asserted in debug builds.
  • Assert-only guards
    Confirm the new checks fail safe in release builds, since MOZ_ASSERT_UNREACHABLE is compiled out and only the break enforces the boundary in production.
diff --git a/gfx/layers/ipc/CompositorBridgeParent.cpp b/gfx/layers/ipc/CompositorBridgeParent.cpp
index 3c3b7234506..89b0220589c 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -157,6 +157,11 @@ bool CompositorBridgeParentBase::OwnsExternalImageId(
   return mNamespace == static_cast<uint32_t>(wr::AsUint64(aId) >> 32);
 }
 
+bool CompositorBridgeParentBase::OwnsPipelineId(
+    const wr::PipelineId& aPipelineId) const {
+  return mNamespace == aPipelineId.mNamespace;
+}
+
 CompositorBridgeParent::LayerTreeState::LayerTreeState()
     : mApzcTreeManagerParent(nullptr),
       mApzInputBridgeParent(nullptr),
diff --git a/gfx/layers/ipc/CompositorBridgeParent.h b/gfx/layers/ipc/CompositorBridgeParent.h
index 52f1f2e98c2..e4b5f7be74c 100644
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -155,6 +155,8 @@ class CompositorBridgeParentBase : public PCompositorBridgeParent,
 
   bool OwnsExternalImageId(const wr::ExternalImageId& aId) const;
 
+  bool OwnsPipelineId(const wr::PipelineId& aPipelineId) const;
+
   CompositorManagerParent* GetCompositorManager() const {
     return mCompositorManager;
   }
diff --git a/gfx/layers/wr/WebRenderBridgeParent.cpp b/gfx/layers/wr/WebRenderBridgeParent.cpp
index 8f0090febd3..2ec21fb9c3c 100644
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -1678,6 +1678,10 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
       case WebRenderParentCommand::TOpAddPipelineIdForCompositable: {
         const OpAddPipelineIdForCompositable& op =
             cmd.get_OpAddPipelineIdForCompositable();
+        if (!GetCompositorBridge()->OwnsPipelineId(op.pipelineId())) {
+          MOZ_ASSERT_UNREACHABLE("Invalid PipelineId!");
+          break;
+        }
 
         AddPipelineIdForCompositable(op.pipelineId(), op.handle(), op.owner(),
                                      aTxn, txnForImageBridge);
@@ -1686,6 +1690,10 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
       case WebRenderParentCommand::TOpRemovePipelineIdForCompositable: {
         const OpRemovePipelineIdForCompositable& op =
             cmd.get_OpRemovePipelineIdForCompositable();
+        if (!GetCompositorBridge()->OwnsPipelineId(op.pipelineId())) {
+          MOZ_ASSERT_UNREACHABLE("Invalid PipelineId!");
+          break;
+        }
 
         auto* pendingOps =
             mLateInit->mApi->GetPendingAsyncImagePipelineOps(aTxn);
@@ -1701,6 +1709,10 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
       case WebRenderParentCommand::TOpUpdateAsyncImagePipeline: {
         const OpUpdateAsyncImagePipeline& op =
             cmd.get_OpUpdateAsyncImagePipeline();
+        if (!GetCompositorBridge()->OwnsPipelineId(op.pipelineId())) {
+          MOZ_ASSERT_UNREACHABLE("Invalid PipelineId!");
+          break;
+        }
 
         auto* pendingOps =
             mLateInit->mApi->GetPendingAsyncImagePipelineOps(aTxn);
@@ -1719,6 +1731,10 @@ bool WebRenderBridgeParent::ProcessWebRenderParentCommands(
       case WebRenderParentCommand::TOpUpdatedAsyncImagePipeline: {
         const OpUpdatedAsyncImagePipeline& op =
             cmd.get_OpUpdatedAsyncImagePipeline();
+        if (!GetCompositorBridge()->OwnsPipelineId(op.pipelineId())) {
+          MOZ_ASSERT_UNREACHABLE("Invalid PipelineId!");
+          break;
+        }
 
         aTxn.InvalidateRenderedFrame(wr::RenderReasons::ASYNC_IMAGE);
 
Loading diff…