High watch CVSS 7.5 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Accessibility
DescriptionUse after free in Accessibility
ComponentAccessibility
Bug ClassUAF
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.
Tracker502978647
Fix commita72c80884d88 (chromium/src) +45/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-15 (per vendor advisory)
Fix landed2026-04-29 (public git)
Disclosed2026-05-12
Patch gap13 days — fix public in source before the advisory

Background

`AutomationTreeManagerOwner`
Owns and manages accessibility tree wrappers keyed by AXTreeID, exposing them to the automation/V8 bindings used by extensions and ChromeVox.
`AXTree` unserialization
Applying an AXTreeUpdate to an existing tree; it can fail partway (e.g., an update references a non-existent node id), leaving the tree in a corrupted, partially-mutated state.
Client wrapper / static caches
The per-tree wrapper and its nested static caches hold pointers into tree nodes; if the tree is corrupted, those pointers can dangle.
`DestroyAccessibilityTree` / `GetTreeIDToTreeMap`
The call that drops and destroys a tree wrapper, and the map of live tree wrappers the manager consults on subsequent events.

Root Cause Analysis

In the serialization/event-handling path (the enclosing function name is not shown in the @@ -1075 hunk header), when an AXTree fails to fully unserialize the code logged the error and called GetAutomationV8Bindings()->SendTreeSerializationError(tree_id) and then simply returned, leaving the corrupted tree_wrapper and its nested static caches in GetTreeIDToTreeMap(). Because the failed update may have partially mutated the tree and left caches pointing at nodes that no longer exist, any subsequent event or lookup for that tree_id would dereference the corrupted, dangling pointers — a use-after-free.

The fix adds DestroyAccessibilityTree(tree_id) immediately before the return, dropping and destroying the corrupted wrapper and its caches so the manager can no longer reference them. The regression test UnserializeFailureClearsCachedTree confirms the invariant: after a bad update whose node adds a non-existent child_id, GetTreeIDToTreeMap() must be empty (or, in AX_FAIL_FAST_BUILD, the operation death-tests).

Key insight
The single mistake was leaving a partially-unserialized (corrupted) tree wrapper and its caches registered after an unserialize failure instead of destroying them, so later accesses used dangling pointers; the fix calls DestroyAccessibilityTree(tree_id) on the failure path.

Attack Path

  1. Send a malformed tree update An attacker-influenced source of accessibility updates delivers an AXTreeUpdate whose node references a non-existent node id, as the regression test’s bad_node_data.child_ids.push_back(invalid_node_id) shows.
  2. Unserialize fails, tree left cached Unserialization fails; pre-patch the code sends a serialization error and returns, leaving the corrupted wrapper and its static caches in GetTreeIDToTreeMap().
  3. Subsequent access dereferences dangling state A later accessibility event or lookup for the same tree_id reaches the corrupted wrapper/caches whose pointers dangle, a use-after-free.
  4. Corruption/control (inferred) Leveraging the UAF for disclosure or control is (inferred); the diff demonstrates the dangling-state condition and its removal, not an exploit primitive.

Impact Assessment

An attacker able to feed a malformed accessibility tree update could leave AutomationTreeManagerOwner holding a corrupted tree whose later use is a use-after-free, which the advisory classifies as “Use after free in Accessibility” at high severity. The effect is in the process hosting the automation tree manager (e.g., browser/renderer accessibility, relevant to ChromeOS/extension automation); the precondition is reaching the unserialize-failure path, and any escalation beyond the UAF is (inferred). The advisory states no exploited-in-the-wild status.

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

Audit Directions

  • Error paths that log-and-return
    Audit failure branches (especially after “send error” notifications) that leave a half-constructed/half-mutated object registered in a long-lived map; they should tear the object down before returning.
  • Partial-unserialize / partial-apply state
    Wherever an update can fail midway (tree/DOM/graph deserialization), verify the target is either rolled back or destroyed, never left cached in a corrupted state.
  • Caches pointing into mutable trees
    Look for static/nested caches holding raw pointers into nodes of a tree that can be corrupted or resized; confirm they are invalidated whenever the backing tree enters an error state.
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