CVE-2026-16351
Overview
Background
- `BrowsingContext`
- Gecko’s cross-process representation of a navigable browsing context (a window, tab, or frame) whose state is mirrored between the parent process and content processes.
- Synced field
- a
BrowsingContextmember (identified by aFieldIndex<>such asIDX_BrowserId) whose value is replicated across processes via IPC and gated byCanSet/DidSethooks. - `CanSet`
- the parent-process validation hook that decides whether a proposed change to a synced field, optionally originating from a content process (
ContentParent* aSource), is permitted. - `BrowserId`
- the identifier associating a top-level
BrowsingContextwith its owning browser element, used to correlate navigation state across processes.
Root Cause Analysis
The IDX_BrowserId synced field is stored as a 64-bit value, but the BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, ...) validation overload declared its aValue parameter as const uint32_t& instead of const uint64_t&. Because the parameter type did not match the field’s real width, the value flowing through the field-sync machinery was reinterpreted or truncated to 32 bits at the validation boundary, so CanSet did not reason about the same value the field actually carried. This breaks the invariant that a field’s validator inspects the exact value being committed, allowing a mismatch between what the parent validates and what is stored/propagated for the BrowserId. Given the metadata (a use-after-free in DOM navigation leading to sandbox escape), a BrowserId that is validated inconsistently or collides after truncation can misassociate a BrowsingContext, leaving navigation code holding a reference tied to a stale or wrong browser.
The fix changes both the declaration in BrowsingContext.h and the definition in BrowsingContext.cpp to take const uint64_t& aValue, so validation operates on the full-width identifier. With matched types there is no truncation and the parent enforces its “only settable on a top-level context that has no ID yet” check against the true value.
IDX_BrowserId) validated through a CanSet overload typed as uint32_t, so the parent process gated a truncated value rather than the real one. Widening the parameter to uint64_t realigns the validator with the field, restoring correct enforcement over the identifier that binds a BrowsingContext to its browser.Attack Path
- Reach the field sync
A compromised or malicious content process issues an IPC request to set the
BrowserIdsynced field on aBrowsingContext. - Cross the type boundary
The proposed 64-bit value is funneled through the mistyped
CanSet(FieldIndex<IDX_BrowserId>, const uint32_t&, ...)overload, where it is narrowed to 32 bits at validation. - Defeat consistent validation
Because the parent validates a truncated value, its
BrowserIdgating no longer matches the value that governsBrowsingContextassociation. - Misassociate navigation state
Inconsistent
BrowserIdhandling lets aBrowsingContextbe tied to the wrong or a stale browser during navigation. - Trigger the use-after-free Navigation code follows the misassociated linkage to a freed object, yielding the use-after-free described in the metadata.
Impact Assessment
BrowserId synced field, so the consequence crosses the content/parent trust boundary. The precondition is code execution or scripting control within a content process capable of proposing BrowsingContext field changes over IPC.Files Changed
docshell/base/BrowsingContext.cppdocshell/base/BrowsingContext.h
Audit Directions
- Synced-field type parityAudit every
CanSetandDidSetoverload against its field’s declared storage type to ensure nouint32_t/uint64_t(or other width) mismatch narrows values at the validation boundary. - Identifier truncationReview parent-process validators that gate cross-process identifiers (
BrowserId, context IDs, principal keys) for any implicit narrowing that could cause collisions or inconsistent enforcement. - IPC validation-vs-commit consistencyConfirm that the value a parent-process hook validates is byte-for-byte the value subsequently stored and propagated, so a content process cannot make the checked value differ from the committed one.
Patch
diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp
index 4a8af95b0c1..0fcb739a037 100644
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -4234,7 +4234,7 @@ bool BrowsingContext::CanSet(
return XRE_IsParentProcess() && !aSource && IsTop();
}
-bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
+bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint64_t& aValue,
ContentParent* aSource) {
// We should only be able to set this for toplevel contexts which don't have
// an ID yet.
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
index e30efc99abb..7c50af51999 100644
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -1485,7 +1485,7 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
void DidSet(FieldIndex<IDX_HasSessionHistory>, bool aOldValue);
- bool CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue,
+ bool CanSet(FieldIndex<IDX_BrowserId>, const uint64_t& aValue,
ContentParent* aSource);
bool CanSet(FieldIndex<IDX_UseErrorPages>, const bool& aUseErrorPages,