Medium watch CVSS 4.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in Accessibility
DescriptionIncorrect reference resolution in Accessibility
ComponentAccessibility
Sub-componentA11y
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker513135531
Fix commit551ccd1418a1 (chromium/src) +79/-28
CWECWE-706
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-07-30 (public git)
Disclosed2026-09-08
Patch gap40 days — fix public in source before the advisory

Background

`BrowserAccessibilityCocoa`
the Objective-C NSAccessibility wrapper object that exposes a Chrome ui::BrowserAccessibility node to macOS AppKit / VoiceOver.
`AXNodeID` (`_owner->GetId()`)
the accessibility node identifier that is only guaranteed unique within a single accessibility tree (one frame/document), not across trees.
`AXPlatformNodeId` / `GetUniqueId()`
a globally-unique, process-wide platform node identifier (backed by AXUniqueId) that distinguishes every wrapper regardless of which tree it belongs to.
`-isEqual:` / `-hash` contract
Cocoa requires that equal objects share a hash and that an object’s -hash stay constant while it is held in a hash-keyed collection (NSDictionary, NSSet).

Root Cause Analysis

Before the patch, -[BrowserAccessibilityCocoa hash] returned _owner->GetId() (the per-tree AXNodeID) while the wrapper was active, and fell back to [super hash] once the wrapper was detached (![self instanceActive]), while -isEqual: was defined purely as hash equality ([self hash] == [object hash]). Because AXNodeID is only unique within one tree, two wrappers backing nodes in different frames/trees that happened to share the same per-tree id would report identical hashes and therefore compare equal, violating object identity. Worse, an object’s hash was not stable across its lifetime: it changed from _owner->GetId() to [super hash] at detach, so any AppKit collection that had keyed the wrapper by its earlier hash could no longer locate it.

The fix caches a globally-unique _uniqueId (GetUniqueId()) at construction time and uses it for both -hash and -isEqual:, and rewrites -isEqual: to first type-check via base::apple::ObjCCast<BrowserAccessibilityCocoa> and then compare the two cached _uniqueId values directly. This restores a stable, per-wrapper, cross-tree-unique identity that survives detachment, since the id was copied out of _owner before that pointer could dangle.

Key insight
The wrapper derived its identity from a tree-local id (AXNodeID) that was neither globally unique nor stable across the object’s lifetime; caching the process-wide GetUniqueId() at init and comparing that cached value fixes both the cross-tree collision and the mid-life hash change.

Attack Path

  1. Enable accessibility with multiple trees With assistive technology active, Chrome builds a BrowserAccessibilityCocoa wrapper per node, and a page with multiple frames/documents produces several independent accessibility trees whose per-tree AXNodeID values overlap.
  2. Force a per-tree id collision Arrange for a node in one tree to share the same AXNodeID as a node in another tree, so their wrappers produce identical -hash values and -isEqual: reports the two distinct wrappers as equal.
  3. Trigger incorrect reference resolution When AppKit or Chrome code looks a wrapper up (e.g. in a hash-keyed collection or an equality comparison), the collision causes the wrong accessibility node to be resolved, or a stored wrapper to become unfindable after its hash changes at detach.
  4. Consequence The mismatched/stale identity yields use of the wrong or a detached wrapper object, whose _owner may be a dangling raw_ptr after teardown (memory-safety corruption beyond incorrect resolution is (inferred); the patch demonstrates only the identity/logic error).

Impact Assessment

An attacker who can drive the accessibility tree (any web content, given accessibility is enabled) can cause distinct nodes from different frames to be treated as the same object and cause a wrapper’s identity to change mid-lifetime, leading to incorrect reference resolution in the browser process’s accessibility layer. The preconditions are that assistive technology / accessibility is active on macOS and that colliding per-tree ids can be arranged across trees. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.

Changed Functions

FunctionChangeNotes
AXNodeIdDelegate
ui/accessibility/platform/ax_platform_node_id.h
modified
if
ui/accessibility/platform/test_ax_node_id_delegate.cc
modified
TestAXNodeIdDelegate
ui/accessibility/platform/test_ax_node_id_delegate.h
modified
TEST_F
ui/accessibility/platform/browser_accessibility_mac_unittest.mm
modified

Files Changed

  • ui/accessibility/platform/test_ax_node_id_delegate.h
  • ui/accessibility/platform/browser_accessibility_mac_unittest.mm
  • ui/accessibility/platform/test_ax_node_id_delegate.cc
  • ui/accessibility/platform/ax_platform_node_id.h
  • ui/accessibility/platform/browser_accessibility_cocoa.mm
  • ui/accessibility/platform/ax_node_id_delegate.h

