CVE-2026-118351
Overview
Background
- Unique key
- A
GrUniqueKeymaps a key to exactly one cached resource; reassigning a key that another resource (old) already holds requires removing it fromold. - 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”).
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
- Drive GPU resource caching
Via WebGL/Canvas2D/WebGPU workloads (inferred web reachability; the diff is Skia-internal), create GPU resources that populate
GrResourceCacheand leave some at refcount 0 in the purgeable queue. - Force a unique-key reassignment
Cause a
UniqueKeycurrently held by such anoldresource to be assigned to a new resource, entering theelsebranch that reaches theremoveUniqueKey(sk_ref_sp(old).get())path on a first-ref/purgeableold. - Desynchronize queue vs. refcount
The
sk_ref_sptemporary refs without transitioningoldout of the purgeable queue, then unrefs at statement end whileoldis in the wrong queue, leaving inconsistent bookkeeping. - Trigger a purge
Subsequent cache pressure / purge walks the purgeable queue and frees
oldwhile it is still effectively referenced (or double-frees/corrupts queue links). - 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
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 resourcesGrep Skia Ganesh for
sk_ref_sp(applied toGrGpuResource*/GrResourceCachemembers where the cache must own the first ref; these can skip the purgeable→unpurgeable transition. - Purgeable-queue invariantsAudit all
removeUniqueKey/changeUniqueKey/releasecallers to confirm the resource’s queue membership matches its refcount beforeunref(). - First-ref ownership ruleReview 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.
Patch
From 039111f1628723eb9171f4ff27e9d8386d3e3a86 Mon Sep 17 00:00:00 2001 From: Nathan Sanchez <[email protected]> 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 <[email protected]> Commit-Queue: Nathan Sanchez <[email protected]> --- 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));