Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Accessibility
DescriptionUse after free in Accessibility
ComponentAccessibility
Bug ClassUAF
Tracker502978647
Fix commita72c80884d88 (chromium/src) +45/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
TEST_F
ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
modified

Files Changed

  • ui/accessibility/platform/automation/automation_tree_manager_owner.cc
  • ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
From a72c80884d88cb68fba22a552ec795f3b552f1a1 Mon Sep 17 00:00:00 2001
From: Addison Luh <aluh@chromium.org>
Date: Tue, 28 Apr 2026 18:12:04 -0700
Subject: [PATCH] Reland "Prevent invalid tree state in AutomationTreeManagerOwner"

This is a reland of commit d2ab7831eecf3a1c192ae64b05c2c6a797b556e9

Fixed tree id in the test.

Original change's description:
> Prevent invalid tree state in AutomationTreeManagerOwner
>
> If an AXTree fails to fully unserialize in the automation tree manager
> owner, drop and destroy the corrupted client wrapper and its nested
> static caches immediately. This prevents the tree manager from
> referencing the corrupted, dangling pointers subsequently.
>
> Bug: b:502978647
> Test: out/Default/accessibility_unittests --gtest_filter=AutomationTreeManagerOwnerTest.UnserializeFailureClearsCachedTree
> Change-Id: Idbcbdab0e7b7a46d8d094a38f3ba4a32cf301ce7
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793837
> Reviewed-by: David Tseng <dtseng@chromium.org>
> Auto-Submit: Addison Luh <aluh@chromium.org>
> Commit-Queue: David Tseng <dtseng@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1621369}

Cq-Include-Trybots: luci.chrome.try:linux-chromeos-chrome
Bug: b:502978647
Change-Id: I720b30a6baa76570ea54b072e1e9bc0ad6c50f0a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7802331
Auto-Submit: Addison Luh <aluh@chromium.org>
Reviewed-by: David Tseng <dtseng@chromium.org>
Commit-Queue: Addison Luh <aluh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1622148}
---

diff --git a/ui/accessibility/platform/automation/automation_tree_manager_owner.cc b/ui/accessibility/platform/automation/automation_tree_manager_owner.cc
index d6a36fce..17f0803 100644
--- a/ui/accessibility/platform/automation/automation_tree_manager_owner.cc
+++ b/ui/accessibility/platform/automation/automation_tree_manager_owner.cc
@@ -1075,6 +1075,7 @@
                                            mouse_location)) {
     DLOG(ERROR) << tree_wrapper->ax_tree()->error();
     GetAutomationV8Bindings()->SendTreeSerializationError(tree_id);
+    DestroyAccessibilityTree(tree_id);
     return;
   }
 
diff --git a/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc b/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
index a16a96d..693b5e5f 100644
--- a/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
+++ b/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
@@ -1000,4 +1000,48 @@
   EXPECT_TRUE(tree_destroyed);
 }
 
+TEST_F(AutomationTreeManagerOwnerTest, UnserializeFailureClearsCachedTree) {
+  EXPECT_TRUE(GetTreeIDToTreeMap().empty());
+
+  const AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
+  const int valid_node_id = 1;
+  const int invalid_node_id = 2;
+
+  std::vector<AXTreeUpdate> updates;
+  updates.emplace_back();
+  auto& tree_update = updates.back();
+  auto& tree_data = tree_update.tree_data;
+  tree_data.tree_id = tree_id;
+  tree_update.has_tree_data = true;
+  tree_update.root_id = valid_node_id;
+  tree_update.nodes.emplace_back();
+  auto& node_data = tree_update.nodes.back();
+  node_data.role = ax::mojom::Role::kDesktop;
+  node_data.id = valid_node_id;
+  std::vector<AXEvent> events;
+  SendAccessibilityEvents(tree_id, updates, gfx::Point(), events);
+
+  ASSERT_EQ(1U, GetTreeIDToTreeMap().size());
+
+  updates.clear();
+  updates.emplace_back();
+  auto& bad_update = updates.back();
+  bad_update.has_tree_data = true;
+  bad_update.tree_data.tree_id = tree_id;
+  bad_update.root_id = valid_node_id;
+  bad_update.nodes.emplace_back();
+  auto& bad_node_data = bad_update.nodes.back();
+  bad_node_data.id = valid_node_id;
+  // Adds a non-existent node id.
+  bad_node_data.child_ids.push_back(invalid_node_id);
+
+#if AX_FAIL_FAST_BUILD()
+  EXPECT_DEATH_IF_SUPPORTED(
+      SendAccessibilityEvents(tree_id, updates, gfx::Point(), events), "");
+#else
+  SendAccessibilityEvents(tree_id, updates, gfx::Point(), events);
+  EXPECT_TRUE(GetTreeIDToTreeMap().empty());
+#endif
+}
+
 }  // namespace ui
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc b/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
index a16a96d..693b5e5f 100644
--- a/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
+++ b/ui/accessibility/platform/automation/automation_tree_manager_owner_unittest.cc
@@ -1000,4 +1000,48 @@
   EXPECT_TRUE(tree_destroyed);
 }
 
