CVE-2026-16356
Overview
Background
- `DocAccessibleParent`
- the parent-process (chrome) endpoint of the accessibility IPC actor that mirrors a content document’s accessibility tree from a child process.
- `RemoteAccessible`
- a parent-process proxy object representing one accessible node whose real state lives in a content process.
- Split show event
- a subtree-insertion (
ShowEvent) notification that is too large for a single IPC message and is delivered across several messages, with only the final message carryingaComplete. - `mPendingShowChild`
- saved identity of the root of a split show subtree, held between messages so the deferred subtree can be attached when the final message arrives.
Root Cause Analysis
When a show event was split across messages, the original DocAccessibleParent::ProcessShowEvent saved mPendingShowChild/mPendingShowParent/mPendingShowIndex only after the per-node processing loop had already run, and it never verified that the subtree root’s parent (rootParent) was still attached to the tree. A compromised content process could therefore drive a sequence of split show and reparenting messages that left the pending show’s target parent detached from the document tree and subsequently freed, while AttachChild would later dereference that stale RemoteAccessible when the completing message arrived. The invariant violated is that any parent a subtree is attached to must still be reachable from the document root (a doc, or having a live RemoteParent()).
The fix captures the pending-show identity up front while processing the first (root) accessible and immediately rejects the message with IPC_FAIL if the root’s parent is already detached (!rootParent->IsDoc() && !rootParent->RemoteParent()). It further hardens AttachChild to refuse attaching to a detached parent and to refuse attaching the pending show’s own parent as a child, and adds a release assertion in CreateAcc that a reused proxy has no unexpected children, closing the window in which a freed proxy could be reused.
RemoteAccessible to be used. The fix validates parent attachment at the moment the pending show is recorded and again in AttachChild, failing the IPC channel rather than dereferencing a detached node.Attack Path
- Compromise a content process
The attacker gains code execution in a sandboxed content process and speaks the accessibility IPC protocol directly to
DocAccessibleParent. - Begin a split show The child sends the first message of a multi-part show event whose subtree root points at a chosen parent accessible.
- Detach the target parent
Through interleaved show/hide/reparent messages, the attacker causes that parent
RemoteAccessibleto be removed from the tree and freed whilemPendingShowChildstill references the subtree. - Complete the show
The final (
aComplete) message triggersAttachChild, which dereferences the now-freed parent proxy, yielding a use-after-free in the parent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifaccessible/ipc/DocAccessibleParent.cpp |
modified |
Files Changed
accessible/ipc/DocAccessibleParent.cpp
Audit Directions
- Deferred IPC stateAudit any actor that stashes node identities across multiple messages (like
mPendingShow*) and re-validate that referenced nodes are still attached and alive before using them in the completing message. - Cross-process tree mutationReview show/hide/reparent handlers in
DocAccessibleParentand siblings for paths that let a content process detach or free a node that another in-flight operation still references. - Proxy reuseLook for places that reuse or resurrect
RemoteAccessible(or similar proxy) objects and confirm invariants like child count and parent linkage are asserted before reuse.
Patch
diff --git a/accessible/ipc/DocAccessibleParent.cpp b/accessible/ipc/DocAccessibleParent.cpp
index 41fba965cca..c2958285852 100644
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -188,6 +188,16 @@ mozilla::ipc::IPCResult DocAccessibleParent::ProcessShowEvent(
// This is the first Accessible, which is the root of the shown subtree.
root = child;
rootParent = parent;
+ if (!aComplete) {
+ // This is the first message for a show event split across multiple
+ // messages. Save the show target for subsequent messages and return.
+ mPendingShowChild = accData.ID();
+ mPendingShowParent = accData.ParentID();
+ mPendingShowIndex = accData.IndexInParent();
+ if (!rootParent->IsDoc() && !rootParent->RemoteParent()) {
+ return IPC_FAIL(this, "Attempt to split show with detached root");
+ }
+ }
}
// If this show event has been split across multiple messages and this is
// not the last message, don't attach the shown root to the tree yet.
@@ -202,21 +212,11 @@ mozilla::ipc::IPCResult DocAccessibleParent::ProcessShowEvent(
MOZ_ASSERT(CheckDocTree());
- if (!aComplete && !mPendingShowChild) {
- // This is the first message for a show event split across multiple
- // messages. Save the show target for subsequent messages and return.
- const auto& accData = aNewTree[0];
- mPendingShowChild = accData.ID();
- mPendingShowParent = accData.ParentID();
- mPendingShowIndex = accData.IndexInParent();
- return IPC_OK();
- }
if (!aComplete) {
// This show event has been split into multiple messages, but this is
- // neither the first nor the last message. There's nothing more to do here.
+ // not the last message. There's nothing more to do here.
return IPC_OK();
}
- MOZ_ASSERT(aComplete);
if (mPendingShowChild) {
// This is the last message for a show event split across multiple
// messages. Retrieve the saved show target, attach it to the tree and fire
@@ -288,6 +288,8 @@ RemoteAccessible* DocAccessibleParent::CreateAcc(
"Attempt to move RemoteAccessible which has a pending parent");
return nullptr;
}
+ MOZ_RELEASE_ASSERT(newProxy->ChildCount() == 0 || newProxy->IsOuterDoc(),
+ "Reused RemoteAccessible unexpectedly has children!");
return newProxy;
}
@@ -341,6 +343,15 @@ bool DocAccessibleParent::AttachChild(RemoteAccessible* aParent,
return false;
}
+ if (!aParent->IsDoc() && !aParent->RemoteParent() &&
+ aParent->ID() != mPendingShowChild) {
+ MOZ_ASSERT_UNREACHABLE("Attempt to attach child to a detached parent!");
+ return false;
+ }
+
+ MOZ_RELEASE_ASSERT(!mPendingShowChild || aChild->ID() != mPendingShowParent,
+ "Attempt to attach the pending show's parent as a child!");
+
if (aParent == aChild) {
MOZ_ASSERT_UNREACHABLE("Attempt to make an accessible its own child!");
return false;