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
Tracker498885920
Fix commitfa454ea8264a (chromium/src) +43/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
BrowserAccessibility
ui/accessibility/platform/ax_platform_node_delegate.h
modified
ChildIterator
ui/accessibility/platform/ax_platform_node_delegate.h
modified
if
ui/accessibility/platform/browser_accessibility.cc
modified
if
ui/accessibility/platform/browser_accessibility_com_win.cc
modified
if
ui/accessibility/platform/browser_accessibility_manager_win.cc
modified
for
ui/accessibility/platform/browser_accessibility_manager_win.cc
modified

Files Changed

  • ui/accessibility/platform/ax_platform_node_delegate.cc
  • ui/accessibility/platform/ax_platform_node_delegate.h
  • ui/accessibility/platform/browser_accessibility.cc
  • ui/accessibility/platform/browser_accessibility.h
  • ui/accessibility/platform/browser_accessibility_com_win.cc
  • ui/accessibility/platform/browser_accessibility_manager_win.cc
From fa454ea8264a3a10c1992a370183b91037d0c347 Mon Sep 17 00:00:00 2001
From: Peter K <pkotwicz@google.com>
Date: Mon, 20 Jul 2026 15:19:56 -0700
Subject: [PATCH] [A11y] Fix potential type confusion in BrowserAccessibilityManagerWin

This CL fixes potential type confusion in BrowserAccessibilityManagerWin::OnAtomicUpdateFinished() and
BrowserAccessibilityComWin::GetTargetFromChildID()
by using
ToBrowserAccessibilityComWin(
    BrowserAccessibility::FromAXPlatformNodeDelegate())

Bug:498885920

Change-Id: Idd3e2185e87fbaed58d8aa3f0025b5767fa0d514
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7958810
Reviewed-by: David Tseng <dtseng@chromium.org>
Commit-Queue: Peter Kotwicz <pkotwicz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1664976}
---

diff --git a/ui/accessibility/platform/ax_platform_node_delegate.cc b/ui/accessibility/platform/ax_platform_node_delegate.cc
index 1daf0aad..c65a0cb 100644
--- a/ui/accessibility/platform/ax_platform_node_delegate.cc
+++ b/ui/accessibility/platform/ax_platform_node_delegate.cc
@@ -1354,4 +1354,8 @@
   return result;
 }
 
+BrowserAccessibility* AXPlatformNodeDelegate::ToBrowserAccessibility() {
+  return nullptr;
+}
+
 }  // namespace ui
diff --git a/ui/accessibility/platform/ax_platform_node_delegate.h b/ui/accessibility/platform/ax_platform_node_delegate.h
index ca2e975..67759416 100644
--- a/ui/accessibility/platform/ax_platform_node_delegate.h
+++ b/ui/accessibility/platform/ax_platform_node_delegate.h
@@ -47,6 +47,7 @@
 struct AXActionData;
 struct AXNodeData;
 struct AXTreeData;
+class BrowserAccessibility;
 class ChildIterator;
 
 using TextAttribute = std::pair<std::string, std::string>;
@@ -664,12 +665,16 @@
   }
 
  protected:
+  friend class BrowserAccessibility;
+
   explicit AXPlatformNodeDelegate(AXNode* node);
 
   virtual std::string SubtreeToStringHelper(size_t level);
 
   virtual void NotifyAccessibilityApiUsage() const {}
 
+  virtual BrowserAccessibility* ToBrowserAccessibility();
+
   AXPlatformNodeDelegate* GetParentDelegate() const;
 
   // Given a set of Blink node IDs, get their respective platform nodes and
diff --git a/ui/accessibility/platform/browser_accessibility.cc b/ui/accessibility/platform/browser_accessibility.cc
index 88fb3ce..ee13b80b 100644
--- a/ui/accessibility/platform/browser_accessibility.cc
+++ b/ui/accessibility/platform/browser_accessibility.cc
@@ -64,9 +64,10 @@
 // static
 BrowserAccessibility* BrowserAccessibility::FromAXPlatformNodeDelegate(
     AXPlatformNodeDelegate* delegate) {
-  if (!delegate || !delegate->IsWebContent())
+  if (!delegate) {
     return nullptr;
-  return static_cast<BrowserAccessibility*>(delegate);
+  }
+  return delegate->ToBrowserAccessibility();
 }
 
 BrowserAccessibility::BrowserAccessibility(BrowserAccessibilityManager* manager,
@@ -987,6 +988,10 @@
   return result;
 }
 
