Critical watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Accessibility
DescriptionUse after free in Accessibility
ComponentAccessibility
Sub-componentA11y
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker503419515
Fix commit7bf572e7a2ed (chromium/src) +55/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-16 (per vendor advisory)
Fix landed2026-04-17 (public git)
Disclosed2026-04-28
Patch gap11 days — fix public in source before the advisory

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()) of AXPlatformNodeWin* 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 AXPlatformNodeWin that 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.

Key insight
The single mistake was inserting a raw 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

  1. 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.
  2. Fire the alert event Dispatching the alert runs the STA message pump, allowing assistive-technology/client code to run reentrantly during the notification.
  3. Destroy the node mid-dispatch That reentrant work tears down the AXPlatformNodeWin, putting it in the IsDestroyed() state before control returns to AddAlertTarget(). (inferred)
  4. Insert the dangling pointer Pre-patch, AddAlertTarget() then inserts the freed node’s this into the global GetAlertTargets() set, and a later access of that set dereferences the stale pointer — the use-after-free. (inferred)

Impact Assessment

The advisory rates this critical, a use-after-free in 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

FunctionChangeNotes
TEST_F
ui/accessibility/platform/ax_platform_node_win_unittest.cc
modified

Files Changed

  • ui/accessibility/platform/ax_platform_node_win.cc
  • ui/accessibility/platform/ax_platform_node_win.h
  • ui/accessibility/platform/ax_platform_node_win_unittest.cc

Audit Directions

  • Reentrant event dispatch then self-mutation
    Audit 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 an IsDestroyed()/WeakPtr check.
  • Raw pointers in global sets
    Review all insert sites into GetAlertTargets() and similar process-global collections of raw AXPlatformNode*, ensuring removal and liveness invariants hold across node destruction.
  • Post-notification code paths
    Check IAccessible/UIA notification handlers for member access or container mutation occurring after the notification call returns, where the owning node may already be destroyed.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • ui/accessibility/platform/ax_platform_node_base.h
  • ui/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

  1. 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.
  2. 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.
  3. Premature Destruction: Processing the IPC destroys the views::View, which in turn destroys its ViewAXPlatformNodeDelegate. The delegate owns the AXPlatformNode via a unique_ptr, which calls AXPlatformNodeWin::Destroy() (line 685).
  4. Missed Removal: The first action in Destroy() is RemoveAlertTarget(). However, because execution is still paused inside NotifyAccessibilityEvent and hasn’t reached the addition step, the node is not in the set, and the removal is a no-op.
  5. Adding the Dangling Pointer: Once the synchronous OS call returns, NotifyAccessibilityEvent resumes. At line 795, it calls AddAlertTarget(), which blindly inserts the now-freed (or soon-to-be-freed) this pointer into the process-global absl::flat_hash_set<AXPlatformNodeWin*> returned by GetAlertTargets().
  6. Permanent Dangling Pointer: Crucially, the destructor ~AXPlatformNodeWin() (lines 399-405) does not call RemoveAlertTarget(). The pointer remains in the set permanently.
  7. 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 over GetAlertTargets() (line 2147) and calls IsDescendant(target), which invokes the pure virtual method target->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.)

  1. An attacker compromises a renderer process.
  2. The attacker triggers a UI element in the browser process that fires a kAlert event (e.g., a Geolocation permission prompt).
  3. Immediately after, the attacker sends an IPC to close the prompt.
  4. The kAlert event triggers the synchronous ::UiaRaiseAutomationEvent call, which pumps the message loop, processes the close IPC, and destroys the node.
  5. The node’s raw pointer is leaked into the global GetAlertTargets set.
  6. The attacker grooms the browser process heap to control the vtable of the reclaimed object.
  7. 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:

  1. Update AddAlertTarget() or NotifyAccessibilityEvent to check if the node has been destroyed before adding it to the set (e.g., if (event_type == ax::mojom::Event::kAlert && !IsDestroyed())).
  2. Modify the destructor AXPlatformNodeWin::~AXPlatformNodeWin() to ensure RemoveAlertTarget() 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.

View on issue tracker