CVE-2026-7984
Overview
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 serializedAXTreeUpdateto the tree and returns aboolindicating whether the update applied cleanly. - `ReadAnythingAppModel`
- the renderer-side model (
read_anything_app_model.cc) that holds per-tree-IDAXTreeobjects 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.
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
- Deliver a malformed tree update
An attacker-influenced page (or the browser process relaying attacker-shaped accessibility data) causes an
AXTreeUpdatethatUnserializecannot apply cleanly to be sent to the Reading Mode model. - Unserialize fails silently
tree->Unserialize(update)returnsfalse, leaving theAXTreein an invalid/partially-mutated state, but the pre-patch code ignores the result and continues. - 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/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 valuesGrep for
Unserialize,Deserialize, and similar apply/parse calls whosebool/status result is discarded, especially where the object is used immediately afterward. - Invalid-state-after-partial-mutationAudit 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 lifetimeReview other
AXTreeupdate sites in the renderer for the same “check Unserialize, destroy on failure” discipline, since accessibility trees are shaped by cross-process data.
Patch
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.
Original Bug Report
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.ccui/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
- Unchecked Return Value: In
ReadAnythingAppModel::UnserializeUpdates, the return value oftree->Unserialize(update)is ignored. If an update fails mid-way, theAXTreeis left in a partially updated, inconsistent state. - Invalid ID Bypass: An attacker sends an update specifying a child with
id: 0. InAXTree::CreateNode(0),SANITIZER_CHECK(id != kInvalidAXNodeID)is used. In official release builds,SANITIZER_CHECKis a no-op, allowing node ID 0 to be processed and inserted intoid_map_[0]. - ID Collision and Dangling Pointer: The attacker sends a second update referencing
id: 0.AXTree::GetFromId(0)has a hardcoded fast-path that returnsnullptrforkInvalidAXNodeID, bypassing theid_map_. Thus, the tree believes node 0 does not exist and callsCreateNode(0)again. - Inside
CreateNode(0), a newAXNodeis allocated viastd::make_unique. It attempts to insert it usingid_map_.try_emplace(0, std::move(node)). Since0is already in the map from the first update,try_emplacefails. Under C++17, theunique_ptrretains ownership. CreateNodeextracts the raw pointer (node.get()) and returns it. As the function exits, theunique_ptrgoes out of scope, immediately freeing the newly allocatedAXNode.- 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 theraw_ptrwas constructed, the BackupRefPtr (BRP) refcount was 0 at the time of free. In release builds where BRP cookie checks are disabled, creating araw_ptrto this freed memory simply increments the refcount in the freed slot’s metadata, successfully creating araw_ptrto attacker-reclaimable memory without crashing. - The attacker can then use heap spraying to reclaim this memory with a controlled payload. A third
AXTreeUpdatecan trigger operations (likeDestroySubtree) 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)
- Compromise a content renderer process.
- Wait for or trick the user into opening the “Read Anything” side panel for the compromised page.
- Send Update 1:
{root_id: 5, nodes: [{id: 5, child_ids: [0]}]}to the WebUI renderer. This populatesid_map_[0]. The update fails later, but the state persists. - Send Update 2:
{nodes: [{id: 5, child_ids: [0]}]}. This triggersCreateNode(0)again, which failstry_emplace, frees the node, and stores the dangling pointer in node 5’s children. - Spray the WebUI renderer’s heap to overwrite the freed
AXNodeslot with a counterfeit object. - Send Update 3:
{nodes: [{id: 5, child_ids: []}]}. This removes node 5’s children, invokingDestroySubtreeon the counterfeit object, triggering the UAF and executing the payload.
Proposed Fix
- Check Return Values: In
ReadAnythingAppModel::UnserializeUpdates, check the return value oftree->Unserialize(). If it fails, explicitly destroy or reset theAXTreeto prevent using a corrupted state. - Prevent Dangling Pointers: In
AXTree::CreateNode,CHECKthattry_emplacesucceeds (CHECK(inserted);is currently aDCHECK). If it fails, do not return the raw pointer of the localunique_ptr. - Strict ID Validation: Upgrade
SANITIZER_CHECK(id != kInvalidAXNodeID)inAXTree::CreateNodeand other critical paths to a fatalCHECKin all builds to completely prevent the ingestion ofkInvalidAXNodeID.
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.