CVE-2026-79175
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BrowserAccessibilityui/accessibility/platform/ax_platform_node_delegate.h |
modified | |
ChildIteratorui/accessibility/platform/ax_platform_node_delegate.h |
modified | |
ifui/accessibility/platform/browser_accessibility.cc |
modified | |
ifui/accessibility/platform/browser_accessibility_com_win.cc |
modified | |
ifui/accessibility/platform/browser_accessibility_manager_win.cc |
modified | |
forui/accessibility/platform/browser_accessibility_manager_win.cc |
modified |
Files Changed
ui/accessibility/platform/ax_platform_node_delegate.ccui/accessibility/platform/ax_platform_node_delegate.hui/accessibility/platform/browser_accessibility.ccui/accessibility/platform/browser_accessibility.hui/accessibility/platform/browser_accessibility_com_win.ccui/accessibility/platform/browser_accessibility_manager_win.cc
Patch
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(); + } } }
Original Bug Report
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.ccui/accessibility/platform/browser_accessibility_com_win.ccui/accessibility/ax_tree.ccui/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:
- Incomplete
changesList Population: InAXTree::Unserialize(ui/accessibility/ax_tree.cc), if an ignored nodeIloses an unignored childY, its unignored parentXhas its unignored cache updated. However,Xis intentionally omitted from thechangeslist because of the checkif (unignored_ancestor->id() != node_id). - Skipped Parent Update: In
BrowserAccessibilityManager::CollectChangedNodesAndParentsForAtomicUpdate, nodeIis processed, but its parentXis explicitly skipped ifI’s role iskInlineTextBox.
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).
- Assume the victim is using an IA2 screen reader (e.g., NVDA) on Windows.
- From a compromised renderer, send an
AXTreeUpdateestablishing an unignored parentX, an ignored childI, and an unignored grandchildY.XcachesY’sAXUniqueId. - Send a second
AXTreeUpdatethat changesI’s role tokInlineTextBox, removes itsignoredstate, and deletesY. - Due to the logic flaws,
X’s hypertext cache is not invalidated. - Rapidly create and delete dummy accessibility nodes via IPC to increment the global
AXUniqueIdcounter ~2.1 billion times until it wraps around toY’s freed ID. - Trigger a browser UI action that allocates a new Views node (
AXPlatformNodeWin), which receives the stale ID. - Use heap spraying techniques to control the memory immediately following this
AXPlatformNodeWinallocation. - Trigger an accessibility query on
X(e.g., via focus). The screen reader callsget_hyperlink, receives the out-of-bounds pointer, and invokes a virtual method, hijacking control flow.
Suggested Fix
- Safe Casting: In
BrowserAccessibilityComWin::get_hyperlink, verify the node type before casting. Check if the node belongs to web content beforestatic_casting toBrowserAccessibilityComWin*. - Invalidation Logic: Re-evaluate the
kInlineTextBoxoptimization inCollectChangedNodesAndParentsForAtomicUpdateto ensure it doesn’t leave stale IDs in a parent’s hypertext cache. Additionally, ensureAXTree::Unserializeproperly 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.