CVE-2026-74970
Overview
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
mNamespacefield scoping it to a particular owner. - Namespace
- a per-
CompositorBridgeParentnumeric 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.
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
- Compromised child endpoint
An attacker gains control of a content/child process that speaks the
PCompositorBridge/WebRender IPC protocol. - Forge a foreign PipelineId
The attacker crafts a parent command (e.g.
OpUpdateAsyncImagePipeline) whosepipelineId()carries anmNamespacebelonging to a differentCompositorBridgeParent. - Bypass absent ownership check
Before the fix,
ProcessWebRenderParentCommandsaccepted the command without confirming the pipeline was owned by the sender. - 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
Files Changed
gfx/layers/ipc/CompositorBridgeParent.cppgfx/layers/ipc/CompositorBridgeParent.hgfx/layers/wr/WebRenderBridgeParent.cpp
Audit Directions
- Unvalidated IPC identifiersAudit every parent-side command handler that consumes a child-supplied
PipelineId,ExternalImageId, or handle for a matchingOwns*ownership check before use. - Namespace-scoped resourcesVerify that any resource keyed by
mNamespaceis only mutated by the endpoint whose namespace matches, not merely asserted in debug builds. - Assert-only guardsConfirm the new checks fail safe in release builds, since
MOZ_ASSERT_UNREACHABLEis compiled out and only thebreakenforces the boundary in production.
Patch
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);