Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ReadingMode
DescriptionUse after free in ReadingMode
ComponentReadingMode
Bug ClassUAF
Tracker498277368
Fix commitb30d69ffbdea (chromium/src) +8/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
modified

Files Changed

  • chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
From b30d69ffbdea747bd420ac064c2bbbd7ac4a1951 Mon Sep 17 00:00:00 2001
From: Lauren Winston <[email protected]>
Date: Mon, 06 Apr 2026 11:25:54 -0700
Subject: [PATCH] Return early and destroy the tree if Unserialize returns false.

This means that there is invalid state and the tree should
be destroyed.

Fixed: 498277368
Change-Id: Ie9c4045a87453fc23d1ae226a1d1e69f638a2e6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728849
Reviewed-by: Eitan Goldberger <[email protected]>
Commit-Queue: Lauren Winston <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1610371}
---

diff --git a/chrome/renderer/accessibility/read_anything/read_anything_app_model.cc b/chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
index 1ae4917..6475ce9 100644
--- a/chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
+++ b/chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
@@ -573,7 +573,14 @@
       VLOG(1) << "Unserializing an update with a known tree ID: "
               << update.tree_data.tree_id;
     }
-    tree->Unserialize(update);
+    // If tree->Unserialize returns false, there is invalid state and the tree
+    // should be destroyed.
+    const bool unserialized = tree->Unserialize(update);
+    DUMP_WILL_BE_CHECK(unserialized);
+    if (!unserialized) {
+      OnAXTreeDestroyed(tree_id);
+      return;
+    }
   }
 
   // Set URL info if it hasn't already been set.
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in Read Anything WebUI via unchecked AXTree::Unserialize

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: ReadAnythingAppModel::UnserializeUpdates ignores the return value of AXTree::Unserialize. A compromised renderer can send malformed accessibility updates with node ID 0 to force an ID collision, resulting in a dangling pointer being stored in the tree, bypassing MiraclePtr.

Affected files:

  • chrome/renderer/accessibility/read_anything/read_anything_app_model.cc
  • ui/accessibility/ax_tree.cc

Estimated timestamp from git blame: 2025-06-25

Summary

A potential Use-After-Free (UAF) vulnerability exists in the Read Anything WebUI renderer. ReadAnythingAppModel::UnserializeUpdates fails to check the return value of ui::AXTree::Unserialize(). A compromised content renderer can exploit this by sending a sequence of malformed AXTreeUpdate messages containing the invalid node ID 0 (ui::kInvalidAXNodeID). This triggers an ID collision in AXTree::CreateNode, causing it to return a raw pointer to a node that is immediately destroyed. The dangling pointer is stored in the tree, leading to a UAF that could be leveraged for Remote Code Execution (RCE) in the privileged WebUI process.

Technical Details

  1. Unchecked Return Value: In ReadAnythingAppModel::UnserializeUpdates, the return value of tree->Unserialize(update) is ignored. If an update fails mid-way, the AXTree is left in a partially updated, inconsistent state.
  2. Invalid ID Bypass: An attacker sends an update specifying a child with id: 0. In AXTree::CreateNode(0), SANITIZER_CHECK(id != kInvalidAXNodeID) is used. In official release builds, SANITIZER_CHECK is a no-op, allowing node ID 0 to be processed and inserted into id_map_[0].
  3. ID Collision and Dangling Pointer: The attacker sends a second update referencing id: 0. AXTree::GetFromId(0) has a hardcoded fast-path that returns nullptr for kInvalidAXNodeID, bypassing the id_map_. Thus, the tree believes node 0 does not exist and calls CreateNode(0) again.
  4. Inside CreateNode(0), a new AXNode is allocated via std::make_unique. It attempts to insert it using id_map_.try_emplace(0, std::move(node)). Since 0 is already in the map from the first update, try_emplace fails. Under C++17, the unique_ptr retains ownership.
  5. CreateNode extracts the raw pointer (node.get()) and returns it. As the function exits, the unique_ptr goes out of scope, immediately freeing the newly allocated AXNode.
  6. MiraclePtr Bypass: The returned dangling raw pointer is then appended to the parent’s children vector, which stores raw_ptr<AXNode>. Because the node was completely freed before the raw_ptr was constructed, the BackupRefPtr (BRP) refcount was 0 at the time of free. In release builds where BRP cookie checks are disabled, creating a raw_ptr to this freed memory simply increments the refcount in the freed slot’s metadata, successfully creating a raw_ptr to attacker-reclaimable memory without crashing.
  7. The attacker can then use heap spraying to reclaim this memory with a controlled payload. A third AXTreeUpdate can trigger operations (like DestroySubtree) on this dangling pointer, leading to arbitrary memory read/write or control-flow hijacking.

Suggested Attacker Steps

(Note: These are potential steps based on code analysis; our tooling agent cannot execute code to verify a working exploit)

  1. Compromise a content renderer process.
  2. Wait for or trick the user into opening the “Read Anything” side panel for the compromised page.
  3. Send Update 1: {root_id: 5, nodes: [{id: 5, child_ids: [0]}]} to the WebUI renderer. This populates id_map_[0]. The update fails later, but the state persists.
  4. Send Update 2: {nodes: [{id: 5, child_ids: [0]}]}. This triggers CreateNode(0) again, which fails try_emplace, frees the node, and stores the dangling pointer in node 5’s children.
  5. Spray the WebUI renderer’s heap to overwrite the freed AXNode slot with a counterfeit object.
  6. Send Update 3: {nodes: [{id: 5, child_ids: []}]}. This removes node 5’s children, invoking DestroySubtree on the counterfeit object, triggering the UAF and executing the payload.

Proposed Fix

  1. Check Return Values: In ReadAnythingAppModel::UnserializeUpdates, check the return value of tree->Unserialize(). If it fails, explicitly destroy or reset the AXTree to prevent using a corrupted state.
  2. Prevent Dangling Pointers: In AXTree::CreateNode, CHECK that try_emplace succeeds (CHECK(inserted); is currently a DCHECK). If it fails, do not return the raw pointer of the local unique_ptr.
  3. Strict ID Validation: Upgrade SANITIZER_CHECK(id != kInvalidAXNodeID) in AXTree::CreateNode and other critical paths to a fatal CHECK in all builds to completely prevent the ingestion of kInvalidAXNodeID.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results from 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