CVE-2026-7344
Overview
Background
- `AXPlatformNodeWin`
- the Windows platform accessibility wrapper for an accessibility tree node, implementing MSAA/UIA/IAccessible2 interfaces.
- Alert targets set
- a process-global set (returned by
GetAlertTargets()) ofAXPlatformNodeWin*raw pointers tracking nodes that currently have outstanding alert events. - STA message pump reentrancy
- firing an accessibility event on Windows can synchronously dispatch to assistive-technology clients through the single-threaded-apartment message pump, which can run arbitrary code that destroys the node mid-notification.
- `IsDestroyed()`
- a predicate on
AXPlatformNodeWinthat reports whether the node’s backing delegate/data has already been torn down, i.e. the wrapper is a logically dead object.
Root Cause Analysis
AXPlatformNodeWin::AddAlertTarget() unconditionally inserted this into the process-global GetAlertTargets() set. When an alert event is fired, the reentrant STA message pump can synchronously destroy the very node that is being announced, so by the time AddAlertTarget() ran it could insert a pointer to an already-destroyed node, leaving a dangling raw pointer in a long-lived global set. Any later traversal of that set (to re-fire or clean up alert targets) would then dereference freed memory, a use-after-free.
The fix adds an if (IsDestroyed()) { return; } guard at the top of AddAlertTarget() so a node that has already been torn down is never inserted. The accompanying regression test forces the IsDestroyed() state via SetDelegateForTesting(nullptr) and asserts the global set size is unchanged after calling the add path, pinning the intended behavior.
this pointer into a persistent global set without checking whether the node had already been destroyed by reentrant event dispatch; the fix returns early when IsDestroyed() is true so no dangling pointer is ever stored.Attack Path
- Create an alert node
Page/content produces an accessibility node with
ax::mojom::Role::kAlert(or otherwise triggers an alert notification) while a Windows accessibility client is active. - Fire the alert event Dispatching the alert runs the STA message pump, allowing assistive-technology/client code to run reentrantly during the notification.
- Destroy the node mid-dispatch
That reentrant work tears down the
AXPlatformNodeWin, putting it in theIsDestroyed()state before control returns toAddAlertTarget(). (inferred) - Insert the dangling pointer
Pre-patch,
AddAlertTarget()then inserts the freed node’sthisinto the globalGetAlertTargets()set, and a later access of that set dereferences the stale pointer — the use-after-free. (inferred)
Impact Assessment
Accessibility on Windows; it requires an active platform accessibility client (screen reader / UIA or MSAA consumer) so that alert events are fired and the STA pump reentrancy path is exercised. An attacker who wins the destroy-during-dispatch race leaves a dangling AXPlatformNodeWin* in a process-global set whose later use could corrupt memory (inferred); the advisory does not report in-the-wild exploitation, and any escalation beyond the stale-pointer insertion is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fui/accessibility/platform/ax_platform_node_win_unittest.cc |
modified |
Files Changed
ui/accessibility/platform/ax_platform_node_win.ccui/accessibility/platform/ax_platform_node_win.hui/accessibility/platform/ax_platform_node_win_unittest.cc
Audit Directions
- Reentrant event dispatch then self-mutationAudit any accessibility method that registers
this(or other node pointers) in global/long-lived containers after an event fire that can pump the STA message loop; each needs anIsDestroyed()/WeakPtrcheck. - Raw pointers in global setsReview all insert sites into
GetAlertTargets()and similar process-global collections of rawAXPlatformNode*, ensuring removal and liveness invariants hold across node destruction. - Post-notification code pathsCheck IAccessible/UIA notification handlers for member access or container mutation occurring after the notification call returns, where the owning node may already be destroyed.
Patch
From 7bf572e7a2ed8c0d893e686b232f018d36bcd6cb Mon Sep 17 00:00:00 2001 From: Benjamin Beaudry <[email protected]> Date: Thu, 16 Apr 2026 17:11:41 -0700 Subject: [PATCH] [a11y] Guard against node destruction during alert event dispatch This CL fixes a potential use-after-free in AXPlatformNodeWin where a node destroyed during the reentrant message pump inside an alert event notification would be inserted into the process-global alert targets set as a dangling pointer. See bug for details. We fix it by preventing AddAlertTarget() from inserting a node that is already destroyed, and add a regression test that reproduces the post-reentrancy state and asserts the global set is unchanged. Fixed: 503419515 Change-Id: I6721a22bd0ba67b0d6d4c8ef84d7cabd5447d6d9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7769271 Auto-Submit: Benjamin Beaudry <[email protected]> Commit-Queue: Benjamin Beaudry <[email protected]> Reviewed-by: Kevin Babbitt <[email protected]> Cr-Commit-Position: refs/heads/main@{#1616207} --- diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc index a5354a2..5832649 100644 --- a/ui/accessibility/platform/ax_platform_node_win.cc +++ b/ui/accessibility/platform/ax_platform_node_win.cc @@ -8315,9 +8315,23 @@ } void AXPlatformNodeWin::AddAlertTarget() { + // Firing an alert event can reentrantly destroy this node via the STA + // message pump; don't insert a dangling pointer. crbug.com/503419515. + if (IsDestroyed()) { + return; + } GetAlertTargets().insert(this); } +// static +size_t AXPlatformNodeWin::GetAlertTargetCountForTesting() { + return GetAlertTargets().size(); +} + +void AXPlatformNodeWin::AddAlertTargetForTesting() { + AddAlertTarget(); +} + void AXPlatformNodeWin::RemoveAlertTarget() { if (GetAlertTargets().find(this) != GetAlertTargets().end()) { GetAlertTargets().erase(this); diff --git a/ui/accessibility/platform/ax_platform_node_win.h b/ui/accessibility/platform/ax_platform_node_win.h index 7413fc3..1101800 100644 --- a/ui/accessibility/platform/ax_platform_node_win.h +++ b/ui/accessibility/platform/ax_platform_node_win.h @@ -1231,6 +1231,12 @@ // see above. static Counts ResetCountsForTesting(); + // Returns the size of the process-global alert targets set. + static size_t GetAlertTargetCountForTesting(); + + // Test-only wrapper around AddAlertTarget(). + void AddAlertTargetForTesting(); + bool IsUIAControl() const; protected: diff --git a/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/ui/accessibility/platform/ax_platform_node_win_unittest.cc index a34792d..9bb2610b 100644 --- a/ui/accessibility/platform/ax_platform_node_win_unittest.cc +++ b/ui/accessibility/platform/ax_platform_node_win_unittest.cc @@ -8291,4 +8291,39 @@ } } +// Regression test for crbug.com/503419515: a node destroyed mid-event must +// not be inserted into the global alert targets set. +TEST_F(AXPlatformNodeWinTest, DestroyedNodeNotAddedToAlertTargets) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {2}; + + AXNodeData alert; + alert.id = 2; + alert.role = ax::mojom::Role::kAlert; + + Init(root, alert); + AXNode* alert_ax_node = GetRoot()->children()[0]; + + auto* alert_node = static_cast<AXPlatformNodeWin*>( + AXPlatformNodeFromNode(alert_ax_node)); + ASSERT_TRUE(alert_node); + + const size_t initial_count = + AXPlatformNodeWin::GetAlertTargetCountForTesting(); + + // Put the node in the IsDestroyed() state without actually destroying it, + // so the wrapper can still tear down cleanly at the end of the test. + AXPlatformNodeDelegate* original_delegate = + alert_node->SetDelegateForTesting(nullptr); + ASSERT_TRUE(alert_node->IsDestroyed()); + + alert_node->AddAlertTargetForTesting(); + EXPECT_EQ(initial_count, + AXPlatformNodeWin::GetAlertTargetCountForTesting()); + + alert_node->SetDelegateForTesting(original_delegate); +} + } // namespace ui
Regression Test / PoC
diff --git a/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/ui/accessibility/platform/ax_platform_node_win_unittest.cc
index a34792d..9bb2610b 100644
--- a/ui/accessibility/platform/ax_platform_node_win_unittest.cc
+++ b/ui/accessibility/platform/ax_platform_node_win_unittest.cc
@@ -8291,4 +8291,39 @@
}
}
+// Regression test for crbug.com/503419515: a node destroyed mid-event must
+// not be inserted into the global alert targets set.
+TEST_F(AXPlatformNodeWinTest, DestroyedNodeNotAddedToAlertTargets) {
+ AXNodeData root;
+ root.id = 1;
+ root.role = ax::mojom::Role::kRootWebArea;
+ root.child_ids = {2};
+
+ AXNodeData alert;
+ alert.id = 2;
+ alert.role = ax::mojom::Role::kAlert;
+
+ Init(root, alert);
+ AXNode* alert_ax_node = GetRoot()->children()[0];
+
+ auto* alert_node = static_cast<AXPlatformNodeWin*>(
+ AXPlatformNodeFromNode(alert_ax_node));
+ ASSERT_TRUE(alert_node);
+
+ const size_t initial_count =
+ AXPlatformNodeWin::GetAlertTargetCountForTesting();
+
+ // Put the node in the IsDestroyed() state without actually destroying it,
+ // so the wrapper can still tear down cleanly at the end of the test.
+ AXPlatformNodeDelegate* original_delegate =
+ alert_node->SetDelegateForTesting(nullptr);
+ ASSERT_TRUE(alert_node->IsDestroyed());
+
+ alert_node->AddAlertTargetForTesting();
+ EXPECT_EQ(initial_count,
+ AXPlatformNodeWin::GetAlertTargetCountForTesting());
+
+ alert_node->SetDelegateForTesting(original_delegate);
+}
+
} // namespace ui
Original Bug Report
Potential Use-After-Free in AXPlatformNodeWin via reentrant accessibility event firing
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A potential Use-After-Free exists in the Windows accessibility implementation where a node can be destroyed during a synchronous COM call and subsequently added to a global set. This results in a permanently dangling pointer that is later dereferenced via virtual function calls. The issue can potentially be triggered by a compromised renderer to achieve a browser-process sandbox escape.
Affected files:
ui/accessibility/platform/ax_platform_node_win.ccui/accessibility/platform/ax_platform_node_base.hui/accessibility/platform/ax_platform_node.h
Estimated timestamp from git blame: 2026-02-24
Summary
A potential Use-After-Free (UAF) vulnerability exists in the browser process on Windows within the AXPlatformNodeWin accessibility implementation. The issue is caused by a deterministic race condition during the firing of alert events. A node can be destroyed during a synchronous COM/Win32 call before it is added to a global tracking set. This results in a permanently dangling pointer in a global container, which is later dereferenced via virtual function calls. Because the container stores raw pointers, MiraclePtr does not mitigate the issue.
Vulnerability Details
- Reentrant Event Notification: In
ui/accessibility/platform/ax_platform_node_win.cc,NotifyAccessibilityEvent(kAlert)fires events to the OS using::NotifyWinEvent(line 760) and::UiaRaiseAutomationEvent(line 789). As documented in Chromium, firing these events can cause UIA to call back into Chromium’s APIs, leading to synchronous reentrancy and pumping the thread’s Single-Threaded Apartment (STA) message loop. - The Race Condition: If a compromised renderer queues an IPC to close the UI element (e.g.,
window.close()) immediately after triggering an alert event, the IPC can be processed during the reentrant message pump inside::UiaRaiseAutomationEvent. - Premature Destruction: Processing the IPC destroys the
views::View, which in turn destroys itsViewAXPlatformNodeDelegate. The delegate owns theAXPlatformNodevia aunique_ptr, which callsAXPlatformNodeWin::Destroy()(line 685). - Missed Removal: The first action in
Destroy()isRemoveAlertTarget(). However, because execution is still paused insideNotifyAccessibilityEventand hasn’t reached the addition step, the node is not in the set, and the removal is a no-op. - Adding the Dangling Pointer: Once the synchronous OS call returns,
NotifyAccessibilityEventresumes. At line 795, it callsAddAlertTarget(), which blindly inserts the now-freed (or soon-to-be-freed)thispointer into the process-globalabsl::flat_hash_set<AXPlatformNodeWin*>returned byGetAlertTargets(). - Permanent Dangling Pointer: Crucially, the destructor
~AXPlatformNodeWin()(lines 399-405) does not callRemoveAlertTarget(). The pointer remains in the set permanently. - The Dereference: The dangling pointer is dereferenced when an assistive technology queries the “alerts” relation via
IAccessible2_2::get_relationTargetsOfType(L"alerts"). This method iterates overGetAlertTargets()(line 2147) and callsIsDescendant(target), which invokes the pure virtual methodtarget->IsDescendantOf(this). This results in a vtable fetch and an indirect call on freed memory.
Potential Exploitation Steps
(Note: These are suggested steps based on code analysis; our tooling has not executed a working proof-of-concept.)
- An attacker compromises a renderer process.
- The attacker triggers a UI element in the browser process that fires a
kAlertevent (e.g., a Geolocation permission prompt). - Immediately after, the attacker sends an IPC to close the prompt.
- The
kAlertevent triggers the synchronous::UiaRaiseAutomationEventcall, which pumps the message loop, processes the close IPC, and destroys the node. - The node’s raw pointer is leaked into the global
GetAlertTargetsset. - The attacker grooms the browser process heap to control the vtable of the reclaimed object.
- The attacker triggers a query for the “alerts” relation, causing a virtual call on the dangling pointer, leading to arbitrary code execution in the browser process (sandbox escape).
Suggested Fix
There are two main ways to fix this:
- Update
AddAlertTarget()orNotifyAccessibilityEventto check if the node has been destroyed before adding it to the set (e.g.,if (event_type == ax::mojom::Event::kAlert && !IsDestroyed())). - Modify the destructor
AXPlatformNodeWin::~AXPlatformNodeWin()to ensureRemoveAlertTarget()is called when the object memory is actually freed.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.