Audit Directions

  • Identity from tree-local ids
    Audit any platform accessibility wrapper (Windows IAccessible2, Linux ATK/AT-SPI) that derives -hash/equality/lookup keys from AXNodeID rather than the process-wide GetUniqueId(), since per-tree ids collide across frames.
  • Hash/identity changing mid-lifetime
    Look for -hash/hashCode/equality implementations that branch on liveness state (e.g. instanceActive, detach, dangling owner) and thus return different values over an object’s lifetime while it may sit in a hash-keyed container.
  • Identity through a dangling `raw_ptr`
    Flag comparison or hashing code that dereferences a DanglingUntriaged/raw_ptr owner (like _owner) instead of a value cached at construction, as it both changes and risks use-after-free after teardown.
From 551ccd1418a14d9f4a4dc1a52da82cc8f8e99ffa Mon Sep 17 00:00:00 2001
From: Avi Drissman <[email protected]>
Date: Thu, 30 Jul 2026 08:05:00 -0700
Subject: [PATCH] [a11y][mac] Give BrowserAccessibilityCocoa stable identity

-isEqual: was implemented as hash equality, and -hash returned the
per-tree AXNodeID while the wrapper was active and [super hash] once
detached. AXNodeID is only unique within a single tree, so wrappers from
different frames could compare equal, and -hash changed mid-lifetime for
objects that AppKit may still be holding in hash-keyed collections.

Fix this by using the unique id of the nodes, and caching it.

Fixed: 513135531
Change-Id: Ie1c4136a3dcdac0b3d7b0517e04941346a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8156962
Commit-Queue: Avi Drissman <[email protected]>
Auto-Submit: Avi Drissman <[email protected]>
Reviewed-by: David Tseng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1671071}
---
diff --git a/ui/accessibility/platform/ax_node_id_delegate.h b/ui/accessibility/platform/ax_node_id_delegate.h
index e05d51b..70b5abe 100644
--- a/ui/accessibility/platform/ax_node_id_delegate.h
+++ b/ui/accessibility/platform/ax_node_id_delegate.h
@@ -28,13 +28,7 @@
   virtual void OnAXNodeDeleted(AXNodeID ax_node_id) = 0;
 
  protected:
-  using PassKey = base::PassKey<AXNodeIdDelegate>;
-
   AXNodeIdDelegate() = default;
-
-  // Returns a PassKey to be used by implementations so that they may create
-  // AXPlatformNodeId instances with values of their own choosing.
-  static constexpr PassKey MakePassKey() { return PassKey(); }
 };
 
 }  // namespace ui
