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.
Tracker498885920
Fix commitfa454ea8264a (chromium/src) +43/-9
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-02 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

Background

`AXPlatformNodeDelegate`
The base interface for accessibility platform nodes, whose subclasses include both BrowserAccessibility (web content) and non-web platform nodes such as views/UI nodes.
`BrowserAccessibility`
The web-content accessibility node type that Windows code expects to cast platform nodes to before treating them as BrowserAccessibilityComWin.
`BrowserAccessibilityComWin`
The Windows COM (IAccessible) wrapper subclass of a BrowserAccessibility, on which UpdateStep1/2/3 and child-lookup methods operate.
`IsWebContent()`
A virtual predicate previously used to decide whether an AXPlatformNodeDelegate was safe to static_cast to BrowserAccessibility.

Root Cause Analysis

The Windows accessibility code performed unchecked static_cast<BrowserAccessibilityComWin*> on AXPlatformNode/AXPlatformNodeDelegate pointers whose dynamic type was not verified to be a web-content BrowserAccessibility. In BrowserAccessibilityManagerWin::OnAtomicUpdateFinished() each node in objs_to_update was cast directly, and BrowserAccessibilityComWin::GetTargetFromChildID() cast the result of AXPlatformNodeWin::GetFromUniqueId(-child_id) — a lookup that can return a non-web platform node. The prior gatekeeper, BrowserAccessibility::FromAXPlatformNodeDelegate(), relied on IsWebContent() plus a static_cast, which is unsound because IsWebContent() returning true does not guarantee the object is actually a BrowserAccessibility.

The fix adds a virtual ToBrowserAccessibility() that returns this only on BrowserAccessibility and nullptr on the base delegate, routes casts through it, and null-checks the results before use, so a mismatched type yields nullptr instead of a type-confused pointer.

Key insight
The core mistake was using a boolean predicate (IsWebContent()) plus an unconditional static_cast to identify a subclass, which does not actually establish the object’s dynamic type; the fix replaces it with a virtual ToBrowserAccessibility() downcast that returns nullptr for non-BrowserAccessibility delegates.

Attack Path

  1. Introduce a non-web platform node Cause a node whose delegate is an AXPlatformNodeDelegate that is not a BrowserAccessibility (or whose IsWebContent() is unreliable) to be referenced by the Windows accessibility manager or reachable via a child ID.
  2. Reach the unchecked cast Trigger OnAtomicUpdateFinished() over objs_to_update, or call GetTargetFromChildID() so GetFromUniqueId(-child_id) returns that node, where the old code did static_cast<BrowserAccessibilityComWin*>.
  3. Type confusion The pointer is treated as a BrowserAccessibilityComWin and its methods (UpdateStep1ComputeWinAttributes, UpdateStep2ComputeHypertext, UpdateStep3FireEvents, GetOwner) run against an object of the wrong type. (inferred) memory corruption or invalid dereference follows from operating on the mismatched layout.

Impact Assessment

An attacker able to influence the accessibility tree on Windows can cause type confusion in the browser process by driving a non-BrowserAccessibility platform node through code that assumes BrowserAccessibilityComWin. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any progression to a controlled read/write or code execution is (inferred).

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.h
  • ui/accessibility/platform/ax_platform_node_delegate.cc
  • ui/accessibility/platform/browser_accessibility.cc
  • ui/accessibility/platform/browser_accessibility_com_win.cc
  • ui/accessibility/platform/browser_accessibility.h
  • ui/accessibility/platform/browser_accessibility_manager_win.cc

Audit Directions

  • Predicate-then-cast idioms
    Look for IsX()/IsWebContent()-style booleans followed by unconditional static_cast to a subclass; prefer virtual downcast methods returning nullptr on mismatch.
  • Unique-ID lookups
    Audit GetFromUniqueId/registry lookups that return base-class pointers later cast to platform-specific subclasses without verifying dynamic type.
  • Accessibility update loops
    Review batch update paths (objs_to_update, atomic-update finishers) that assume every element is the platform COM subclass and dereference without a converted-null guard.
From fa454ea8264a3a10c1992a370183b91037d0c347 Mon Sep 17 00:00:00 2001
From: Peter K <[email protected]>
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 <[email protected]>
Commit-Queue: Peter Kotwicz <[email protected]>
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 [email protected]

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