+TEST_F(AutomationTreeManagerOwnerTest, UnserializeFailureClearsCachedTree) {
+  EXPECT_TRUE(GetTreeIDToTreeMap().empty());
+
+  const AXTreeID tree_id = AXTreeID::CreateNewAXTreeID();
+  const int valid_node_id = 1;
+  const int invalid_node_id = 2;
+
+  std::vector<AXTreeUpdate> updates;
+  updates.emplace_back();
+  auto& tree_update = updates.back();
+  auto& tree_data = tree_update.tree_data;
+  tree_data.tree_id = tree_id;
+  tree_update.has_tree_data = true;
+  tree_update.root_id = valid_node_id;
+  tree_update.nodes.emplace_back();
+  auto& node_data = tree_update.nodes.back();
+  node_data.role = ax::mojom::Role::kDesktop;
+  node_data.id = valid_node_id;
+  std::vector<AXEvent> events;
+  SendAccessibilityEvents(tree_id, updates, gfx::Point(), events);
+
+  ASSERT_EQ(1U, GetTreeIDToTreeMap().size());
+
+  updates.clear();
+  updates.emplace_back();
+  auto& bad_update = updates.back();
+  bad_update.has_tree_data = true;
+  bad_update.tree_data.tree_id = tree_id;
+  bad_update.root_id = valid_node_id;
+  bad_update.nodes.emplace_back();
+  auto& bad_node_data = bad_update.nodes.back();
+  bad_node_data.id = valid_node_id;
+  // Adds a non-existent node id.
+  bad_node_data.child_ids.push_back(invalid_node_id);
+
+#if AX_FAIL_FAST_BUILD()
+  EXPECT_DEATH_IF_SUPPORTED(
+      SendAccessibilityEvents(tree_id, updates, gfx::Point(), events), "");
+#else
+  SendAccessibilityEvents(tree_id, updates, gfx::Point(), events);
+  EXPECT_TRUE(GetTreeIDToTreeMap().empty());
+#endif
+}
+
 }  // namespace ui
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-after-free in extension renderer via AutomationAXTreeWrapper state poisoning

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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The extension renderer’s AutomationAXTreeWrapper fails to reset its internal AXTree when Unserialize fails. A compromised content renderer can send an update referencing child ID 0, poisoning the tree’s internal map. A subsequent update for the same tree ID causes a collision during node creation, resulting in a persistent Use-After-Free that bypasses MiraclePtr.

Affected files:

  • ui/accessibility/platform/automation/automation_ax_tree_wrapper.cc
  • ui/accessibility/ax_tree.cc
  • ui/accessibility/platform/automation/automation_tree_manager_owner.cc

Estimated timestamp from git blame: 2025-06-25

Summary

A potential Use-After-Free (UAF) vulnerability exists in the extension renderer process due to improper state management in AutomationAXTreeWrapper. When an accessibility tree update fails to unserialize, the wrapper maintains its corrupted AXTree state instead of resetting it. An attacker can exploit this by poisoning the tree’s node map to cause an ID collision on a subsequent update. This collision causes node creation to fail, returning a dangling pointer that is then stored persistently in the tree structure. This vulnerability bypasses MiraclePtr mitigations.