diff --git a/ui/accessibility/platform/ax_platform_node_id.h b/ui/accessibility/platform/ax_platform_node_id.h
index e62e936..1ab7121 100644
--- a/ui/accessibility/platform/ax_platform_node_id.h
+++ b/ui/accessibility/platform/ax_platform_node_id.h
@@ -12,8 +12,6 @@
 
 namespace ui {
 
-class AXNodeIdDelegate;
-
 // The underlying type of a uniquely-generated accessibility node identifier for
 // use with the platform. The actual id store can only a accommodate 32 bit
 // integers on Windows, because IAccessible2 uses LONG for get_uniqueID() and
@@ -37,12 +35,6 @@
   constexpr AXPlatformNodeId()
       : base::StrongAlias<class AXPlatformNodeIdTag, int32_t>::StrongAlias(0) {}
 
-  // Allow implementations of AXNodeIdDelegate to create instances, as they are
-  // responsible for per-window allocation of unique identifiers.
-  constexpr explicit AXPlatformNodeId(base::PassKey<AXNodeIdDelegate>,
-                                      int32_t v)
-      : base::StrongAlias<class AXPlatformNodeIdTag, int32_t>::StrongAlias(v) {}
-
   // Allow implicit conversion to the platform's node id type.
   constexpr operator const int32_t&() const { return value_; }
 
diff --git a/ui/accessibility/platform/browser_accessibility_cocoa.mm b/ui/accessibility/platform/browser_accessibility_cocoa.mm
index 03984bef..03d484e7 100644
--- a/ui/accessibility/platform/browser_accessibility_cocoa.mm
+++ b/ui/accessibility/platform/browser_accessibility_cocoa.mm
@@ -495,12 +495,19 @@
 @end
 
 @implementation BrowserAccessibilityCocoa {
-  // Dangling pointer https://crbug.com/1475830.
+  // Dangling pointer https://crbug.com/40928052.
   raw_ptr<ui::BrowserAccessibility, DanglingUntriaged> _owner;
+
+  // The global unique ID of the `_owner`. Cached because this value might be
+  // needed for comparisons/hashing after the owner value is reset.
+  ui::AXPlatformNodeId _uniqueId;
+
   // An array of children of this object. Cached to avoid re-computing.
   NSMutableArray* __strong _children;
+
   // 1-byte bitfield bundle. Main thread only (AppKit / NSAccessibility).
   BrowserAccessibilityCocoaBitfields _bitfields;
+
   // Stores the previous value of an edit field.
   std::u16string _oldValue;
 }
@@ -560,6 +567,7 @@
               withPlatformNode:(ui::AXPlatformNodeMac*)platform_node {
   if ((self = [super initWithNode:platform_node])) {
     _owner = accessibility;
+    _uniqueId = static_cast<ui::AXPlatformNodeDelegate*>(_owner)->GetUniqueId();
     _bitfields.gettingChildren = 0;
     _bitfields.emptyGroupCache = kEmptyGroupCacheUnknown;
   }
@@ -3556,16 +3564,16 @@
 }
 
 - (BOOL)isEqual:(id)object {
-  if (![object isKindOfClass:[BrowserAccessibilityCocoa class]])
-    return NO;
-  return ([self hash] == [object hash]);
+  if (BrowserAccessibilityCocoa* objectAccessibility =
+          base::apple::ObjCCast<BrowserAccessibilityCocoa>(object)) {
+    return _uniqueId == objectAccessibility->_uniqueId;
+  }
+
+  return NO;
 }
 
 - (NSUInteger)hash {
-  // Potentially called during dealloc.
-  if (![self instanceActive])
-    return [super hash];
-  return _owner->GetId();
+  return _uniqueId;
 }
 
 - (BOOL)accessibilityNotifiesWhenDestroyed {
diff --git a/ui/accessibility/platform/test_ax_node_id_delegate.cc b/ui/accessibility/platform/test_ax_node_id_delegate.cc
index 39e1a1b..e2d609f 100644
--- a/ui/accessibility/platform/test_ax_node_id_delegate.cc
+++ b/ui/accessibility/platform/test_ax_node_id_delegate.cc
@@ -6,10 +6,21 @@
 
 namespace ui {
 
-AXPlatformNodeId TestAXNodeIdDelegate::GetOrCreateAXNodeUniqueId(
-    AXNodeID ax_node_id) {
-  // Per-tab uniqueness is not necessary in tests, so return the blink node id.
-  return AXPlatformNodeId(MakePassKey(), ax_node_id);
+TestAXNodeIdDelegate::TestAXNodeIdDelegate() = default;
+TestAXNodeIdDelegate::~TestAXNodeIdDelegate() = default;
+
+ui::AXPlatformNodeId TestAXNodeIdDelegate::GetOrCreateAXNodeUniqueId(
+    ui::AXNodeID ax_node_id) {
+  auto [iter, inserted] =
+      ax_unique_ids_.try_emplace(ax_node_id, ui::AXUniqueId::CreateInvalid());
+  if (inserted) {
+    iter->second = ui::AXUniqueId::Create();
+  }
+  return iter->second;
+}
+
+void TestAXNodeIdDelegate::OnAXNodeDeleted(ui::AXNodeID ax_node_id) {
+  ax_unique_ids_.erase(ax_node_id);
 }
 
 }  // namespace ui
diff --git a/ui/accessibility/platform/test_ax_node_id_delegate.h b/ui/accessibility/platform/test_ax_node_id_delegate.h
index d3c5d25..6635f58f 100644
--- a/ui/accessibility/platform/test_ax_node_id_delegate.h
+++ b/ui/accessibility/platform/test_ax_node_id_delegate.h
@@ -5,17 +5,23 @@
 #ifndef UI_ACCESSIBILITY_PLATFORM_TEST_AX_NODE_ID_DELEGATE_H_
 #define UI_ACCESSIBILITY_PLATFORM_TEST_AX_NODE_ID_DELEGATE_H_
 
+#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
 #include "ui/accessibility/platform/ax_node_id_delegate.h"
+#include "ui/accessibility/platform/ax_unique_id.h"
 
 namespace ui {
 
 class TestAXNodeIdDelegate : public AXNodeIdDelegate {
  public:
-  TestAXNodeIdDelegate() = default;
+  TestAXNodeIdDelegate();
+  ~TestAXNodeIdDelegate() override;
 
   // AXNodeIdDelegate:
   AXPlatformNodeId GetOrCreateAXNodeUniqueId(AXNodeID ax_node_id) override;
-  void OnAXNodeDeleted(AXNodeID ax_node_id) override {}
+  void OnAXNodeDeleted(AXNodeID ax_node_id) override;
+
+ private:
+  absl::flat_hash_map<ui::AXNodeID, ui::AXUniqueId> ax_unique_ids_;
 };
 
 }  // namespace ui
diff --git a/ui/accessibility/platform/browser_accessibility_mac_unittest.mm b/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
index c11015c..2cd7ef28 100644
--- a/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
+++ b/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
@@ -234,6 +234,46 @@
   EXPECT_NSEQ(nil, retainedFirstChild.accessibilityLabel);
 }
 
+// AppKit may retain a wrapper and key it in a hash-based collection, so
+// -hash must remain stable for the lifetime of the wrapper, and distinct
+// wrappers must never compare equal even if their backing nodes happen to
+// share the same per-tree id.
+TEST_F(BrowserAccessibilityMacTest, IdentityIsPerWrapperAndStable) {
+  // Build a second, independent tree whose root re-uses the same per-tree id
+  // as a node in the fixture's tree.
+  AXNodeData other_root;
+  other_root.id = 1000;
+  other_root.role = ax::mojom::Role::kRootWebArea;
+  TestAXNodeIdDelegate other_node_id_delegate;
+  std::unique_ptr<BrowserAccessibilityManager> other_manager =
+      std::make_unique<BrowserAccessibilityManagerMac>(
+          MakeAXTreeUpdateForTesting(other_root), other_node_id_delegate,
+          nullptr);
+  BrowserAccessibilityCocoa* other_wrapper =
+      base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>(
+          other_manager->GetBrowserAccessibilityRoot()
+              ->GetNativeViewAccessible()
+              .Get());
+
+  ASSERT_NE(accessibility_, other_wrapper);
+  EXPECT_FALSE([accessibility_ isEqual:other_wrapper]);
+  EXPECT_FALSE([other_wrapper isEqual:accessibility_]);
+
+  // Hold the wrapper past detach, as the system might.
+  NS_VALID_UNTIL_END_OF_SCOPE BrowserAccessibilityCocoa* retained =
+      accessibility_;
+  const NSUInteger hash_before = retained.hash;
+  EXPECT_TRUE([retained isEqual:retained]);
+
+  // Tearing down the manager detaches the wrapper.
+  manager_.reset();
+  ASSERT_FALSE([retained instanceActive]);
+
+  EXPECT_EQ(hash_before, retained.hash);
+  EXPECT_TRUE([retained isEqual:retained]);
+  EXPECT_FALSE([retained isEqual:other_wrapper]);
+}
+
 TEST_F(BrowserAccessibilityMacTest, TestComputeTextEdit) {
   root_ = AXNodeData();
   root_.id = 1;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/accessibility/platform/browser_accessibility_mac_unittest.mm b/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
index c11015c..2cd7ef28 100644
--- a/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
+++ b/ui/accessibility/platform/browser_accessibility_mac_unittest.mm
@@ -234,6 +234,46 @@
   EXPECT_NSEQ(nil, retainedFirstChild.accessibilityLabel);
 }
 
+// AppKit may retain a wrapper and key it in a hash-based collection, so
+// -hash must remain stable for the lifetime of the wrapper, and distinct
+// wrappers must never compare equal even if their backing nodes happen to
+// share the same per-tree id.
+TEST_F(BrowserAccessibilityMacTest, IdentityIsPerWrapperAndStable) {
+  // Build a second, independent tree whose root re-uses the same per-tree id
+  // as a node in the fixture's tree.
+  AXNodeData other_root;
+  other_root.id = 1000;
+  other_root.role = ax::mojom::Role::kRootWebArea;
+  TestAXNodeIdDelegate other_node_id_delegate;
+  std::unique_ptr<BrowserAccessibilityManager> other_manager =
+      std::make_unique<BrowserAccessibilityManagerMac>(
+          MakeAXTreeUpdateForTesting(other_root), other_node_id_delegate,
+          nullptr);
+  BrowserAccessibilityCocoa* other_wrapper =
+      base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>(
+          other_manager->GetBrowserAccessibilityRoot()
+              ->GetNativeViewAccessible()
+              .Get());
+
+  ASSERT_NE(accessibility_, other_wrapper);
+  EXPECT_FALSE([accessibility_ isEqual:other_wrapper]);
+  EXPECT_FALSE([other_wrapper isEqual:accessibility_]);
+
+  // Hold the wrapper past detach, as the system might.
+  NS_VALID_UNTIL_END_OF_SCOPE BrowserAccessibilityCocoa* retained =
+      accessibility_;
+  const NSUInteger hash_before = retained.hash;
+  EXPECT_TRUE([retained isEqual:retained]);
+
+  // Tearing down the manager detaches the wrapper.
+  manager_.reset();
+  ASSERT_FALSE([retained instanceActive]);
+
+  EXPECT_EQ(hash_before, retained.hash);
+  EXPECT_TRUE([retained isEqual:retained]);
+  EXPECT_FALSE([retained isEqual:other_wrapper]);
+}
+
 TEST_F(BrowserAccessibilityMacTest, TestComputeTextEdit) {
   root_ = AXNodeData();
   root_.id = 1;
Loading diff…

Original Bug Report

reported by [email protected]

Potential NSObject contract violation in BrowserAccessibilityCocoa leads to memory leaks

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: BrowserAccessibilityCocoa incorrectly implements isEqual: and hash using non-unique renderer-supplied IDs. This leads to hash instability when objects are detached and potential cross-origin element collisions in macOS accessibility collections.

Affected files:

  • ui/accessibility/platform/browser_accessibility_cocoa.mm
  • ui/accessibility/platform/ax_platform_node_cocoa.mm

Estimated timestamp from git blame: 2010-07-07

Description

The implementation of -[BrowserAccessibilityCocoa isEqual:] and -[BrowserAccessibilityCocoa hash] in ui/accessibility/platform/browser_accessibility_cocoa.mm violates the fundamental contract for NSObject identity. This results in two primary issues: hash instability during object lifecycle transitions and cross-origin element collisions in the browser process.

1. Hash Mutation After Detach (Memory Leak)

When an accessibility node is removed or a navigation occurs, the native wrapper is detached. The detachAndNotifyDestroyed: method nulls the internal pointers used to identify the node.

In ui/accessibility/platform/browser_accessibility_cocoa.mm:

- (NSUInteger)hash {
  if (![self instanceActive])
    return [super hash];
  return _owner->GetId();
}

Once the object is detached, [self instanceActive] returns NO, causing the hash to transition from the node’s ID to a pointer-based hash ([super hash]). If this object is currently stored as a key in an NSDictionary or within an NSSet used by AppKit or assistive tools (like VoiceOver), the collection will be unable to locate the object to remove it after the hash changes. This leads to a persistent memory leak in the browser process that can be triggered by a malicious or malfunctioning renderer.

2. Cross-Tree ID Collisions (Information Confusion)

The current implementation uses _owner->GetId(), which returns an AXNodeID. These IDs are only unique within a single accessibility tree (frame). Consequently, nodes in different frames (potentially from different origins) often share the same ID (e.g., multiple frames having a node with id=1).

Because isEqual: is implemented solely by comparing these hashes:

- (BOOL)isEqual:(id)object {
  if (![object isKindOfClass:[BrowserAccessibilityCocoa class]])
    return NO;
  return ([self hash] == [object hash]);
}

Two distinct BrowserAccessibilityCocoa wrappers representing elements in different origins will be considered equal. If a system collection caches these objects, an element from a victim origin could be erroneously substituted for one from an attacker-controlled origin.

Potential Attack Steps

  1. An attacker-controlled renderer creates an accessibility tree with predictable node IDs (e.g., beginning at 1).
  2. The attacker embeds a cross-origin iframe (the victim) which also contains nodes starting with the same IDs.
  3. The macOS accessibility architecture (VoiceOver) queries and caches elements from both frames in a hash-based collection.
  4. Due to the collision in isEqual: and hash, the collection may return the attacker’s element when the victim’s element is requested, leading to UI spoofing or action redirection.
  5. The attacker navigates the frame, triggering a detach. The hash of the cached objects changes, causing them to become ‘stuck’ in the system’s accessibility cache, leaking memory in the browser process.

Recommendation

BrowserAccessibilityCocoa should be modified to use GetUniqueId() instead of GetId(). The AXPlatformNodeId returned by GetUniqueId() is guaranteed to be unique within the browser process and remains stable throughout the wrapper’s lifetime, even after the backing BrowserAccessibility object is detached.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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