30bde0efb4f79cfb04e6a7bae33fdab3efce99e6 [AX] Fix dump will check crash when serializing accessibility tree with stale cached values
Triage note: Renderer crash during accessibility serialization, reachable only with accessibility enabled. No memory corruption. watch.
The bug at a glance
A renderer-stability bug in the accessibility serialization path, no memory-safety component. During accessibility-tree serialization the AX cache is frozen (updates disallowed), but blink_ax_tree_source called child->IsIncludedInTree(), whose implementation runs CheckCanAccessCachedValues() and would try to refresh/validate cached state\u2014illegal while frozen\u2014firing a NOTREACHED (a ‘dump will check’ / DCHECK-class abort). The trigger is recursive ChildAt() traversal reaching child objects whose cached values are stale because they were not directly marked dirty. The fix reads the already-computed value via CachedIsIncludedInTree(), which the lifecycle guarantees is current by the kSerialize phase. Because it aborts on stale cached values that arise from ordinary DOM/AX churn during serialization, it is reachable in the field (particularly with assistive tech or headless AX dumps active), but yields only a clean process termination\u2014no corruption, no disclosure. Low severity; cvss 5.0 (availability-only, and gated on the AX serialization path being exercised, which is not universal). No test ships with the fix.
There are two distinct ‘is this node included in the AX tree’ entry points: IsIncludedInTree(), which may lazily validate/refresh cached values (and therefore calls CheckCanAccessCachedValues), and CachedIsIncludedInTree(), which reads the stored value with no side effects. The serialization phase runs with the cache frozen and relies on the lifecycle having already brought cached values up to date, so it must use the read-only accessor. The bug is one caller using the validating accessor inside the frozen window; the angle for auditors is that this mistake can recur anywhere serialization-time code reaches for a cached-value getter that isn’t the Cached* variant.
Root cause
blink_ax_tree_source serializes the AX tree by walking nodes and their children. At the flagged site it validated that a child received during traversal is actually included in the tree via child->IsIncludedInTree(), using the result to gate a NOTREACHED() guard (‘Should not receive unincluded child.’). The problem is not that guard’s logic but the getter it calls: IsIncludedInTree() invokes CheckCanAccessCachedValues(), which enforces that cached values may be accessed/refreshed only in permitted lifecycle states. During serialization the cache is frozen, so refresh is not allowed; when ChildAt() recursively descends and reaches a child whose cached values are stale (it was not directly marked dirty, so its cache wasn’t refreshed in the current cycle), CheckCanAccessCachedValues() detects the disallowed access and hits its NOTREACHED, aborting. The fix replaces IsIncludedInTree() with CachedIsIncludedInTree(), which reads the stored cached inclusion bit directly without triggering validation or updates. Correctness rests on two documented invariants: (1) the AX lifecycle guarantees cached values are up to date by the kSerialize phase, and (2) the cache is frozen during serialization so no update would be legal anyway\u2014hence reading the cached value directly is both safe and semantically correct.
Key code
Serialization guard switched from the validating getter to the read-only cached getter
- if (!child->IsIncludedInTree()) {
+ // Use CachedIsIncludedInTree() since this is called during serialization
+ // when cache is frozen and we should not trigger cached value updates.
+ if (!child->CachedIsIncludedInTree()) {
NOTREACHED() << "Should not receive unincluded child."
<< "\nChild: " << child->ToString().Utf8()
<< "\nParent: " << node->ToString().Utf8();
Patch walkthrough
third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.cc— Single-line behavioral fix in the child-inclusion guard used during tree serialization. child->IsIncludedInTree() is replaced by child->CachedIsIncludedInTree(), with a comment explaining that serialization runs with the cache frozen so the validating accessor (which would call CheckCanAccessCachedValues and attempt an update) must not be used. The NOTREACHED guard body is unchanged\u2014it still asserts no unincluded child should reach this point\u2014but the condition is now evaluated from the frozen cached state instead of a live re-validation, so recursively-reached children with stale (but lifecycle-guaranteed-current-by-kSerialize) cached values no longer trip CheckCanAccessCachedValues’s own NOTREACHED.
Background
AX cache freezing — During accessibility-tree serialization Blink freezes the AXObjectCache: cached values are treated as final for the pass and updates are disallowed. Code in this window must read cached state, not recompute it.
CheckCanAccessCachedValues() — A guard invoked by validating cached-value getters (like IsIncludedInTree()) to ensure cached access/refresh only happens in permitted lifecycle states. It NOTREACHEDs when called in a disallowed state, e.g. against stale values while frozen.
IsIncludedInTree() vs CachedIsIncludedInTree() — The former may validate/refresh the cached inclusion state (and calls CheckCanAccessCachedValues); the latter returns the stored cached bit directly with no side effects\u2014the correct choice under a frozen cache.
kSerialize lifecycle phase — The AX document lifecycle stage at which cached values are guaranteed up to date; serialization runs here, which is why reading cached values directly is safe.
stale cached values via ChildAt recursion — Recursive traversal can reach child objects that were not individually marked dirty, so their cache wasn’t refreshed this cycle. The validating getter then attempts a disallowed refresh on those, causing the crash.
Vulnerability window
- root cause — The serialization child-inclusion guard used the validating IsIncludedInTree() rather than the cached accessor, latent until recursive traversal reached a stale, not-directly-dirtied child under a frozen cache.
- report — Bug 448749714: ‘dump will check’ crash when serializing the accessibility tree with stale cached values.
- fix — 2026-01-14, main@{#1569324}: Alexander Surkov (Igalia) switches the guard to CachedIsIncludedInTree(), documenting the frozen-cache and kSerialize invariants. Reviewed by David Tseng.
- status — No shipped test, no CVE. Security-flagged as a reachable NOTREACHED (renderer DoS); watch-tier stability hardening on the AX path.
Proof of concept
No test or PoC ships with this commit, and none is directly reconstructable from the one-line diff. Honest reproduction recipe (crash repro, not an exploit): run a build with accessibility enabled and serialization exercised\u2014either with an assistive-technology client attached, or via a headless/automation AX-tree dump (e.g. content_shell –dump-accessibility or the AccessibilityTreeFormatter dump path)\u2014on a page that mutates the DOM/AX tree such that some descendants change inclusion state without being individually marked dirty, then force AX-tree serialization while children are reached recursively via ChildAt(). On pre-patch builds the traversal would reach a stale-cached child, call IsIncludedInTree() -> CheckCanAccessCachedValues() under the frozen cache, and abort at NOTREACHED. Establishing the exact stale-child condition requires targeting the AX cache-dirtying logic and is not reduced to a stable script here; no exploit primitive exists and none is claimed.
Exploitation
- prerequisite — The AX serialization path must be active: an assistive-technology client, an automation/AX dump consumer, or a feature that requests the platform AX tree. Not all users exercise this, which bounds reach.
- trigger — Page content/DOM mutations that leave descendant AX objects with stale (not-directly-dirtied) cached inclusion state at serialization time, reached via recursive ChildAt() while the cache is frozen.
- impact — Reachable NOTREACHED -> renderer abort: a DoS confined to the renderer, no memory disclosure or corruption. Availability-only.
Detection & hunting
For defenders and detection engineers:
- crash signature —
- assistive-tech correlation —
Audit directions
- serialization-time cached getters —
- cache-dirty propagation —
- other frozen-cache consumers —