+BrowserAccessibility* BrowserAccessibility::ToBrowserAccessibility() {
+  return this;
+}
+
 const std::vector<gfx::NativeViewAccessible>
 BrowserAccessibility::GetUIADirectChildrenInRange(AXPlatformNodeDelegate* start,
                                                   AXPlatformNodeDelegate* end) {
diff --git a/ui/accessibility/platform/browser_accessibility.h b/ui/accessibility/platform/browser_accessibility.h
index 7aba93b3..4ea1d42 100644
--- a/ui/accessibility/platform/browser_accessibility.h
+++ b/ui/accessibility/platform/browser_accessibility.h
@@ -488,6 +488,8 @@
 
   std::string SubtreeToStringHelper(size_t level) override;
 
+  BrowserAccessibility* ToBrowserAccessibility() override;
+
   // The UIA tree formatter needs access to GetUniqueId() to identify the
   // starting point for tree dumps.
   friend class AccessibilityTreeFormatterUia;
diff --git a/ui/accessibility/platform/browser_accessibility_com_win.cc b/ui/accessibility/platform/browser_accessibility_com_win.cc
index 9e97672..1a7cf45 100644
--- a/ui/accessibility/platform/browser_accessibility_com_win.cc
+++ b/ui/accessibility/platform/browser_accessibility_com_win.cc
@@ -28,6 +28,7 @@
 #include "ui/accessibility/ax_mode.h"
 #include "ui/accessibility/ax_role_properties.h"
 #include "ui/accessibility/platform/ax_platform.h"
+#include "ui/accessibility/platform/ax_platform_node_win.h"
 #include "ui/accessibility/platform/browser_accessibility_manager_win.h"
 #include "ui/accessibility/platform/browser_accessibility_win.h"
 #include "ui/base/win/accessibility_ids_win.h"
@@ -1865,8 +1866,13 @@
     return ToBrowserAccessibilityComWin(owner->PlatformGetChild(child_id - 1));
   }
 
-  auto* child = static_cast<BrowserAccessibilityComWin*>(
-      AXPlatformNodeWin::GetFromUniqueId(-child_id));
+  auto* platform_node = AXPlatformNodeWin::GetFromUniqueId(-child_id);
+  if (!platform_node) {
+    return nullptr;
+  }
+  auto* child = ToBrowserAccessibilityComWin(
+      BrowserAccessibility::FromAXPlatformNodeDelegate(
+          platform_node->GetDelegate()));
   if (child && child->GetOwner()->IsDescendantOf(owner)) {
     return child;
   }
diff --git a/ui/accessibility/platform/browser_accessibility_manager_win.cc b/ui/accessibility/platform/browser_accessibility_manager_win.cc
index 043ec239..b2140dd 100644
--- a/ui/accessibility/platform/browser_accessibility_manager_win.cc
+++ b/ui/accessibility/platform/browser_accessibility_manager_win.cc
@@ -63,6 +63,14 @@
 constexpr char kBrowserRootViewClassName[] = "BrowserRootView";
 constexpr char kTabClassName[] = "Tab";
 
+BrowserAccessibilityComWin* ToBrowserAccessibilityComWin(AXPlatformNode* node) {
+  if (!node) {
+    return nullptr;
+  }
+  return ::ui::ToBrowserAccessibilityComWin(
+      BrowserAccessibility::FromAXPlatformNodeDelegate(node->GetDelegate()));
+}
+
 }  // namespace
 
 // static
@@ -1067,8 +1075,9 @@
   // recomputes all of win_attributes_ other than IAccessibleText.
   auto state_scan = update_states.begin();
   for (auto* node : objs_to_update) {
-    static_cast<BrowserAccessibilityComWin*>(node)
-        ->UpdateStep1ComputeWinAttributes(&*state_scan);
+    if (auto* com_win = ToBrowserAccessibilityComWin(node)) {
+      com_win->UpdateStep1ComputeWinAttributes(&*state_scan);
+    }
     ++state_scan;
   }
 
@@ -1076,8 +1085,9 @@
   // concatenation of all of its child text nodes, so it can't run until
   // the text of all of the nodes was computed in the previous step.
   for (auto* node : objs_to_update) {
-    static_cast<BrowserAccessibilityComWin*>(node)
-        ->UpdateStep2ComputeHypertext();
+    if (auto* com_win = ToBrowserAccessibilityComWin(node)) {
+      com_win->UpdateStep2ComputeHypertext();
+    }
   }
 
   // The third step fires events on nodes based on what's changed - like
