High patch-now ✔︎ verified CVSS 3.1 chrome UAF 🔧 Commit mapped 🧬 Ignoring a failure result and using the partial output

Overview

High
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Sub-componentGPU
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Root cause matches commit. Diff only shows stale atlas generation/locators (sampling evicted glyph slots); memory-UAF consequence vs. wrong-glyph/cross-origin texel leak is not shown. Requires atlas pressure splitting a subrun across flushes.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker540016074
Fix commit9b1e5fd08d2c (skia) +26/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-09-04 (public git)
Disclosed2026-09-15
Patch gap11 days — fix public in source before the advisory

Background

Glyph atlas
A shared GPU texture into which rasterized glyph images are packed for text rendering; entries can be evicted and overwritten when space is needed for new glyphs.
`fAtlasGeneration`
A per-GlyphData/subrun token cached to detect whether the atlas has changed since the subrun’s glyphs were last placed; when it matches the atlas’ current generation, a fast path skips re-adding glyphs and reuses previously computed atlas coordinates.
Subrun split across a flush (`begin > 0`)
When a subrun’s glyphs cannot all be processed in a single flush, later calls resume at an offset begin, meaning glyphs in [0, begin) were handled in an earlier flush and glyphs in [begin, end] are handled now.
`fBulkUseUpdater`
The accumulator that batches glyph use/token updates for the current pass and is reset at the start of a regeneration.

Root Cause Analysis

In both the Ganesh and Graphite text paths (src/gpu/ganesh/text/GlyphData.cpp and src/gpu/graphite/text/GlyphData.cpp), after placing glyphs into the atlas the code updated fAtlasGeneration whenever success && begin + glyphsPlacedInAtlas == glyphVector.glyphCount(), i.e. whenever processing reached the end of the glyph vector. This wrongly assumed that reaching the end implied all of the subrun’s glyphs were currently resident in the atlas with a consistent use/flush token. When begin > 0, the subrun had been split across flushes: glyphs in [0, begin) were consumed in a prior flush and are now evictable, and placing the remaining [begin, end] glyphs may have evicted/overwritten those earlier entries. Recording fAtlasGeneration in that case falsely validates the fast path, so a later reuse skips re-adding glyphs and trusts stale atlas coordinates that no longer point at live atlas data.

The fix requires begin == 0 before caching fAtlasGeneration (success && begin == 0 && glyphsPlacedInAtlas == glyphVector.glyphCount()), so the fast path is only taken when the entire subrun was placed in a single pass and genuinely shares one token, and it clarifies that fBulkUseUpdater.reset() must run so a subsequent addGlyphToBulkAndSetUseToken() re-registers first use under the new token.

Key insight
The single mistake was treating “reached the end of the glyph vector” (begin + glyphsPlacedInAtlas == glyphCount()) as equivalent to “the whole subrun is atlas-resident under one token,” when a partial (begin > 0) update leaves earlier glyphs potentially evicted; the fix adds the begin == 0 guard so fAtlasGeneration is only cached after a complete single-pass placement.

Attack Path

  1. Force a large text subrun An attacker renders attacker-controlled text (e.g. via a web page, canvas, or CSS) that produces a GlyphData/subrun with more glyphs than can be flushed at once.
  2. Trigger a partial update The subrun is split across flushes so that a later regeneration call runs with begin > 0, processing only [begin, end] while [0, begin) glyphs were placed earlier and are now evictable.
  3. Cause eviction under the buggy guard Placing the [begin, end] glyphs evicts/overwrites the earlier [0, begin) entries, yet the old condition still records fAtlasGeneration because processing reached glyphCount().
  4. Reuse the stale fast path On a subsequent draw the cached fAtlasGeneration matches, so the code skips re-adding glyphs and uses atlas coordinates that now reference freed/overwritten atlas texture memory, yielding a use-after-free.
  5. Escalate from the UAF Reading through the stale atlas locator gives an out-of-bounds/freed-memory access that could be shaped toward information disclosure or memory corruption in the GPU/render context, and any further primitive or code execution beyond this is (inferred).

Impact Assessment

An attacker who can drive text rendering gains a use-after-free of glyph-atlas resources in the process running Skia’s GPU text path (renderer/GPU context), reachable with attacker-influenced glyph content that splits a subrun across flushes so that begin > 0. Escalation to a read/write primitive, sandbox escape, or code execution is (inferred) and not demonstrated by the diff. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1.

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/text/GlyphData.cpp
modified
if
src/gpu/graphite/text/GlyphData.cpp
modified

Files Changed

  • src/gpu/ganesh/text/GlyphData.cpp
  • src/gpu/graphite/text/GlyphData.cpp

Audit Directions

  • Completion implies validity
    Look for atlas/cache-generation or “still resident” fast paths that conclude from offset + count == total (reaching the end) that all elements are current, ignoring items processed in earlier partial passes.
  • Split-across-flush state
    Audit code paths keyed on a begin/offset that resumes work across flushes, checking whether per-element tokens or generation counters are cached as if the whole range shared one flush token.
  • Eviction after use-token caching
    Review any place that records a generation/version after operations (like atlas insertion) that can evict prior entries, ensuring the cached token is only set when no earlier still-referenced entry could have been evicted.
From 9b1e5fd08d2cbc9c8933c3538716905c4cf19c5a Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Fri, 04 Sep 2026 09:38:19 -0400
Subject: [PATCH] [gpu] Don't mark GlyphData's atlas generation on a partial update

