CVE-2026-7344
Overview
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
Patch
From 7bf572e7a2ed8c0d893e686b232f018d36bcd6cb Mon Sep 17 00:00:00 2001
From: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
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 <benjamin.beaudry@microsoft.com>
Commit-Queue: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
Reviewed-by: Kevin Babbitt <kbabbitt@microsoft.com>
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.