High watch CVSS 8.3 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in Accessibility
DescriptionType Confusion in Accessibility
ComponentAccessibility
Sub-componentA11y
Bug ClassType Confusion
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.
Tracker498401609
Fix commit375566bd90e1 (chromium/src) +59/-3
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

Background

`BrowserAccessibilityComWin`
the Windows COM wrapper object exposing a browser accessibility node to assistive technology via MSAA/IA2 interfaces such as IAccessibleHyperlink.
`AXPlatformNode` / `UniqueIdMap`
accessibility nodes are addressed by a process-wide unique ID; a global UniqueIdMap resolves an ID back to the live AXPlatformNode*, and IDs can be recycled/reassigned as nodes come and go.
`static_cast` vs. `QueryInterface`
a C++ static_cast asserts a type without checking it, whereas COM QueryInterface returns E_NOINTERFACE when the object does not actually implement the requested interface.
Hypertext cache / hyperlinks array
a parent (e.g., an iframe host) caches the unique IDs of its child hyperlinks; if a child frame’s BrowserAccessibilityManager detaches without invalidating that cache, the parent keeps stale IDs.

Root Cause Analysis

get_hyperlink resolved a hyperlink index to an AXPlatformNode* (via the process-wide UniqueIdMap) and then performed an unchecked static_cast<BrowserAccessibilityComWin*>(node), assuming the node was that concrete type. As the commit message states, a stale unique ID could be reassigned to a different node type after the parent cached it, so the static_cast produced an invalid/mistyped pointer that was then handed back as an IAccessibleHyperlink* through link->NewReference() — a type confusion. The stale ID arose because BrowserAccessibilityManager::DetachFromParentManager tore down the child-to-parent link without telling the parent to refresh its hypertext, leaving the parent’s hyperlinks array pointing at IDs whose nodes had changed type.

The fix replaces the blind cast with a COM QueryInterface for IAccessibleHyperlink (IID_PPV_ARGS(&hyperlink_result)), returning E_FAIL when the node does not implement that interface, and separately invalidates the parent’s cache on detach by calling UpdateAttributesOnParent. The regression test TestHyperlinkSafeTypeCheck confirms valid hyperlinks still return S_OK while out-of-range indices return E_INVALIDARG, locking in the checked behavior.

Key insight
The single core mistake was trusting a recycled unique ID’s node to be a BrowserAccessibilityComWin and casting it unchecked; the fix verifies the type at runtime with QueryInterface (and prevents stale IDs by invalidating the parent’s hypertext cache on detach) so a reassigned ID can no longer be reinterpreted as a hyperlink.

Attack Path

  1. Set up nested frames Content creates a parent document (e.g., an iframe host) whose accessibility node caches the unique IDs of child hyperlinks in its hyperlinks array.
  2. Detach a child frame The child frame’s BrowserAccessibilityManager detaches via DetachFromParentManager; pre-patch this did not invalidate the parent’s hypertext cache, so the parent retains the now-stale unique IDs.
  3. Recycle the unique ID The freed unique ID is reassigned to a different node type that is not a BrowserAccessibilityComWin hyperlink.
  4. Trigger the cast Assistive-technology (or a script path that drives it) calls get_hyperlink for the cached index; pre-patch the unchecked static_cast reinterprets the mistyped node and returns it as IAccessibleHyperlink*, yielding type confusion.
  5. Operate on the confused object Subsequent use of the returned interface dereferences a pointer whose real type differs from the assumed layout (inferred memory-safety consequence); the patch does not itself demonstrate a specific read/write primitive.

Impact Assessment

An attacker controlling page content (and able to provoke the accessibility tree via AT or automation) can cause a type confusion in the browser process’s Windows accessibility layer, which the advisory classifies as Type Confusion in Accessibility, severity high. Preconditions include an active accessibility client (screen reader / IA2 consumer) and the frame-detach-plus-ID-reassignment sequence; any escalation to a controlled read/write or code execution is (inferred) and not shown by the diff. The advisory does not report in-the-wild exploitation.

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

Audit Directions

  • Unchecked downcasts from ID maps
    Search for static_cast/reinterpret_cast applied to pointers obtained from UniqueIdMap or other process-wide ID lookups and require a type-verifying path (QueryInterface, role check, or safe downcast) before use.
  • Stale caches on teardown
    Audit every detach/destroy path in BrowserAccessibilityManager and related trees for caches (hyperlinks, hypertext, child ID lists) that are not invalidated when a subtree disconnects, enabling stale-ID reuse.
  • Recyclable identifiers
    Wherever an integer unique ID can be reassigned, verify consumers re-resolve and re-check type rather than assuming a previously cached ID still maps to the same object kind.
From 375566bd90e177693f2dd91cacd945e00a16dc23 Mon Sep 17 00:00:00 2001
From: Jacques Newman <janewman@microsoft.com>
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 <benjamin.beaudry@microsoft.com>
Commit-Queue: Jacques Newman <janewman@microsoft.com>
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 vm...@google.com

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