High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Accessibility
DescriptionType Confusion in Accessibility
ComponentAccessibility
Bug ClassType Confusion
Tracker498401609
Fix commit375566bd90e1 (chromium/src) +59/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
ui/accessibility/platform/browser_accessibility_manager.cc
modified
TEST_F
ui/accessibility/platform/browser_accessibility_win_unittest.cc
modified

Files Changed

  • ui/accessibility/platform/browser_accessibility_com_win.cc
  • ui/accessibility/platform/browser_accessibility_manager.cc
  • ui/accessibility/platform/browser_accessibility_win_unittest.cc
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • ui/accessibility/ax_unique_id.cc
  • ui/accessibility/platform/ax_platform_node_base.cc
  • ui/accessibility/ax_tree_manager.cc
  • content/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:

  1. AXPlatformNodeWin::GetFromUniqueId(stale_id) returns the new AXPlatformNodeWin* object.
  2. The code performs an unchecked static_cast<BrowserAccessibilityComWin*>(node). This is a type confusion because the object is actually a plain AXPlatformNodeWin (used for Views), which does not implement the BrowserAccessibilityComWin interface.
  3. The code then performs static_cast<IAccessibleHyperlink*>(link->NewReference()). Since IAccessibleHyperlink is a base class of BrowserAccessibilityComWin but not of the original AXPlatformNodeWin, this cast adds an offset (typically sizeof(AXPlatformNodeWin) + 8) to the pointer.
  4. The resulting pointer physically points past the end of the AXPlatformNodeWin allocation, 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.

  1. Setup: The victim is running Chrome on Windows with an Assistive Technology (AT) active. An attacker controls a compromised renderer page containing an iframe.
  2. Cache Initialization: The AT polls the accessibility tree, causing the parent iframe node to cache the child document’s AXUniqueId in its hypertext_.hyperlinks array.
  3. Frame Destruction: The attacker’s JavaScript destroys the iframe. Due to the early return in AXTreeManager::~AXTreeManager, the parent’s cache is not invalidated.
  4. Counter Wraparound: The attacker’s JavaScript rapidly creates and destroys millions of DOM nodes, causing the AXUniqueId counter to increment until it reaches INT32_MAX and wraps around back to the stale ID.
  5. 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.
  6. ID Reassignment: The attacker triggers a browser UI action (e.g., window.print()) that allocates a new native AXPlatformNodeWin. This new node is assigned the wrapped-around stale ID.
  7. 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.
  8. 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:

  1. Cache Invalidation: Ensure that when RenderFrameHostImpl destroys a child frame’s BrowserAccessibilityManager, the parent node’s cache is properly invalidated. The early return in AXTreeManager::ParentConnectionChanged when parent is null needs to be addressed, potentially by maintaining a persistent reference or identifying an alternative way to trigger UpdateAttributesOnParent before the manager is fully detached.
  2. Safe Casting: In BrowserAccessibilityComWin::get_hyperlink, replace the static_cast with a safe cast or a type check. Before casting the retrieved AXPlatformNode to BrowserAccessibilityComWin, verify its role, class type, or delegate to ensure it is actually an instance of BrowserAccessibilityComWin and not a plain AXPlatformNodeWin.

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.

View on issue tracker