High firefox UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape due to use-after-free in the Disability Access APIs component
ComponentCore
Bug ClassUAF
Tracker2052562
Fix commitbfe514f21d25 (firefox) +22/-11
CISA KEVNot listed
CreditedOskar L
Disclosed2026-07-21

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 carrying aComplete.
`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.

Key insight
The single core mistake was trusting a child process’s split show sequence without confirming the deferred subtree’s parent was still attached to the tree, allowing a stale/freed 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

  1. Compromise a content process The attacker gains code execution in a sandboxed content process and speaks the accessibility IPC protocol directly to DocAccessibleParent.
  2. 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.
  3. Detach the target parent Through interleaved show/hide/reparent messages, the attacker causes that parent RemoteAccessible to be removed from the tree and freed while mPendingShowChild still references the subtree.
  4. Complete the show The final (aComplete) message triggers AttachChild, which dereferences the now-freed parent proxy, yielding a use-after-free in the parent process.

Impact Assessment

An attacker who already controls a content process can trigger a use-after-free inside the privileged parent (chrome) process via crafted accessibility IPC, turning it into a sandbox escape. Preconditions are code execution in a content process and the accessibility engine being active for the target document. The corrupted state lives in the higher-privileged process, so successful exploitation escalates beyond the content sandbox.

Changed Functions

FunctionChangeNotes
if
accessible/ipc/DocAccessibleParent.cpp
modified

Files Changed

  • accessible/ipc/DocAccessibleParent.cpp

Audit Directions

  • Deferred IPC state
    Audit 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 mutation
    Review show/hide/reparent handlers in DocAccessibleParent and siblings for paths that let a content process detach or free a node that another in-flight operation still references.
  • Proxy reuse
    Look for places that reuse or resurrect RemoteAccessible (or similar proxy) objects and confirm invariants like child count and parent linkage are asserted before reuse.
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;
Loading diff…