High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Skia
DescriptionMemory corruption in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassUAF
Tracker570825067
Fix commit039111f16287 (skia) +8/-2
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

Background

Unique key
A GrUniqueKey maps a key to exactly one cached resource; reassigning a key that another resource (old) already holds requires removing it from old.
Purgeable / unpurgeable queues
A resource with refcount 0 lives in the purgeable queue (eligible to be freed); refcounting it up must transition it to the unpurgeable queue. unref() expects the resource to be in the unpurgeable queue when its refcount is manipulated.
`refResource()` vs `sk_ref_sp()`
refResource() is the cache’s own ref primitive, which (for a first ref) also moves the resource out of the purgeable queue. sk_ref_sp(old) is a generic smart-pointer that refs on construction and unrefs when the temporary is destroyed — without queue bookkeeping.
`removeUniqueKey()`
Removes a resource’s unique key; it “expects a non-zero refcount” / an external owner of the resource.

Root Cause Analysis

Although the advisory says “Memory corruption in Skia,” the diff pins the mechanism to incorrect refcount/queue bookkeeping that leaves a purgeable resource in an inconsistent state — a use-after-free / premature-free hazard. When switching a UniqueKey onto a new resource while an old resource still holds it (and old is not simply purgeable-and-releasable), the pre-patch code called this->removeUniqueKey(sk_ref_sp(old).get()). sk_ref_sp(old) creates a temporary owning ref: it refs old for the duration of the full expression and unrefs it when the temporary is destroyed at the statement’s end. Two problems follow. First, if this is the first ref on old, the ref was taken by sk_ref_sp rather than by the cache’s refResource(), so old is not transitioned out of the purgeable queue; Skia requires the cache to own the first ref of a resource. Second, when the temporary unrefs at end of statement, unref() runs while old is still (incorrectly) in the purgeable queue even though unref() expects it to be in the unpurgeable queue — violating the queue invariant. The net effect is a resource whose queue membership and refcount tracking disagree, which can lead to the resource being purged/freed while still referenced (or double-managed) — memory corruption of a live GPU resource.

The patch performs the ref through the cache and orders the operations explicitly: this->refResource(old); this->removeUniqueKey(old); old->unref();. refResource() gives the cache-owned first ref and, when it is the first ref, transitions old to the unpurgeable queue so the invariant unref() relies on holds; removeUniqueKey(old) then runs with the required non-zero refcount; and the final explicit old->unref() balances the ref while old is correctly in the unpurgeable queue. The replaced comment makes the invariant explicit (“the first ref of a resource must be by the resource cache”).

Key insight
Refing a cache resource with a generic sk_ref_sp temporary instead of the cache’s refResource() skipped the mandatory first-ref-owned-by-cache / purgeable→unpurgeable transition, so the temporary’s end-of-statement unref() operated on a resource in the wrong queue — corrupting the cache’s lifetime bookkeeping.

Attack Path

  1. Drive GPU resource caching Via WebGL/Canvas2D/WebGPU workloads (inferred web reachability; the diff is Skia-internal), create GPU resources that populate GrResourceCache and leave some at refcount 0 in the purgeable queue.
  2. Force a unique-key reassignment Cause a UniqueKey currently held by such an old resource to be assigned to a new resource, entering the else branch that reaches the removeUniqueKey(sk_ref_sp(old).get()) path on a first-ref/purgeable old.
  3. Desynchronize queue vs. refcount The sk_ref_sp temporary refs without transitioning old out of the purgeable queue, then unrefs at statement end while old is in the wrong queue, leaving inconsistent bookkeeping.
  4. Trigger a purge Subsequent cache pressure / purge walks the purgeable queue and frees old while it is still effectively referenced (or double-frees/corrupts queue links).
  5. Memory corruption (inferred) A later use of the freed/mis-tracked GPU resource is a use-after-free in the GPU process; reclaim and control of the freed block are not shown by the diff.

Impact Assessment

The defect is in Skia Ganesh, i.e. the GPU process (where Chrome’s Skia GPU work runs). Reachability from the web is plausible through GPU-accelerated rendering paths but is (inferred) — the diff is purely Skia cache internals with no web entry point shown. The honest primitive is corruption of GrResourceCache lifetime/queue bookkeeping that can free a still-referenced GPU resource (a UAF-class hazard); the concrete freed-then-used sink and any escalation to code execution are (inferred). The advisory’s generic “Memory corruption” is consistent with the code, but the specific class the diff supports is a use-after-free / premature-free from a refcount-queue invariant violation.

Files Changed

  • src/gpu/ganesh/GrResourceCache.cpp

Audit Directions

  • `sk_ref_sp` on cache resources
    Grep Skia Ganesh for sk_ref_sp( applied to GrGpuResource*/GrResourceCache members where the cache must own the first ref; these can skip the purgeable→unpurgeable transition.
  • Purgeable-queue invariants
    Audit all removeUniqueKey/changeUniqueKey/release callers to confirm the resource’s queue membership matches its refcount before unref().
  • First-ref ownership rule
    Review other code paths that ref cache resources outside refResource(), since “the first ref of a resource must be by the resource cache” is a load-bearing invariant.
From 039111f1628723eb9171f4ff27e9d8386d3e3a86 Mon Sep 17 00:00:00 2001
From: Nathan Sanchez <nathanasanchez@google.com>
Date: Tue, 13 Oct 2026 16:00:52 -0400
Subject: [PATCH] [Ganesh] Ref resource in GrResourceCache when switching UniqueKey

This ensures the resource is referenced by the cache to ensure it is
transitioned out of the purgeable queue when refcount is 0.

Bug: 570825067
Change-Id: I4942482ed8d42561e603952462336a1851918f8b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1392837
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Nathan Sanchez <nathanasanchez@google.com>
---
diff --git a/src/gpu/ganesh/GrResourceCache.cpp b/src/gpu/ganesh/GrResourceCache.cpp
index 80727af..2e1bae5 100644
--- a/src/gpu/ganesh/GrResourceCache.cpp
+++ b/src/gpu/ganesh/GrResourceCache.cpp
@@ -295,8 +295,14 @@
                 old->resourcePriv().isPurgeable()) {
                 old->cacheAccess().release();
             } else {
-                // removeUniqueKey expects an external owner of the resource.
-                this->removeUniqueKey(sk_ref_sp(old).get());
+                // GrResourceCache::removeUniqueKey() expects a non-zero refcount, we refcount with
+                // GrResourceCache::refResource() instead of sk_ref_sp since the first ref of a
+                // resource must be by the resource cache. In the case that this is the first ref
+                // on the resource, refResource() also transfers the resource to the unpurgeable
+                // queue, which is required as unref() expects the resource to be there.
+                this->refResource(old);
+                this->removeUniqueKey(old);
+                old->unref();
             }
         }
         SkASSERT(nullptr == fUniqueHash.find(newKey));
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.