← WebKit — 2026-W36

6d28d6994a1f3178058782606b1145197aadaa08  AX: VoiceOver repeats the first sentence over and over as content streams in on gemini.google.com

severity low class UAF confidence 0.35 signal score 14.36 Source/WebCore/accessibility
Primitive: Stale m_liveRegions iterator reused after buildLiveRegionSnapshot mutates the map
Triage note: Mostly a live-region announcement feature (aggregatedText/objectEndOffsets, empty-settle timer, streaming diff), but it also re-finds the m_liveRegions entry instead of reusing an iterator that buildLiveRegionSnapshot may have invalidated and adds CheckedRef captures. The iterator-invalidation hardening is real but incidental to a large feature, so low confidence and not graded.
Structural signals: smartptr_member_added (+6.47), bounds_guard (+3.25), null_guard (+2.59), uaf_lifetime (+4.75), clamp_saturate (+1.8), msg_noise (-2.5), large_churn (-2.0)

Commit message

AX: VoiceOver repeats the first sentence over and over as content streams in on gemini.google.com
https://bugs.webkit.org/show_bug.cgi?id=323464
rdar://186694747

Reviewed by Dominic Mazzoni.

VoiceOver repeated a Gemini response as it streamed in -- the first line several
times, then the first and second together several times, and so on. Any page that
streams text into an aria-live region by re-rendering it is affected.

The region was diffed by matching accessibility objects between the old and new
snapshot by AXID, which assumes the objects backing already-announced text survive an
update. When a page re-renders a region instead of appending to it they do not, and
four separate things went wrong:

  1. These types of pages commonly re-render by emptying the container and refilling it in a later
     task, so the region genuinely has no content for one update. That empty snapshot
     became the baseline, which made the refilled text look entirely new.
  2. Announced text is re-created under new AXIDs, and re-segmented as markup
     resolves -- one text node becomes three once bold or a citation lands -- so
     nothing matched.
  3. Streaming delivers sub-word tokens, so the previous text usually ends mid-word.
  4. Once the response completes the many small nodes are coalesced into a few large
     ones holding byte-identical text.

The region is now compared by its combined text rather than by its objects. A snapshot
caches that text -- every object's text joined by a single space, with whitespace runs
collapsed -- along with the offset each object ends at, so comparing is a string
compare while the result can still be mapped back onto the objects carrying it, which
is what preserves per-object language and the name/value distinction.

Identical text now announces nothing however the region was re-chunked, fixing 4.
Text that only grew at the end announces just the appended tail, fixing 2. When the
split point lands inside a word it backs up to the start of that word and announces
the word whole, fixing 3 and also making a counter ticking 5 to 50 say "50" rather
than the fragment "0".

For 1, the last snapshot that had text is kept instead of being replaced by the empty
one. A region that asked to hear removals still needs to hear that its content went
away, so that announcement is still posted from the kept snapshot.

* LayoutTests/accessibility/isolated-tree/mac/live-regions/live-region-emptied-removal-expected.txt: Added.
* LayoutTests/accessibility/isolated-tree/mac/live-regions/live-region-emptied-removal.html: Added.
* LayoutTests/accessibility/mac/live-regions/live-region-emptied-removal-expected.txt: Added.
* LayoutTests/accessibility/mac/live-regions/live-region-emptied-removal.html: Added.
* LayoutTests/accessibility/mac/live-regions/live-region-streamed-content-not-repeated-expected.txt: Added.
* LayoutTests/accessibility/mac/live-regions/live-region-streamed-content-not-repeated.html: Added.
* Source/WebCore/accessibility/AXAnnouncementTypes.h:
* Source/WebCore/accessibility/AXLiveRegionManager.cpp:
(WebCore::AXLiveRegionManager::handleLiveRegionChange):
(WebCore::aggregateText):
(WebCore::isPunctuationOnly):
(WebCore::AXLiveRegionManager::buildLiveRegionSnapshot const):
(WebCore::appendedObjects):
(WebCore::AXLiveRegionManager::computeChanges const):
(WebCore::AXLiveRegionManager::computeAnnouncement const):
(WebCore::AXLiveRegionManager::postAnnouncementForChange):
* Source/WebCore/accessibility/AXLiveRegionManager.h:

Canonical link: https://commits.webkit.org/320576@main

Before / after

Loading diff…