Medium watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ReadingMode
DescriptionUse after free in ReadingMode
ComponentReadingMode
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.
Tracker498277368
Fix commitb30d69ffbdea (chromium/src) +8/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-06 (public git)
Disclosed2026-05-05
Patch gap29 days — fix public in source before the advisory

Background

Reading Mode / Read Anything
a Chrome accessibility feature that builds a simplified view of a page from an accessibility tree, implemented in the renderer under chrome/renderer/accessibility/read_anything.
`AXTree` / `Unserialize`
an accessibility tree object; tree->Unserialize(update) applies a serialized AXTreeUpdate to the tree and returns a bool indicating whether the update applied cleanly.
`ReadAnythingAppModel`
the renderer-side model (read_anything_app_model.cc) that holds per-tree-ID AXTree objects and applies incoming updates.
`OnAXTreeDestroyed`
the model’s teardown routine that removes and destroys the tree associated with a given tree ID.

Root Cause Analysis

The update-applying code in read_anything_app_model.cc called tree->Unserialize(update) and ignored its return value, continuing to use the tree regardless of whether unserialization succeeded. When Unserialize returns false, the tree is left in an invalid/inconsistent partial state, yet the pre-patch code proceeded (e.g. setting URL info and continuing to operate on the tree), so later accesses could dereference nodes or state corrupted or torn down by the failed update — a use-after-free per the advisory.

The fix captures the result into const bool unserialized, adds DUMP_WILL_BE_CHECK(unserialized), and on !unserialized calls OnAXTreeDestroyed(tree_id) and returns early, so an invalid tree is destroyed rather than used. This works because the only safe response to a failed unserialize is to discard the now-invalid tree, which the early return plus destruction guarantees before any downstream use. The enclosing function is not named in the @@ hunk header.

Key insight
The code treated AXTree::Unserialize as infallible and kept using a tree that had failed to apply its update; the fix checks the return value and, on failure, destroys the tree (OnAXTreeDestroyed) and returns before any further use.

Attack Path

  1. Deliver a malformed tree update An attacker-influenced page (or the browser process relaying attacker-shaped accessibility data) causes an AXTreeUpdate that Unserialize cannot apply cleanly to be sent to the Reading Mode model.
  2. Unserialize fails silently tree->Unserialize(update) returns false, leaving the AXTree in an invalid/partially-mutated state, but the pre-patch code ignores the result and continues.
  3. Continued use of invalid tree The model proceeds to operate on the invalid tree (and later code paths traverse it), accessing nodes/state left dangling by the failed update.
  4. Use-after-free A subsequent dereference touches freed or invalid tree memory, the use-after-free reported by the advisory (the specific freed object and sink are not shown in this diff).

Impact Assessment

An attacker able to drive a failing accessibility-tree update can leave the Reading Mode model operating on an invalid tree, producing a use-after-free in the renderer process where Reading Mode runs; the precondition is that the feature is active and processes the malformed update. The vendor advisory rates this medium severity (“Use after free in ReadingMode”) and does not report in-the-wild exploitation. Any escalation to a controlled read/write primitive or code execution is (inferred) and not demonstrated by the diff.

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

Audit Directions

  • Ignored fallible return values
    Grep for Unserialize, Deserialize, and similar apply/parse calls whose bool/status result is discarded, especially where the object is used immediately afterward.
  • Invalid-state-after-partial-mutation
    Audit routines that mutate a long-lived object in place from untrusted input and keep using it even when the mutation reports failure; the safe pattern is destroy-and-return.
  • Accessibility tree lifetime
    Review other AXTree update sites in the renderer for the same “check Unserialize, destroy on failure” discipline, since accessibility trees are shaped by cross-process data.
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