CVE-2026-16375
Overview
Background
- `HttpChannelParent`
- the parent-process IPC actor that services an HTTP channel opened by a content process over the
PHttpChannelprotocol. - `HttpBackgroundChannelParent`
- a companion parent-process actor on the
PBackgroundprotocol that delivers asynchronous channel events back to the same content process. - `BackgroundChannelRegistrar`
- the parent-process registry that pairs an
HttpChannelParentwith its matchingHttpBackgroundChannelParentusing a content-supplied channel Id as the key. - `ContentParentId`
- a
dom::ContentParentId(child ID) that uniquely identifies which content process a parent-side actor belongs to.
Root Cause Analysis
When a content process opened an HTTP channel, BackgroundChannelRegistrar::NotifyChannelLinked paired the HttpChannelParent with an HttpBackgroundChannelParent solely by matching a uint64_t channel Id, and that channel Id is chosen and supplied entirely by the content process. Because the registrar never checked that the two actors originated from the same content process, a compromised or malicious content process could supply a channel Id that collides with one belonging to a different content process, causing the registrar to cross-link the two actors and violating the invariant that a channel and its background channel must stay within one process boundary (site isolation).
The fix threads the owning process identity through both actors: HttpBackgroundChannelParent::Init now takes and stores a dom::ContentParentId captured via BackgroundParent::GetChildID, HttpChannelParent::GetContentParentId derives its owner from its IPC manager chain, and NotifyChannelLinked refuses to link the pair when GetContentParentId() differs between them. This restores the invariant by making process identity — which the parent derives itself and the content process cannot forge — a mandatory part of the linking decision rather than trusting only the attacker-controlled key.
ContentParentId from trusted parent-side sources and rejects any link whose two halves come from different processes.Attack Path
- Compromise a content process
An attacker gains code execution in one content process (for example via a renderer bug) so it can drive
PHttpChannelandPBackgroundIPC directly. - Predict or observe a victim channel Id
The attacker arranges for a channel Id that collides with the
uint64_tchannel Id of a channel belonging to a different content process. - Open a crafted background channel
The malicious process constructs an
HttpBackgroundChannelParentand callsInitwith the colliding channel Id, registering it inBackgroundChannelRegistrar. - Trigger cross-process linking
NotifyChannelLinkedmatches the two actors purely by channel Id and pairs the attacker’s background channel with the victim’sHttpChannelParent. - Receive another origin's channel events The attacker’s process now receives background channel events destined for the victim process, breaching the site-isolation boundary.
Impact Assessment
Files Changed
ipc/glue/BackgroundParentImpl.cppnetwerk/protocol/http/BackgroundChannelRegistrar.cppnetwerk/protocol/http/HttpBackgroundChannelParent.cppnetwerk/protocol/http/HttpBackgroundChannelParent.hnetwerk/protocol/http/HttpChannelParent.cppnetwerk/protocol/http/HttpChannelParent.h
Audit Directions
- Content-supplied keys as identityAudit any parent-process registry or map keyed on values (IDs, handles, tokens) chosen by a content process and confirm each lookup also verifies the requesting process’s identity.
- Cross-actor linking across IPC protocolsWherever two actors from different IPC protocols (
PHttpChannelandPBackgroundhere) are paired, verify both halves are bound to and checked against the sameContentParentIdbefore linking. - Trusted vs. untrusted origin derivationPrefer deriving process identity from parent-side facilities like
BackgroundParent::GetChildIDor the actor manager chain rather than trusting fields sent in the IPC payload.
Patch
diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp
index 292e261ee48..a401403b92c 100644
--- a/ipc/glue/BackgroundParentImpl.cpp
+++ b/ipc/glue/BackgroundParentImpl.cpp
@@ -1073,7 +1073,12 @@ BackgroundParentImpl::RecvPHttpBackgroundChannelConstructor(
net::HttpBackgroundChannelParent* aParent =
static_cast<net::HttpBackgroundChannelParent*>(aActor);
- if (NS_WARN_IF(NS_FAILED(aParent->Init(aChannelId)))) {
+ // Record the content process that owns this PBackground actor, so the
+ // background channel can only ever be paired with a HttpChannelParent
+ // from the same process.
+ dom::ContentParentId cpId(BackgroundParent::GetChildID(this));
+
+ if (NS_WARN_IF(NS_FAILED(aParent->Init(cpId, aChannelId)))) {
return IPC_FAIL_NO_REASON(this);
}
diff --git a/netwerk/protocol/http/BackgroundChannelRegistrar.cpp b/netwerk/protocol/http/BackgroundChannelRegistrar.cpp
index 960ce95715a..b397c9235de 100644
--- a/netwerk/protocol/http/BackgroundChannelRegistrar.cpp
+++ b/netwerk/protocol/http/BackgroundChannelRegistrar.cpp
@@ -48,6 +48,13 @@ void BackgroundChannelRegistrar::NotifyChannelLinked(
MOZ_ASSERT(aChannelParent);
MOZ_ASSERT(aBgParent);
+ // Only link the two actors when they originate from the same content
+ // process, since the channel Id used as the key is supplied by
+ // the content process.
+ if (aChannelParent->GetContentParentId() != aBgParent->GetContentParentId()) {
+ return;
+ }
+
aBgParent->LinkToChannel(aChannelParent);
aChannelParent->OnBackgroundParentReady(aBgParent);
}
diff --git a/netwerk/protocol/http/HttpBackgroundChannelParent.cpp b/netwerk/protocol/http/HttpBackgroundChannelParent.cpp
index 22c60ffedc0..5b198b8380d 100644
--- a/netwerk/protocol/http/HttpBackgroundChannelParent.cpp
+++ b/netwerk/protocol/http/HttpBackgroundChannelParent.cpp
@@ -79,12 +79,15 @@ HttpBackgroundChannelParent::~HttpBackgroundChannelParent() {
MOZ_ASSERT(!mIPCOpened);
}
-nsresult HttpBackgroundChannelParent::Init(const uint64_t& aChannelId) {
+nsresult HttpBackgroundChannelParent::Init(const dom::ContentParentId& aCpId,
+ const uint64_t& aChannelId) {
LOG(("HttpBackgroundChannelParent::Init [this=%p channelId=%" PRIu64 "]\n",
this, aChannelId));
AssertIsInMainProcess();
AssertIsOnBackgroundThread();
+ mContentParentId = aCpId;
+
RefPtr<ContinueAsyncOpenRunnable> runnable =
new ContinueAsyncOpenRunnable(this, aChannelId);
diff --git a/netwerk/protocol/http/HttpBackgroundChannelParent.h b/netwerk/protocol/http/HttpBackgroundChannelParent.h
index db89d0f240e..31aa564d18b 100644
--- a/netwerk/protocol/http/HttpBackgroundChannelParent.h
+++ b/netwerk/protocol/http/HttpBackgroundChannelParent.h
@@ -8,6 +8,7 @@
#include "mozilla/net/PHttpBackgroundChannelParent.h"
#include "mozilla/Atomics.h"
#include "mozilla/Mutex.h"
+#include "mozilla/dom/ipc/IdType.h"
#include "nsID.h"
#include "nsISupportsImpl.h"
@@ -24,9 +25,14 @@ class HttpBackgroundChannelParent final : public PHttpBackgroundChannelParent {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HttpBackgroundChannelParent, final)
- // Try to find associated HttpChannelParent with the same
- // channel Id.
- nsresult Init(const uint64_t& aChannelId);
+ // Try to find associated HttpChannelParent with the same content process
+ // and channel Id.
+ nsresult Init(const dom::ContentParentId& aCpId, const uint64_t& aChannelId);
+
+ // The content process that opened this background channel. Used by
+ // BackgroundChannelRegistrar to ensure the channel is only linked to a
+ // HttpChannelParent belonging to the same process.
+ dom::ContentParentId GetContentParentId() const { return mContentParentId; }
// Callbacks for BackgroundChannelRegistrar to notify
// the associated HttpChannelParent is found.
@@ -112,6 +118,10 @@ class HttpBackgroundChannelParent final : public PHttpBackgroundChannelParent {
nsCOMPtr<nsISerialEventTarget> mBackgroundThread
MOZ_GUARDED_BY(mBgThreadMutex);
+ // The content process that opened this background channel, set once in Init
+ // before the actor is registered.
+ dom::ContentParentId mContentParentId;
+
// associated HttpChannelParent for generating the channel events
RefPtr<HttpChannelParent> mChannelParent;
};
diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp
index bd284f6345f..f2e8d562495 100644
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -212,6 +212,10 @@ void HttpChannelParent::TryInvokeAsyncOpen(nsresult aRv) {
InvokeAsyncOpen(aRv);
}
+dom::ContentParentId HttpChannelParent::GetContentParentId() const {
+ return static_cast<ContentParent*>(Manager()->Manager())->ChildID();
+}
+
void HttpChannelParent::OnBackgroundParentReady(
HttpBackgroundChannelParent* aBgParent) {
LOG(("HttpChannelParent::OnBackgroundParentReady [this=%p bgParent=%p]\n",
diff --git a/netwerk/protocol/http/HttpChannelParent.h b/netwerk/protocol/http/HttpChannelParent.h
index 9f48ab236e2..3fd64fd0c8b 100644
--- a/netwerk/protocol/http/HttpChannelParent.h
+++ b/netwerk/protocol/http/HttpChannelParent.h
@@ -102,6 +102,11 @@ class HttpChannelParent final : public nsIInterfaceRequestor,
const nsACString& aPromptAction,
const nsACString& aTopLevelSite);
+ // The content process this channel parent belongs to. Used by
+ // BackgroundChannelRegistrar to ensure a background channel is only linked
+ // to a channel from the same process.
+ dom::ContentParentId GetContentParentId() const;
+
// Callback while background channel is ready.
void OnBackgroundParentReady(HttpBackgroundChannelParent* aBgParent);
// Callback while background channel is destroyed.