If a GlyphData/subrun is split across a flush (i.e. begin > 0 for a
second call to the same GlyphData), we don't want to mark its atlas
generation as if it had successfully pushed all of its glyphs into the
atlas.

The previous assumption was that if we reached the end, then all the
glyphs were still in the atlas. However, if begin > 0, then the [0,
begin) glyphs were already used and consumed from a previous flush and
then are seen as evictable. It is plausible then that the remaining
[begin, end] glyphs had to overwrite earlier ones in order to fit.

This guards the atlas generation fast path for the scenario when all [0,
glyphCount] glyphs were added in one go, so they really are guaranteed
to have the same use/flush token.

Bug: 540016074
Change-Id: Ibefee9d541cf149c2ce5640ea304aa801c590cab
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1350696
Reviewed-by: Thomas Smith <thomsmit@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
---
diff --git a/src/gpu/ganesh/text/GlyphData.cpp b/src/gpu/ganesh/text/GlyphData.cpp
index f4a2e8a..d5b16d9 100644
--- a/src/gpu/ganesh/text/GlyphData.cpp
+++ b/src/gpu/ganesh/text/GlyphData.cpp
@@ -198,7 +198,14 @@
     if (fAtlasGeneration != currentAtlasGen) {
         SkSpan<const Glyph> glyphSpan = glyphVector.accessBackendGlyphs<Glyph>();
         // Calculate the texture coordinates for the vertexes during first use (fAtlasGeneration
-        // is set to kInvalidAtlasGeneration) or the atlas has changed in subsequent calls.
+        // is set to kInvalidAtlasGeneration) or the atlas has changed in subsequent calls. Always
+        // reset the update, even when begin > 0. When begin > 0, this subrun was split across
+        // flushes and previously used glyphs in [0, begin) have an older token. If those glyphs are
+        // not used in [begin, end], they shouldn't be part of the next bulk update (and because
+        // we won't have updated the whole subrun in one go, we won't set `fAtlasGeneration` to take
+        // the fast path on reuse). If we do reuse the glyphs, we need the bulk update to have been
+        // reset so that the call to addGlyphToBulkAndSetUseToken() sees the first use with the new
+        // token and updates the atlas locator.
         fBulkUseUpdater.reset();
 
         SkBulkGlyphMetricsAndImages metricsAndImages{fTextStrike->strikeSpec()};
@@ -228,8 +235,11 @@
             glyphsPlacedInAtlas++;
         }
 
-        // Update atlas generation if there are no more glyphs to put in the atlas.
-        if (success && begin + glyphsPlacedInAtlas == glyphVector.glyphCount()) {
+        // Update atlas generation if there are no more glyphs to put in the atlas. We can only do
+        // this if we successfully checked/added the entire glyph vector in one pass. Otherwise, on
+        // a partial update, some of the previous glyphs of the subrun that were already drawn could
+        // have been evicted to make room for these remaining glyphs.
+        if (success && begin == 0 && glyphsPlacedInAtlas == glyphVector.glyphCount()) {
             // Need to get the freshest value of the atlas' generation because
             // updateTextureCoordinates may have changed it.
             fAtlasGeneration = atlasManager->atlasGeneration(fResolvedMaskFormat);
diff --git a/src/gpu/graphite/text/GlyphData.cpp b/src/gpu/graphite/text/GlyphData.cpp
index bbbf808..10be6dc 100644
--- a/src/gpu/graphite/text/GlyphData.cpp
+++ b/src/gpu/graphite/text/GlyphData.cpp
@@ -65,7 +65,14 @@
 
     if (fAtlasGeneration != currentAtlasGen) {
         // Calculate the texture coordinates for the vertexes during first use (fAtlasGeneration
-        // is set to kInvalidAtlasGeneration) or the atlas has changed in subsequent calls.
+        // is set to kInvalidAtlasGeneration) or the atlas has changed in subsequent calls. Always
+        // reset the update, even when begin > 0. When begin > 0, this subrun was split across
+        // flushes and previously used glyphs in [0, begin) have an older token. If those glyphs are
+        // not used in [begin, end], they shouldn't be part of the next bulk update (and because
+        // we won't have updated the whole subrun in one go, we won't set `fAtlasGeneration` to take
+        // the fast path on reuse). If we do reuse the glyphs, we need the bulk update to have been
+        // reset so that the call to addGlyphToBulkAndSetUseToken() sees the first use with the new
+        // token and updates the atlas locator.
         fBulkUseUpdater.reset();
 
         SkBulkGlyphMetricsAndImages metricsAndImages{fTextStrike->strikeSpec()};
@@ -93,8 +100,11 @@
             glyphsPlacedInAtlas++;
         }
 
-        // Update atlas generation if there are no more glyphs to put in the atlas.
-        if (success && begin + glyphsPlacedInAtlas == glyphVector.glyphCount()) {
+        // Update atlas generation if there are no more glyphs to put in the atlas. We can only do
+        // this if we successfully checked/added the entire glyph vector in one pass. Otherwise, on
+        // a partial update, some of the previous glyphs of the subrun that were already drawn could
+        // have been evicted to make room for these remaining glyphs.
+        if (success && begin == 0 && glyphsPlacedInAtlas == glyphVector.glyphCount()) {
             // Need to get the freshest value of the atlas' generation because
             // updateTextureCoordinates may have changed it.
             fAtlasGeneration = atlasManager->atlasGeneration(fRenderData.maskFormat);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.