CVE-2026-8533
Overview
Background
- `BrowserAccessibilityManager`
- the browser-process object that holds the cached accessibility tree for a frame and applies incoming accessibility updates/events from the renderer.
- `OnAccessibilityEvents`
- the method that processes a batch of accessibility events/tree mutations; re-entering it while a prior invocation is still on the stack can mutate or free tree nodes the outer call still holds.
- `in_on_accessibility_events_`
- a boolean re-entrancy sentinel, set via
base::AutoReset<bool>for the duration of event handling; before the patch it existed only underDCHECK_IS_ON(). - `DCHECK_IS_ON()` vs `BUILDFLAG(IS_WIN)`
DCHECK_IS_ON()is true only in debug/dcheck builds;BUILDFLAG(IS_WIN)is true in all Windows builds including release/stable.
Root Cause Analysis
Processing a batch of accessibility events in OnAccessibilityEvents can, on Windows, synchronously trigger platform accessibility callbacks that re-enter the manager and run OnAccessibilityEvents again while the outer call is still iterating; the outer frame then continues to use tree nodes the nested call has mutated or destroyed, which is the use-after-free. The re-entrancy sentinel in_on_accessibility_events_ and its base::AutoReset guard already existed, but only compiled under DCHECK_IS_ON(), so shipping (release) Windows builds carried no re-entrancy detection at all.
The patch adds an always-on CHECK(!in_on_accessibility_events_) under BUILDFLAG(IS_WIN) at method entry and compiles the sentinel and its AutoReset whenever BUILDFLAG(IS_WIN) || DCHECK_IS_ON(). This converts an undefined-behavior re-entrant UAF on stable Windows into a deterministic, safe abort before the nested processing can corrupt state. It is a detect-and-crash mitigation that tightens enforcement rather than a redesign that removes the re-entrancy itself.
OnAccessibilityEvents was debug-only (DCHECK_IS_ON()), leaving stable Windows unprotected; the fix enables an always-on CHECK plus the sentinel on BUILDFLAG(IS_WIN), so a re-entrant call aborts safely instead of proceeding into a use-after-free.Attack Path
- Deliver crafted accessibility updates
A compromised or malicious renderer sends a batch of accessibility events that the browser processes in
OnAccessibilityEvents(inferred: renderer control of the event stream). - Provoke synchronous re-entry
On Windows, handling those events invokes platform accessibility APIs that synchronously call back into the manager, re-entering
OnAccessibilityEventswhile the first invocation is still active. - Use freed/mutated nodes The nested invocation mutates or frees accessibility tree nodes that the suspended outer invocation continues to reference, producing the use-after-free described by the advisory.
- Post-patch behavior
With the change, the entry
CHECK(!in_on_accessibility_events_)fails on the nested call and aborts the process before the dangling access, turning the UAF into a controlled crash on Windows.
Impact Assessment
OnAccessibilityEvents processing on Windows (reachable from renderer-supplied accessibility events), and a successful UAF would occur in the browser process; the patch only adds a detect-and-abort guard, so any escalation from the freed-memory access to code execution is not demonstrated by the diff and is (inferred). No exploited-in-the-wild status is stated in the provided advisory.Files Changed
ui/accessibility/platform/browser_accessibility_manager.ccui/accessibility/platform/browser_accessibility_manager.h
Audit Directions
- Debug-only re-entrancy/state guardsGrep for sentinels and
AutoReset/CHECKlogic gated solely onDCHECK_IS_ON(); any that defend against re-entrant mutation of shared/owned state leave release builds unprotected. - Synchronous platform callbacks during tree mutationAudit browser-process code (accessibility, a11y platform integration) that calls OS APIs while iterating or mutating a cached tree, since those APIs can synchronously re-enter and invalidate iterators/nodes.
- Renderer-driven event batchesReview handlers that process batched, renderer-supplied events for assumptions of non-re-entrancy and for node lifetime across callbacks within a single batch.
Patch
From c52ae89ece584a8ee0bcb57eb703ae450592811d Mon Sep 17 00:00:00 2001
From: Takashi Sakamoto <tasak@google.com>
Date: Mon, 06 Apr 2026 01:10:32 -0700
Subject: [PATCH] Enable CHECK(!in_on_accessiblity_events) when BUILDFLAG(IS_WIN).
Bug: 495247950
No-Try: true
Change-Id: I3577464e3c716b3b2f48b58ca2c37ea63dc6c0a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7707532
Reviewed-by: Mitsuru Oshima <oshima@chromium.org>
Commit-Queue: Takashi Sakamoto <tasak@google.com>
Cr-Commit-Position: refs/heads/main@{#1610239}
---
diff --git a/ui/accessibility/platform/browser_accessibility_manager.cc b/ui/accessibility/platform/browser_accessibility_manager.cc
index b8e5100..5650ded 100644
--- a/ui/accessibility/platform/browser_accessibility_manager.cc
+++ b/ui/accessibility/platform/browser_accessibility_manager.cc
@@ -470,9 +470,13 @@
// remove this exclusion.
base::ScopedSafetyChecksExclusion scoped_unsafe;
-#if DCHECK_IS_ON()
+#if BUILDFLAG(IS_WIN)
+ CHECK(!in_on_accessibility_events_)
+ << "Should not re-enter OnAccessiblityEvents()";
+#endif // BUILDFLAG(IS_WIN)
+#if BUILDFLAG(IS_WIN) || DCHECK_IS_ON()
base::AutoReset<bool> auto_reset(&in_on_accessibility_events_, true);
-#endif // DCHECK_IS_ON()
+#endif // BUILDFLAG(IS_WIN) || DCHECK_IS_ON()
// Update the cached device scale factor.
if (!use_custom_device_scale_factor_for_testing_)
diff --git a/ui/accessibility/platform/browser_accessibility_manager.h b/ui/accessibility/platform/browser_accessibility_manager.h
index 5ce77548..4561ba3b 100644
--- a/ui/accessibility/platform/browser_accessibility_manager.h
+++ b/ui/accessibility/platform/browser_accessibility_manager.h
@@ -620,7 +620,7 @@
static bool never_suppress_or_delay_events_for_testing_;
// For debug only: True when handling OnAccessibilityEvents.
-#if DCHECK_IS_ON()
+#if BUILDFLAG(IS_WIN) || DCHECK_IS_ON()
bool in_on_accessibility_events_ = false;
#endif // DCHECK_IS_ON()
Original Bug Report
Potential UAF in BrowserAccessibilityManager::OnAccessibilityEvents via nested message loop
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential heap use-after-free vulnerability exists in BrowserAccessibilityManager on Windows due to iterator invalidation during synchronous accessibility event firing. If a compromised renderer triggers a re-entrant IPC call during a COM nested message loop, the underlying event map is cleared, leaving the outer loop’s iterators pointing to freed memory. This freed memory is not protected by MiraclePtr, potentially allowing an attacker to escape the renderer sandbox.
Affected files:
ui/accessibility/platform/browser_accessibility_manager.ccui/accessibility/platform/browser_accessibility_manager_win.ccui/accessibility/ax_event_generator.h
Estimated timestamp from git blame: 2025-05-26
Description
A potential Heap-Use-After-Free (UAF) vulnerability exists in the browser process’s accessibility subsystem on Windows. Specifically, BrowserAccessibilityManager::OnAccessibilityEvents iterates over generated events using a standard library iterator that becomes invalidated if re-entry occurs. Re-entry can happen because firing accessibility events on Windows involves synchronous COM calls that can spin a nested message loop. A compromised renderer process can exploit this window to trigger a re-entrant IPC, clearing the underlying event map and leaving the outer loop’s iterators pointing to freed heap memory.
Technical Details
In ui/accessibility/platform/browser_accessibility_manager.cc, the OnAccessibilityEvents method processes generated events in a range-based for loop:
587: for (const auto& targeted_event : event_generator()) {
588: BrowserAccessibility* event_target = GetFromID(targeted_event.node_id);
...
599: if (focus && focus != event_target && focus->IsDescendantOf(event_target)) {
600: FireGeneratedEvent(targeted_event.event_params->event,
601: event_target->node());
602: } else {
603: deferred_events.push_back(targeted_event);
604: }
605: }
The event_generator() returns an AXEventGenerator object. Its begin() method returns an AXEventGenerator::Iterator, which internally maintains std::map::const_iterator and std::set::const_iterator pointing to the dynamically allocated nodes of the tree_events_ map.
On Windows, the virtual FireGeneratedEvent method is implemented in BrowserAccessibilityManagerWin::FireGeneratedEvent. Depending on the event type, this method synchronously calls Windows accessibility APIs such as ::NotifyWinEvent or UIA COM methods (e.g., ::UiaRaiseAutomationEvent).
If an Assistive Technology (AT) client is active, these COM calls can trigger a nested message loop on the browser’s UI thread (which is a Single-Threaded Apartment) to prevent cross-process deadlocks while waiting for the COM call to complete.
During this nested message loop, the UI thread continues to pump messages. If a compromised renderer sends a new blink.mojom.RenderAccessibilityHost::HandleAXEvents IPC, the task is dequeued and executed, leading to a re-entrant call to BrowserAccessibilityManager::OnAccessibilityEvents.
The re-entrant call processes its own events and subsequently reaches line 638:
638: event_generator().ClearEvents();
AXEventGenerator::ClearEvents() calls tree_events_.clear(). This destroys the std::map and explicitly frees all its internal red-black tree nodes (_Rb_tree_node) and associated std::set data.
When the re-entrant call and the nested message loop finish, the original Windows COM call returns, and execution unwinds back to the original for loop at line 587. The loop implicitly calls operator++() on its AXEventGenerator::Iterator. Because the iterator relies on raw std::map node pointers, it accesses the memory that was just freed, resulting in a use-after-free.
Mitigation Bypasses
- MiraclePtr (BackupRefPtr): The primary UAF occurs during the traversal of standard library iterators (
std::map::const_iterator). These iterators use raw pointers internally (e.g.,node->next), which are not protected by MiraclePtr. Furthermore, theTargetedEventstruct explicitly marks its reference asraw_ref<const EventParams, DanglingUntriaged>, opting out of BRP protection. - DCHECKs:
BrowserAccessibilityManagercontains a boolean flagin_on_accessibility_events_intended to catch re-entrancy, but it is strictly guarded by#if DCHECK_IS_ON(). It does not prevent re-entrancy in release builds.
Potential Attack Steps
(Note: The following steps describe a theoretical attack scenario, as the AI agent has not executed a live proof-of-concept).
- Prerequisite: The attacker compromises a renderer process. An assistive technology (like a screen reader) is active, enabling the accessibility tree and Windows COM event routing.
- Trigger IPC: The compromised renderer sends an
AXTreeUpdatevia theHandleAXEventsMojo IPC, crafting it so that a target node contains the currently focused node as a descendant (satisfying the condition at line 599). - Nested Message Loop: The browser UI thread enters
FireGeneratedEventand calls a synchronous Windows API (e.g.,::NotifyWinEvent), spinning up a nested message loop. - Re-entrant IPC: While the UI thread is in the nested loop, the renderer sends a second, distinct
HandleAXEventsIPC. - Free Memory: The nested loop dequeues the second IPC, re-enters
OnAccessibilityEvents, and eventually callsClearEvents(), freeing thestd::mapnodes used by the first invocation’s iterator. - Heap Spray / Reclaim: The attacker uses parallel WebWorkers or other IPC channels to spray the browser process heap, reclaiming the freed
_Rb_tree_nodememory with controlled data. - Exploitation: The original COM call returns, and the outer loop calls
operator++. The iterator traverses the attacker’s forged heap data, granting the attacker an arbitrary memory read/write primitive, which can be escalated to arbitrary code execution (Browser RCE / Sandbox Escape).
Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f
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. Please feel free to reach out to me if you have concerns or feedback.