Prerequisites

This vulnerability requires an active extension utilizing the chrome.automation API (e.g., ChromeVox or Select-to-Speak on ChromeOS).

When an extension uses this API, the browser sets the accessibility mode to kAXModeWebContentsOnly, which lacks the kNativeAPIs flag. Consequently, the browser_accessibility_manager_ in the Browser process remains null. This architectural quirk causes the Browser to skip running AXTree::Unserialize during RenderFrameHostImpl::SendAccessibilityEventsToManager, effectively forwarding malicious accessibility updates from a compromised content renderer directly to the extension renderer without native validation.

Potential Exploitation Steps

(Note: These are suggested steps based on source code analysis; a working proof-of-concept has not been executed).

1. Poisoning Phase

A compromised content renderer sends an AXTreeUpdate where a node (e.g., ID 1) includes a child with ID 0 (kInvalidAXNodeID).

  • During AXTree::Unserialize, GetFromId(0) is called. Because 0 is hardcoded as invalid, GetFromId explicitly returns nullptr without checking the id_map_.
  • Assuming node 0 is new, the code calls CreateNode(0). In official production builds, SANITIZER_CHECK(id != kInvalidAXNodeID) resolves to a no-op DCHECK.
  • CreateNode(0) successfully inserts the newly allocated node into the tree’s id_map_ via try_emplace(0, ...).
  • The update eventually fails validation (e.g., in ValidatePendingChangesComplete) because no data was provided for node 0.
  • Unserialize returns false. However, AutomationAXTreeWrapper::OnAccessibilityEvents returns without clearing or resetting the ax_tree_. The tree remains poisoned with ID 0 in id_map_.

2. Collision and UAF Phase

The attacker sends a second AXTreeUpdate for the same tree_id, again referencing child ID 0.

  • Unserialize is called on the poisoned tree.
  • CreateNewChildVector calls GetFromId(0). It still explicitly returns nullptr, forcing the code to call CreateNode(0) again.
  • Inside CreateNode(0), a new AXNode is allocated in a std::unique_ptr.
  • id_map_.try_emplace(0, ...) is called. This time, the call fails because ID 0 is already present in the map from the first update.
  • Because insertion fails, the unique_ptr is not moved. The function returns the raw pointer (node_raw) to the newly created node.
  • The unique_ptr goes out of scope and the AXNode is destroyed, leaving node_raw as a dangling pointer.
  • In CreateNewChildVector, update_state->pending_node_ids.insert(child->id()); executes, immediately dereferencing the dangling pointer to read data_.id.
  • new_children->push_back(child) constructs a raw_ptr from the dangling pointer.
  • UpdateNode calls node->SwapChildren(&new_children), persistently storing the dangling pointer in the parent node’s children_ array.

MiraclePtr Bypass

This flow bypasses MiraclePtr (BackupRefPtr). At the time the AXNode is destroyed in CreateNode, it is exclusively owned by a std::unique_ptr and has no active raw_ptr references. Therefore, PartitionAlloc’s BRP refcount is 0, and the memory is freed immediately without quarantine. When new_children->push_back(child) constructs the raw_ptr, it increments the refcount from 0 to 1 on already freed memory, bypassing the mitigation and allowing a stable UAF.

Suggested Fix

  1. Prevent dangling pointer return: Modify AXTree::CreateNode so that if try_emplace fails (i.e., inserted == false), it does not return node_raw. Instead, it should return nullptr or crash, as the tree state is invalid.
  2. Reset state on failure: In AutomationAXTreeWrapper::OnAccessibilityEvents, if ax_tree_->Unserialize(update) returns false, the ax_tree_ instance should be completely reset or destroyed. The tree is in an untrusted, partially updated state and should not be reused for subsequent updates.
  3. Strengthen ID validation: Ensure that AXTree::Unserialize strictly rejects updates containing kInvalidAXNodeID across all build configurations, not just when AX_FAIL_FAST_BUILD() is enabled.

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


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.

Raised in root component due to access or custom field issues on 1279373

View on issue tracker