CVE-2026-9905
Overview
Background
- `SequenceAffineComObjectRoot`
- a COM object base used by Windows accessibility providers that is affine to one sequence (the UI thread), checking sequence affinity with
DCHECK_CALLED_ON_VALID_SEQUENCE. - `IFastRundown`
- a COM interface that signals the COM runtime to run down (release) references faster when an out-of-process client disappears; pre-fix it was implemented by the accessibility COM classes (
ax_platform_node_textchildprovider_win.h:18,:22). - COM fast rundown
- when
IFastRundownis present, COM may bypass single-threaded-apartment marshaling and callRelease()(and thus the destructor) directly on an RPC dispatch thread rather than the UI thread. - `AXTreeManagerMap`
- a process-global, unsynchronized map of accessibility tree managers accessed during provider destruction. (described by reporter; not in shown source)
Root Cause Analysis
The accessibility COM provider classes derived from SequenceAffineComObjectRoot and additionally implemented IFastRundown, e.g. AXPlatformNodeTextChildProviderWin : public SequenceAffineComObjectRoot, public ITextChildProvider, public IFastRundown with COM_INTERFACE_ENTRY(IFastRundown) (ax_platform_node_textchildprovider_win.h:16-22), and the same pattern in AXPlatformNodeTextProviderWin (ax_platform_node_textprovider_win.h:18-25), plus AXPlatformNodeTextRangeProviderWin, AXPlatformNodeWin, and AXPlatformRelationWin (shown in the diff). These classes are designed to live and be destroyed on the UI thread, enforced only by a DCHECK_CALLED_ON_VALID_SEQUENCE that is compiled out of release builds (per reporter). Because IFastRundown lets COM bypass STA marshaling, a provider’s destructor can run on an RPC dispatch thread, so destruction-time work that touches the process-global, unsynchronized AXTreeManagerMap races with UI-thread mutations of that map. The invariant that these sequence-affine objects are only destroyed on their owning sequence was therefore broken by the presence of IFastRundown.
The fix is a revert that removes IFastRundown from the base-class list and the COM_INTERFACE_ENTRY(IFastRundown) from the COM map of all five classes (diff hunks across ax_platform_node_textchildprovider_win.h, ax_platform_node_textprovider_win.h, ax_platform_node_textrangeprovider_win.h, ax_platform_node_win.h, ax_platform_relation_win.h); the commit message states “IFastRundown is incompatible with SequenceAffineComObjectRoot, as it allows dtors to run off the UI thread.” With IFastRundown gone, COM reverts to STA marshaling so these objects are destroyed back on the UI thread, removing the off-thread destruction that enabled the race. The CreateIUnknown edits are an incidental cleanup, and the downstream map/observer corruption steps described by the reporter are (inferred) since that code is not shown here.
IFastRundown on sequence-affine COM objects let COM destroy them off the UI thread, racing with UI-thread accessibility state; the fix removes IFastRundown so destruction returns to the owning thread.Attack Path
- Hold a reference from an out-of-process client A UIA/accessibility client holds COM references to the Windows accessibility providers. (trigger per reporter)
- Terminate the client to trigger fast rundown
When the client dies, COM, seeing
IFastRundown, runsRelease()and the destructor on an RPC dispatch thread instead of the UI thread (ax_platform_node_textprovider_win.h:20, pre-fix). - Race against UI-thread map mutation
A compromised renderer rapidly mutates accessibility state so the UI thread modifies
AXTreeManagerMapwhile the RPC-thread destructor reads it, causing a use-after-free on the map’s backing storage. (inferred; map code not shown) - Corruption primitive The attacker reclaims the freed storage to control pointers read during destruction, yielding memory corruption. (inferred)
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/accessibility/platform/ax_platform_node_textchildprovider_win.cc |
modified | |
COMPONENT_EXPORTui/accessibility/platform/ax_platform_node_textchildprovider_win.h |
modified | |
ifui/accessibility/platform/ax_platform_node_textprovider_win.cc |
modified | |
COMPONENT_EXPORTui/accessibility/platform/ax_platform_node_textprovider_win.h |
modified | |
COMPONENT_EXPORTui/accessibility/platform/ax_platform_node_textrangeprovider_win.h |
modified |
Files Changed
chrome/browser/accessibility/leaked_node_detection_browsertest.ccui/accessibility/platform/ax_platform_node_textchildprovider_win.ccui/accessibility/platform/ax_platform_node_textchildprovider_win.hui/accessibility/platform/ax_platform_node_textprovider_win.ccui/accessibility/platform/ax_platform_node_textprovider_win.hui/accessibility/platform/ax_platform_node_textrangeprovider_win.h
Audit Directions
- Thread-affinity vs. COM rundown interfacesNever combine
IFastRundown(or any interface permitting off-apartment release) with objects whose lifetime is pinned to a single sequence; destruction must honor the affinity. - Release-stripped affinity checksA
DCHECK_CALLED_ON_VALID_SEQUENCEguarding a destructor gives no protection in release builds; enforce withCHECKor by design when the violation is a memory-safety issue. - Unsynchronized process-global containersAudit any process-global map/container reachable from both UI-thread mutations and background-thread callbacks for the possibility of concurrent access during rehash/resize.
Patch
From 52f2425c43f2ca0091b7b28f5aadb2cdffff25a8 Mon Sep 17 00:00:00 2001 From: Greg Thompson <[email protected]> Date: Tue, 26 May 2026 08:48:26 -0700 Subject: [PATCH] [M148] Revert "Reland "[A11yPerformance] Enable fast rundown for accessibility objects"" Original change's description: > Revert "Reland "[A11yPerformance] Enable fast rundown for accessibility objects"" > > This reverts commit 638a472eef8aefbfb4963cab584dfb49a3d45c7e. > > Reason for revert: IFastRundown is incompatible with SequenceAffineComObjectRoot, as it allows dtors to run off the UI thread. > > Original change's description: > > Reland "[A11yPerformance] Enable fast rundown for accessibility objects" > > > > This is a reland of commit 723fef8a40d4e3a3c32a3d065ae230fea3bb77ee. > > LeakedNodeDetectionBrowsertest.TerminateClient is disabled due to > > failures on some bots. > > > > Original change's description: > > > [A11yPerformance] Enable fast rundown for accessibility objects > > > > > > Chrome vends accessibility objects to an assistive technology tool via > > > COM interface pointers. COM machinery is responsible for releasing > > > outstanding references held by an out-of-process client in the event > > > that said client terminates. This "rundown" processing may take on the > > > order of minutes. > > > > > > In this CL, we add IFastRundown to the set of interfaces implemented by > > > our COM classes. This is a signal to COM that we would like rundown to > > > happen more quickly. Local testing shows that rundown happens after > > > approximately ten seconds rather than six minutes. > > > > > > BYPASS_RECITATION_REASON=False positive; only indentation is changing. > > > > > > AX-Relnotes: n/a. > > > Bug: 443107137 > > > Change-Id: Ifacacc3e69fa34a296be513a71528529fe40095c > > > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6917710 > > > Auto-Submit: Greg Thompson <[email protected]> > > > Reviewed-by: Benjamin Beaudry <[email protected]> > > > Commit-Queue: Greg Thompson <[email protected]> > > > Cr-Commit-Position: refs/heads/main@{#1514777} > > > > BYPASS_RECITATION_REASON=False positive; only indentation is changing. > > > > AX-Relnotes: n/a. > > Bug: 443107137 > > Change-Id: Id45749d9f73ce4e327bc73e6c7b89c00fa50eb3f > > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6943297 > > Auto-Submit: Greg Thompson <[email protected]> > > Reviewed-by: Benjamin Beaudry <[email protected]> > > Commit-Queue: Benjamin Beaudry <[email protected]> > > Cr-Commit-Position: refs/heads/main@{#1518866} > > Bug: 443107137 > Fixed: 498883610 > Change-Id: I33ad27037b9dabdf7366b4f4393e172ac3cce8e6 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785869 > Auto-Submit: Greg Thompson <[email protected]> > Reviewed-by: Benjamin Beaudry <[email protected]> > Commit-Queue: Benjamin Beaudry <[email protected]> > Cr-Commit-Position: refs/heads/main@{#1618915} (cherry picked from commit 1f09265853ad1fb13ab57f0c573eb32bf87bc05e) Bug: 514923707,443107137,498883610 Change-Id: I33ad27037b9dabdf7366b4f4393e172ac3cce8e6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876281 Reviewed-by: Benjamin Beaudry <[email protected]> Reviewed-by: Greg Thompson <[email protected]> Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com> Commit-Queue: Benjamin Beaudry <[email protected]> Cr-Commit-Position: refs/branch-heads/7778@{#3756} Cr-Branched-From: 77f495ee216d4c3cc784d33658bad4778c0680ee-refs/heads/main@{#1610480} --- diff --git a/chrome/browser/accessibility/leaked_node_detection_browsertest.cc b/chrome/browser/accessibility/leaked_node_detection_browsertest.cc index 9a196a8..e3f5dc0 100644 --- a/chrome/browser/accessibility/leaked_node_detection_browsertest.cc +++ b/chrome/browser/accessibility/leaked_node_detection_browsertest.cc @@ -70,14 +70,21 @@ WaitForNodeCounts({0U, 0U, 0U, 0U}); } -// TODO(crbug.com/443107137): Disabled due to flaky failures. It seems that not -// all machines perform rundown quckily upon disappearance of the UiaClient -// despite all classes implementing IFastRundown. Run this test with -// --test-launcher-timeout=3600000 --ui-test-action-max-timeout=3600000 -// --ui-test-action-timeout=3600000 --single-process-tests to set the various -// test timeouts longer than the approx six minute slow rundown time. +// Disabled due to flaky failures. It seems that not all machines perform +// rundown quckily upon disappearance of the client. IN_PROC_BROWSER_TEST_P(LeakedNodeDetectionBrowsertest, DISABLED_TerminateClient) { + if (client_api() == ClientApi::kIAccessible2) { + // When MSAA is in use, it can take six minutes for the final references on + // the stubs issued by the accessible process to be released after + // termination of the client. This is far greater than the normal test + // timeout. Run this test with --test-launcher-timeout=3600000 + // --ui-test-action-max-timeout=3600000 --ui-test-action-timeout=3600000 to + // see that it eventually passes. + GTEST_SKIP() << "Termination of an IA2 client with normal " + "test timeouts causes leaks"; + } + // Initialize the UI Automation client; giving it this window. ASSERT_HRESULT_SUCCEEDED(InitializeClient(browser())); diff --git a/ui/accessibility/platform/ax_platform_node_textchildprovider_win.cc b/ui/accessibility/platform/ax_platform_node_textchildprovider_win.cc index 97d8fe4..8278da8 100644 --- a/ui/accessibility/platform/ax_platform_node_textchildprovider_win.cc +++ b/ui/accessibility/platform/ax_platform_node_textchildprovider_win.cc @@ -53,12 +53,8 @@ IUnknown** unknown) { Microsoft::WRL::ComPtr<AXPlatformNodeTextChildProviderWin> text_child_provider(Create(owner)); - if (!text_child_provider) { - *unknown = nullptr; - return; - } - ITextChildProvider* provider_ptr = text_child_provider.Detach(); - *unknown = provider_ptr; + if (text_child_provider) + *unknown = text_child_provider.Detach(); } HRESULT AXPlatformNodeTextChildProviderWin::get_TextContainer( diff --git a/ui/accessibility/platform/ax_platform_node_textchildprovider_win.h b/ui/accessibility/platform/ax_platform_node_textchildprovider_win.h index 78f8d9e..dbc628e 100644 --- a/ui/accessibility/platform/ax_platform_node_textchildprovider_win.h +++ b/ui/accessibility/platform/ax_platform_node_textchildprovider_win.h @@ -14,12 +14,10 @@ namespace ui { class COMPONENT_EXPORT(AX_PLATFORM) AXPlatformNodeTextChildProviderWin : public SequenceAffineComObjectRoot, - public ITextChildProvider, - public IFastRundown { + public ITextChildProvider { public: BEGIN_COM_MAP(AXPlatformNodeTextChildProviderWin) COM_INTERFACE_ENTRY(ITextChildProvider) - COM_INTERFACE_ENTRY(IFastRundown) END_COM_MAP() AXPlatformNodeTextChildProviderWin(); diff --git a/ui/accessibility/platform/ax_platform_node_textprovider_win.cc b/ui/accessibility/platform/ax_platform_node_textprovider_win.cc index 7849e7bb..86c66f52 100644 --- a/ui/accessibility/platform/ax_platform_node_textprovider_win.cc +++ b/ui/accessibility/platform/ax_platform_node_textprovider_win.cc @@ -47,12 +47,8 @@ IUnknown** unknown) { Microsoft::WRL::ComPtr<AXPlatformNodeTextProviderWin> text_provider( Create(owner)); - if (!text_provider) { - *unknown = nullptr; - return; - } - ITextEditProvider* provider_ptr = text_provider.Detach(); - *unknown = provider_ptr; + if (text_provider) + *unknown = text_provider.Detach(); } // diff --git a/ui/accessibility/platform/ax_platform_node_textprovider_win.h b/ui/accessibility/platform/ax_platform_node_textprovider_win.h index 0c78e94..c57e6ebe 100644 --- a/ui/accessibility/platform/ax_platform_node_textprovider_win.h +++ b/ui/accessibility/platform/ax_platform_node_textprovider_win.h @@ -13,16 +13,14 @@ namespace ui { -class COMPONENT_EXPORT(AX_PLATFORM) - __declspec(uuid("3e1c192b-4348-45ac-8eb6-4b58eeb3dcca")) - AXPlatformNodeTextProviderWin : public SequenceAffineComObjectRoot, - public ITextEditProvider, - public IFastRundown { +class COMPONENT_EXPORT(AX_PLATFORM) __declspec( + uuid("3e1c192b-4348-45ac-8eb6-4b58eeb3dcca")) AXPlatformNodeTextProviderWin + : public SequenceAffineComObjectRoot, + public ITextEditProvider { public: BEGIN_COM_MAP(AXPlatformNodeTextProviderWin) COM_INTERFACE_ENTRY(ITextProvider) COM_INTERFACE_ENTRY(ITextEditProvider) - COM_INTERFACE_ENTRY(IFastRundown) COM_INTERFACE_ENTRY(AXPlatformNodeTextProviderWin) END_COM_MAP() diff --git a/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h b/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h index b5769710..291e40d7 100644 --- a/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h +++ b/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h @@ -19,15 +19,13 @@ #include "ui/accessibility/platform/sequence_affine_com_object_root_win.h" namespace ui { -class COMPONENT_EXPORT(AX_PLATFORM) - __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1"))
Regression Test / PoC
diff --git a/chrome/browser/accessibility/leaked_node_detection_browsertest.cc b/chrome/browser/accessibility/leaked_node_detection_browsertest.cc
index 9a196a8..e3f5dc0 100644
--- a/chrome/browser/accessibility/leaked_node_detection_browsertest.cc
+++ b/chrome/browser/accessibility/leaked_node_detection_browsertest.cc
@@ -70,14 +70,21 @@
WaitForNodeCounts({0U, 0U, 0U, 0U});
}
-// TODO(crbug.com/443107137): Disabled due to flaky failures. It seems that not
-// all machines perform rundown quckily upon disappearance of the UiaClient
-// despite all classes implementing IFastRundown. Run this test with
-// --test-launcher-timeout=3600000 --ui-test-action-max-timeout=3600000
-// --ui-test-action-timeout=3600000 --single-process-tests to set the various
-// test timeouts longer than the approx six minute slow rundown time.
+// Disabled due to flaky failures. It seems that not all machines perform
+// rundown quckily upon disappearance of the client.
IN_PROC_BROWSER_TEST_P(LeakedNodeDetectionBrowsertest,
DISABLED_TerminateClient) {
+ if (client_api() == ClientApi::kIAccessible2) {
+ // When MSAA is in use, it can take six minutes for the final references on
+ // the stubs issued by the accessible process to be released after
+ // termination of the client. This is far greater than the normal test
+ // timeout. Run this test with --test-launcher-timeout=3600000
+ // --ui-test-action-max-timeout=3600000 --ui-test-action-timeout=3600000 to
+ // see that it eventually passes.
+ GTEST_SKIP() << "Termination of an IA2 client with normal "
+ "test timeouts causes leaks";
+ }
+
// Initialize the UI Automation client; giving it this window.
ASSERT_HRESULT_SUCCEEDED(InitializeClient(browser()));
Original Bug Report
Potential Sandbox Escape via UAF in AXTreeManagerMap due to IFastRundown Race
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 security team.
Overview: A race condition exists between COM’s IFastRundown mechanism on an RPC thread and UI thread mutations to the process-global AXTreeManagerMap. This leads to a Use-After-Free (UAF) on the map’s backing array, bypassing MiraclePtr (BRP) and allowing a compromised renderer to achieve arbitrary memory corruption and potential sandbox escape.
Affected files:
ui/accessibility/platform/ax_platform_node_textrangeprovider_win.ccui/accessibility/ax_tree_manager_map.hui/accessibility/ax_tree_manager_map.ccui/accessibility/platform/sequence_affine_com_object_root_win.hui/accessibility/ax_tree_manager.ccui/accessibility/ax_tree_manager.h
Estimated timestamp from git blame: 2023-06-23
Background
AXPlatformNodeTextRangeProviderWin is a COM class used to provide text range information to UI Automation (UIA) clients on Windows. It inherits from SequenceAffineComObjectRoot and CComSingleThreadModel, indicating it is designed to run on the UI thread (an STA). To reduce latency during out-of-process client crashes, it implements IFastRundown.
The Vulnerability
When an out-of-process UIA client terminates abruptly, the COM runtime initiates a fast rundown. Because IFastRundown is implemented, COM bypasses STA marshaling and calls Release() directly on an RPC dispatch thread.
SequenceAffineComObjectRoot::InternalRelease() asserts it is on the correct sequence via DCHECK_CALLED_ON_VALID_SEQUENCE. However, because DCHECKs are removed in release builds, the sequence violation is ignored, and the object is destroyed on the RPC thread.
During destruction, ~TextRangeEndpoints() sets its start and end positions to null sequentially:
SetStart(null)setsstart_toAXTreeIDUnknown().SetEnd(null)evaluates whether to callRemoveObserver(end_->tree_id()). The checkend_->tree_id() != start_->tree_id()evaluates totruebecausestart_was just nulled. This erroneously triggersRemoveObserverfor theend_tree ID on the RPC thread.
RemoveObserver calls AXTreeManager::FromID(), which accesses the process-global, unsynchronized AXTreeManagerMap (an absl::flat_hash_map<AXTreeID, raw_ptr<AXTreeManager>>).
The Race Condition and BRP Bypass
Concurrently, a compromised renderer can rapidly create and destroy <iframe> elements, forcing the browser to insert and remove entries in the AXTreeManagerMap on the UI thread. This causes the absl::flat_hash_map to frequently rehash, allocating new backing slot arrays and freeing old ones.
If the RPC thread executes map_.find() while the UI thread is rehashing the map, the RPC thread will read from a freed slot array.
Crucially, MiraclePtr (BRP) does not prevent exploitation here. The UAF occurs on the container’s backing array (which holds the raw_ptr bytes), not the AXTreeManager object itself. An attacker can reclaim the freed slot array and directly overwrite the 8 bytes of the raw_ptr, bypassing BRP quarantine entirely.
Exploitation Primitive
The attacker controls the pointer returned by GetManager. The code proceeds to call ax_tree_manager->ax_tree()->RemoveObserver(this); using the fake AXTreeManager. This invokes ObserverList::RemoveObserver on an attacker-controlled fake AXTree object.
By carefully crafting the fake ObserverList memory structure (e.g., controlling observers_ vector pointers and the live_iterators_ linked list), the attacker can force ObserverList to execute it->MarkForRemoval() or observers_.erase(it) on arbitrary addresses. This provides powerful arbitrary decrement, arbitrary null-write, or arbitrary memory move primitives, which can be leveraged to hijack control flow and escape the sandbox.
Potential Attacker Steps
Note: These steps are theoretical, as our tooling agent cannot execute code.
- Compromise a renderer process.
- Execute a loop rapidly creating and destroying iframes to trigger UI thread map mutations and rehashes.
- Spray the browser heap to reclaim freed
absl::flat_hash_mapbacking arrays with fakeraw_ptr<AXTreeManager>entries. - Induce a crash in an out-of-process UIA client to trigger COM fast rundown on the RPC thread.
- The RPC thread reads the attacker’s fake pointer and accesses a fake
AXTree. - Use the
ObserverList::RemoveObserverprimitive to corrupt a function pointer and execute arbitrary shellcode.
Suggested Fix
- Remove IFastRundown: The safest fix is to remove
IFastRundownfromAXPlatformNodeTextRangeProviderWin. If an object is strictly tied to an STA (UI thread), it must be cleaned up on that thread, even if it delays rundown. - Enforce Sequence Checks: Upgrade the
DCHECKinSequenceAffineComObjectRootto aCHECKto ensure COM objects are never destroyed off-sequence in release builds. - Thread-Safe Map: If cross-thread access to
AXTreeManagerMapis truly required, synchronize access using anabsl::Mutexor base locking mechanism.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.