CVE-2026-74939
Overview
Background
- `nsDocShellLoadState`
- The serializable descriptor of a pending navigation (URI, triggering principal, flags) passed over IPC between content and parent processes.
- `mRemoteTypeOverride`
- An optional field on the load state that forces a document to load into a specific content-process type, a decision that is meant to be made only by the trusted parent process.
- `mTriggeringRemoteType`
- Identifies which process initiated the load; the sentinel
NOT_REMOTE_TYPEmarks a load originating in the parent rather than a content process. - `DocumentLoadListener::Open`
- The parent-process entry point that begins a channel-based document load and enforces preconditions before honoring a remote-type override.
Root Cause Analysis
The IPC-deserialization constructor of nsDocShellLoadState validated cross-process javascript: loads but never checked whether an untrusted content process had populated mRemoteTypeOverride, an override that is supposed to be settable only by the parent. Because that field was accepted verbatim from the sending process, a compromised content process could hand the parent a load state that dictated the destination process type, violating the invariant that process-placement decisions are exclusively a parent privilege.
The fix restructures the guard so that whenever mTriggeringRemoteType != NOT_REMOTE_TYPE (the load came from a content process) and mRemoteTypeOverride.isSome(), the parent raises a FatalError and rejects the actor. DocumentLoadListener::Open is additionally hardened to refuse an override when the load has an effective triggering remote type or originates from session history, closing paths that could reintroduce the same trust confusion. Together the changes re-establish that only parent-originated loads may carry a remote-type override.
mRemoteTypeOverride field without verifying that the load originated in the parent; the fix rejects any content-triggered load state that carries an override, restoring the parent-only invariant on process placement.Attack Path
- Compromise a content process The attacker first gains code execution in a sandboxed content process, for example through a separate renderer bug.
- Forge a load state
The compromised process constructs an
nsDocShellLoadStatefor anabout:blanktop-level load and setsmRemoteTypeOverrideto a target process type of its choosing. - Send it to the parent
The forged load state is delivered over IPC, where the pre-patch constructor accepts the override because it only screened
javascript:schemes. - Coerce process placement
DocumentLoadListener::Openhonors the attacker-chosen override, directing the document into a more privileged process than the content process should be able to reach. - Escalate privileges Executing content in that higher-privileged process context yields capabilities beyond the original sandbox.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdocshell/base/nsDocShellLoadState.cpp |
modified |
Files Changed
docshell/base/nsDocShellLoadState.cppnetwerk/ipc/DocumentLoadListener.cpp
Audit Directions
- Content-supplied fields on IPC load stateAudit every field of
nsDocShellLoadStatedeserialized from a child process for a parent-only invariant and confirm each is validated before use. - Override and privilege-bearing optionsReview other override or process-selection knobs (remote type, principal, sandbox flags) to ensure they cannot be set by a
mTriggeringRemoteType != NOT_REMOTE_TYPEsender. - Session-history and about:blank shortcutsTrace load paths that special-case
about:blankor session-history restores, since these bypassed the primary guard and may harbor similar trust gaps.
Patch
diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp
index ea31776de66..12b11965ab4 100644
--- a/docshell/base/nsDocShellLoadState.cpp
+++ b/docshell/base/nsDocShellLoadState.cpp
@@ -162,10 +162,16 @@ nsDocShellLoadState::nsDocShellLoadState(
return;
}
- if (mURI->SchemeIs("javascript") &&
- mTriggeringRemoteType != NOT_REMOTE_TYPE) {
- aActor->FatalError("Illegal cross-process javascript: load attempt");
- return;
+ if (mTriggeringRemoteType != NOT_REMOTE_TYPE) {
+ if (mURI->SchemeIs("javascript")) {
+ aActor->FatalError("Illegal cross-process javascript: load attempt");
+ return;
+ }
+
+ if (mRemoteTypeOverride.isSome()) {
+ aActor->FatalError("RemoteTypeOverride can only be set by parent");
+ return;
+ }
}
if (!ValidatePrincipalCouldPotentiallyBeLoadedBy(
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
index 9005281a3ae..845b82753d7 100644
--- a/netwerk/ipc/DocumentLoadListener.cpp
+++ b/netwerk/ipc/DocumentLoadListener.cpp
@@ -775,7 +775,9 @@ auto DocumentLoadListener::Open(nsDocShellLoadState* aLoadState,
if (aLoadState->GetRemoteTypeOverride()) {
if (!mIsDocumentLoad || !NS_IsAboutBlank(aLoadState->URI()) ||
- !loadingContext->IsTopContent()) {
+ !loadingContext->IsTopContent() ||
+ aLoadState->GetEffectiveTriggeringRemoteType() != NOT_REMOTE_TYPE ||
+ aLoadState->LoadIsFromSessionHistory()) {
LOG(
("DocumentLoadListener::Open with invalid remoteTypeOverride "
"[this=%p]",