@@ -1089,7 +1099,9 @@
   // At the end, it deletes old_win_attributes_ since they're not needed
   // anymore.
   for (auto* node : objs_to_update) {
-    static_cast<BrowserAccessibilityComWin*>(node)->UpdateStep3FireEvents();
+    if (auto* com_win = ToBrowserAccessibilityComWin(node)) {
+      com_win->UpdateStep3FireEvents();
+    }
   }
 }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Browser RCE via stale AXUniqueId type confusion in AXLegacyHypertext

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 logic flaw in Windows accessibility tree updates can leave a stale AXUniqueId in a parent node’s hypertext cache. By forcing the 32-bit ID counter to wrap around, a compromised renderer can cause the stale ID to point to a different internal UI node. This leads to an out-of-bounds pointer cast and a potential virtual method call on attacker-controlled heap data, resulting in a browser process sandbox escape.

Affected files:

  • ui/accessibility/platform/browser_accessibility_manager.cc
  • ui/accessibility/platform/browser_accessibility_com_win.cc
  • ui/accessibility/ax_tree.cc
  • ui/accessibility/platform/ax_unique_id.cc

Estimated timestamp from git blame: 2023-08-08

Summary

A potential type confusion vulnerability exists in the Windows accessibility subsystem. A logic flaw during accessibility tree updates can cause a parent node to retain a stale AXUniqueId in its hypertext cache. A compromised renderer can exploit this by wrapping the 32-bit global AXUniqueId counter, causing the stale ID to reference a newly allocated, incompatible internal UI node (AXPlatformNodeWin). When queried by a screen reader, an unsafe cast shifts the pointer out-of-bounds, potentially allowing Arbitrary Code Execution (RCE) in the unsandboxed Browser process.

Root Cause Analysis

The vulnerability stems from two interlocking failures when processing AXTreeUpdate IPCs:

  1. Incomplete changes List Population: In AXTree::Unserialize (ui/accessibility/ax_tree.cc), if an ignored node I loses an unignored child Y, its unignored parent X has its unignored cache updated. However, X is intentionally omitted from the changes list because of the check if (unignored_ancestor->id() != node_id).
  2. Skipped Parent Update: In BrowserAccessibilityManager::CollectChangedNodesAndParentsForAtomicUpdate, node I is processed, but its parent X is explicitly skipped if I’s role is kInlineTextBox.

Consequently, X’s hypertext_.hyperlinks cache is never cleared and retains the now-freed AXUniqueId of Y.

Later, when BrowserAccessibilityComWin::get_hyperlink is called, it retrieves the node using the stale ID. If the ID was reassigned to a Views UI node (AXPlatformNodeWin), it performs an unsafe downcast: auto* link = static_cast<BrowserAccessibilityComWin*>(node). It then casts this to an IAccessibleHyperlink*. Because BrowserAccessibilityComWin utilizes multiple inheritance and inherits from IAccessibleHyperlink while AXPlatformNodeWin does not, the C++ compiler applies a vtable offset that shifts the pointer completely out-of-bounds of the AXPlatformNodeWin object. A subsequent virtual method call on this interface uses adjacent heap data.

Potential Steps to Trigger

(Note: Our tooling agent cannot run code; these are suggested steps based on static analysis).

  1. Assume the victim is using an IA2 screen reader (e.g., NVDA) on Windows.
  2. From a compromised renderer, send an AXTreeUpdate establishing an unignored parent X, an ignored child I, and an unignored grandchild Y. X caches Y’s AXUniqueId.
  3. Send a second AXTreeUpdate that changes I’s role to kInlineTextBox, removes its ignored state, and deletes Y.
  4. Due to the logic flaws, X’s hypertext cache is not invalidated.
  5. Rapidly create and delete dummy accessibility nodes via IPC to increment the global AXUniqueId counter ~2.1 billion times until it wraps around to Y’s freed ID.
  6. Trigger a browser UI action that allocates a new Views node (AXPlatformNodeWin), which receives the stale ID.
  7. Use heap spraying techniques to control the memory immediately following this AXPlatformNodeWin allocation.
  8. Trigger an accessibility query on X (e.g., via focus). The screen reader calls get_hyperlink, receives the out-of-bounds pointer, and invokes a virtual method, hijacking control flow.

Suggested Fix

  1. Safe Casting: In BrowserAccessibilityComWin::get_hyperlink, verify the node type before casting. Check if the node belongs to web content before static_casting to BrowserAccessibilityComWin*.
  2. Invalidation Logic: Re-evaluate the kInlineTextBox optimization in CollectChangedNodesAndParentsForAtomicUpdate to ensure it doesn’t leave stale IDs in a parent’s hypertext cache. Additionally, ensure AXTree::Unserialize properly flags parents for updates when their logical unignored child list changes.

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