CVE-2026-7914
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/accessibility/platform/browser_accessibility_manager.cc |
modified | |
TEST_Fui/accessibility/platform/browser_accessibility_win_unittest.cc |
modified |
Files Changed
ui/accessibility/platform/browser_accessibility_com_win.ccui/accessibility/platform/browser_accessibility_manager.ccui/accessibility/platform/browser_accessibility_win_unittest.cc
Patch
From 375566bd90e177693f2dd91cacd945e00a16dc23 Mon Sep 17 00:00:00 2001 From: Jacques Newman <[email protected]> Date: Thu, 02 Apr 2026 08:57:32 -0700 Subject: [PATCH] [a11y] Fix type confusion in get_hyperlink Replace unchecked static_cast with COM QueryInterface in get_hyperlink(). The old code cast an AXPlatformNode* from the process-wide UniqueIdMap directly to BrowserAccessibilityComWin* without verifying the type. If a stale unique ID was reassigned to a different node type, this produced an invalid pointer. QueryInterface for IAccessibleHyperlink returns E_NOINTERFACE for nodes that don't implement it, preventing the bad cast. Also invalidate the parent iframe's hypertext cache when a child frame's BrowserAccessibilityManager detaches. Without this, the parent retains stale unique IDs in its hyperlinks array. Fixed: 498401609 Change-Id: I859b2aafa9383a6fbbce73d76226ef9f95fedca5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7722499 Reviewed-by: Benjamin Beaudry <[email protected]> Commit-Queue: Jacques Newman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1609259} --- diff --git a/ui/accessibility/platform/browser_accessibility_com_win.cc b/ui/accessibility/platform/browser_accessibility_com_win.cc index 880b148..ad38a0716 100644 --- a/ui/accessibility/platform/browser_accessibility_com_win.cc +++ b/ui/accessibility/platform/browser_accessibility_com_win.cc @@ -563,11 +563,13 @@ << "\nroot=" << manager->GetRootManager()->GetRoot(); return E_FAIL; } - auto* link = static_cast<BrowserAccessibilityComWin*>(node); - if (!link) + Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink_result; + if (FAILED(node->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(&hyperlink_result)))) { return E_FAIL; + } - *hyperlink = static_cast<IAccessibleHyperlink*>(link->NewReference()); + *hyperlink = hyperlink_result.Detach(); return S_OK; } diff --git a/ui/accessibility/platform/browser_accessibility_manager.cc b/ui/accessibility/platform/browser_accessibility_manager.cc index a34c0cb..b8e5100 100644 --- a/ui/accessibility/platform/browser_accessibility_manager.cc +++ b/ui/accessibility/platform/browser_accessibility_manager.cc @@ -1928,6 +1928,13 @@ } void BrowserAccessibilityManager::DetachFromParentManager() { + // Notify the parent to invalidate its hypertext cache before disconnecting, + // otherwise stale hyperlink IDs can cause type confusion after reassignment. + if (connected_to_parent_tree_node_) { + if (AXNode* parent = GetParentNodeFromParentTree()) { + UpdateAttributesOnParent(parent); + } + } connected_to_parent_tree_node_ = false; } diff --git a/ui/accessibility/platform/browser_accessibility_win_unittest.cc b/ui/accessibility/platform/browser_accessibility_win_unittest.cc index 39543ea..3d268c4 100644 --- a/ui/accessibility/platform/browser_accessibility_win_unittest.cc +++ b/ui/accessibility/platform/browser_accessibility_win_unittest.cc @@ -4004,4 +4004,51 @@ text_field_node->SetDelegateForTesting(pre_delegate); } +// Regression test for type confusion fix: verify get_hyperlink uses +// QueryInterface and returns correct results for valid hyperlinks. +TEST_F(BrowserAccessibilityWinTest, TestHyperlinkSafeTypeCheck) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.AddState(ax::mojom::State::kFocusable); + + AXNodeData text; + text.id = 2; + text.role = ax::mojom::Role::kStaticText; + text.SetName("Hello "); + + AXNodeData link; + link.id = 3; + link.role = ax::mojom::Role::kLink; + link.AddState(ax::mojom::State::kFocusable); + link.AddState(ax::mojom::State::kLinked); + link.SetName("world"); + link.SetNameFrom(ax::mojom::NameFrom::kContents); + + root.child_ids = {text.id, link.id}; + + std::unique_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdateForTesting(root, text, link), node_id_delegate_, + test_browser_accessibility_delegate_.get())); + + BrowserAccessibilityComWin* root_obj = + ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) + ->GetCOM(); + + LONG hyperlink_count = -1; + EXPECT_EQ(S_OK, root_obj->get_nHyperlinks(&hyperlink_count)); + EXPECT_EQ(1, hyperlink_count); + + Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink; + EXPECT_EQ(S_OK, root_obj->get_hyperlink(0, &hyperlink)); + EXPECT_NE(nullptr, hyperlink.Get()); + hyperlink.Reset(); + + EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(1, &hyperlink)); + EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(-1, &hyperlink)); + + manager.reset(); +} + } // namespace ui
Regression Test / PoC
diff --git a/ui/accessibility/platform/browser_accessibility_win_unittest.cc b/ui/accessibility/platform/browser_accessibility_win_unittest.cc
index 39543ea..3d268c4 100644
--- a/ui/accessibility/platform/browser_accessibility_win_unittest.cc
+++ b/ui/accessibility/platform/browser_accessibility_win_unittest.cc
@@ -4004,4 +4004,51 @@
text_field_node->SetDelegateForTesting(pre_delegate);
}
+// Regression test for type confusion fix: verify get_hyperlink uses
+// QueryInterface and returns correct results for valid hyperlinks.
+TEST_F(BrowserAccessibilityWinTest, TestHyperlinkSafeTypeCheck) {
+ AXNodeData root;
+ root.id = 1;
+ root.role = ax::mojom::Role::kRootWebArea;
+ root.AddState(ax::mojom::State::kFocusable);
+
+ AXNodeData text;
+ text.id = 2;
+ text.role = ax::mojom::Role::kStaticText;
+ text.SetName("Hello ");
+
+ AXNodeData link;
+ link.id = 3;
+ link.role = ax::mojom::Role::kLink;
+ link.AddState(ax::mojom::State::kFocusable);
+ link.AddState(ax::mojom::State::kLinked);
+ link.SetName("world");
+ link.SetNameFrom(ax::mojom::NameFrom::kContents);
+
+ root.child_ids = {text.id, link.id};
+
+ std::unique_ptr<BrowserAccessibilityManager> manager(
+ BrowserAccessibilityManager::Create(
+ MakeAXTreeUpdateForTesting(root, text, link), node_id_delegate_,
+ test_browser_accessibility_delegate_.get()));
+
+ BrowserAccessibilityComWin* root_obj =
+ ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot())
+ ->GetCOM();
+
+ LONG hyperlink_count = -1;
+ EXPECT_EQ(S_OK, root_obj->get_nHyperlinks(&hyperlink_count));
+ EXPECT_EQ(1, hyperlink_count);
+
+ Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink;
+ EXPECT_EQ(S_OK, root_obj->get_hyperlink(0, &hyperlink));
+ EXPECT_NE(nullptr, hyperlink.Get());
+ hyperlink.Reset();
+
+ EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(1, &hyperlink));
+ EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(-1, &hyperlink));
+
+ manager.reset();
+}
+
} // namespace ui
Original Bug Report
Potential Browser RCE via Type Confusion in BrowserAccessibilityComWin::get_hyperlink
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 potential type confusion vulnerability exists in the browser process due to a stale accessibility cache when an iframe is destroyed. By rapidly churning the 32-bit AXUniqueId counter to wrap around, an attacker can reassign a stale ID to a smaller, unrelated accessibility node. An unchecked downcast in BrowserAccessibilityComWin::get_hyperlink then adds an offset to this object, yielding an out-of-bounds pointer that is dereferenced by the OS COM layer, potentially leading to a sandbox escape.
Affected files:
ui/accessibility/platform/browser_accessibility_com_win.ccui/accessibility/ax_unique_id.ccui/accessibility/platform/ax_platform_node_base.ccui/accessibility/ax_tree_manager.cccontent/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2022-10-28
Summary
A type confusion vulnerability in the Windows-specific accessibility implementation can potentially allow a compromised renderer to hijack control flow in the browser process. The issue occurs in BrowserAccessibilityComWin::get_hyperlink, which performs an unchecked downcast on an object retrieved from a process-wide map using a stale unique ID. By driving the 32-bit AXUniqueId counter to wrap around, an attacker can cause a stale ID (formerly associated with a child document) to be reassigned to a different object type, such as a Views-based accessibility node. Subsequent COM method calls on the confused object lead to out-of-bounds memory access and potential arbitrary code execution.
Vulnerability Details
1. Stale Cache Mechanism
In BrowserAccessibilityComWin::get_hyperlink(), an AXPlatformNode* is retrieved from a process-wide UniqueIdMap using a cached unique ID. For iframe nodes, this ID is populated by traversing into the child document during AXPlatformNodeBase::UpdateComputedHypertext.
When a child frame is destroyed or navigated, RenderFrameHostImpl::~RenderFrameHostImpl() calls DetachFromParentManager(), which sets connected_to_parent_tree_node_ to false. Consequently, during AXTreeManager::~AXTreeManager(), the logic early-returns if connected_to_parent_tree_node_ is false, bypassing the call to UpdateAttributesOnParent. This prevents the parent iframe’s hypertext cache from being invalidated or refreshed, leaving it with a stale reference to the destroyed child root’s unique ID.
2. AXUniqueId Wraparound
The AXUniqueId system uses a 32-bit counter (int32_t) that wraps around at INT32_MAX. While a high number of allocations is required (~2^31), a compromised renderer can accelerate this process through rapid DOM churn (e.g., repeatedly creating and deleting nodes). In a long-running session, the counter can wrap, allowing a previously released ID to be reassigned to a new, unrelated accessibility node—such as an AXPlatformNodeWin created for a UI View.
3. Type Confusion and Out-of-Bounds Access
When get_hyperlink is called on an iframe with a stale ID that has been reassigned to a Views-based AXPlatformNodeWin:
AXPlatformNodeWin::GetFromUniqueId(stale_id)returns the newAXPlatformNodeWin*object.- The code performs an unchecked
static_cast<BrowserAccessibilityComWin*>(node). This is a type confusion because the object is actually a plainAXPlatformNodeWin(used for Views), which does not implement theBrowserAccessibilityComWininterface. - The code then performs
static_cast<IAccessibleHyperlink*>(link->NewReference()). SinceIAccessibleHyperlinkis a base class ofBrowserAccessibilityComWinbut not of the originalAXPlatformNodeWin, this cast adds an offset (typicallysizeof(AXPlatformNodeWin) + 8) to the pointer. - The resulting pointer physically points past the end of the
AXPlatformNodeWinallocation, into adjacent heap memory (e.g., PartitionAlloc metadata or an adjacent object).
Notably, the check if(!link) following the cast is ineffective as static_cast will not return null for a non-null input even if the type is incorrect.
Potential Exploitation Steps
Note: These are potential steps; we currently do not have a working proof of concept that has been successfully run.
- Setup: The victim is running Chrome on Windows with an Assistive Technology (AT) active. An attacker controls a compromised renderer page containing an iframe.
- Cache Initialization: The AT polls the accessibility tree, causing the parent iframe node to cache the child document’s
AXUniqueIdin itshypertext_.hyperlinksarray. - Frame Destruction: The attacker’s JavaScript destroys the iframe. Due to the early return in
AXTreeManager::~AXTreeManager, the parent’s cache is not invalidated. - Counter Wraparound: The attacker’s JavaScript rapidly creates and destroys millions of DOM nodes, causing the
AXUniqueIdcounter to increment until it reachesINT32_MAXand wraps around back to the stale ID. - Heap Grooming: The attacker creates controlled allocations (e.g., using Mojo blobs) in the browser process heap, filled with fake vtable pointers, adjacent to where new accessibility nodes will be allocated.
- ID Reassignment: The attacker triggers a browser UI action (e.g.,
window.print()) that allocates a new nativeAXPlatformNodeWin. This new node is assigned the wrapped-around stale ID. - Trigger Type Confusion: The AT queries the parent iframe node by calling
get_hyperlink. The browser looks up the stale ID, retrieves the newly allocated UI node, and performs the unchecked downcast and pointer offset. - Arbitrary Code Execution: The out-of-bounds pointer is returned to the OS COM layer. When the COM layer dereferences this pointer to call a method, it reads the vtable pointer from the attacker-controlled blob, leading to an indirect call to an attacker-controlled address and a browser process sandbox escape.
Impact
This vulnerability provides a primitive that can be leveraged for Remote Code Execution (RCE) in the unsandboxed Browser Process. It is not protected by MiraclePtr (BRP) because the issue involves an arithmetic offset on a live object rather than a Use-After-Free, and raw pointers are passed to the OS COM layer.
Suggested Fix
There are two primary areas that require fixing:
- Cache Invalidation: Ensure that when
RenderFrameHostImpldestroys a child frame’sBrowserAccessibilityManager, the parent node’s cache is properly invalidated. The early return inAXTreeManager::ParentConnectionChangedwhenparentis null needs to be addressed, potentially by maintaining a persistent reference or identifying an alternative way to triggerUpdateAttributesOnParentbefore the manager is fully detached. - Safe Casting: In
BrowserAccessibilityComWin::get_hyperlink, replace thestatic_castwith a safe cast or a type check. Before casting the retrievedAXPlatformNodetoBrowserAccessibilityComWin, verify its role, class type, or delegate to ensure it is actually an instance ofBrowserAccessibilityComWinand not a plainAXPlatformNodeWin.
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.