CVE-2026-9923
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/graphite/GlobalCache.cpp |
modified |
Files Changed
src/gpu/graphite/GlobalCache.cpp
Patch
From 04ff0203681d3e21a201084d39c29e6d44ef8358 Mon Sep 17 00:00:00 2001 From: Robert Phillips <[email protected]> Date: Thu, 23 Apr 2026 12:57:10 -0400 Subject: [PATCH] [graphite] Fix a security issue in GlobalCache::findGraphicsPipeline Bug: b/500393328 Change-Id: I35ad93eaba08fcb4c0896993ff857902622581a0 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1217456 Commit-Queue: Robert Phillips <[email protected]> Reviewed-by: Michael Ludwig <[email protected]> --- diff --git a/src/gpu/graphite/GlobalCache.cpp b/src/gpu/graphite/GlobalCache.cpp index 146c0a2..86583be 100644 --- a/src/gpu/graphite/GlobalCache.cpp +++ b/src/gpu/graphite/GlobalCache.cpp @@ -185,31 +185,33 @@ [[maybe_unused]] bool forPrecompile = SkToBool(pipelineCreationFlags & PipelineCreationFlags::kForPrecompilation); - sk_sp<GraphicsPipeline>* entry = nullptr; + sk_sp<GraphicsPipeline> result; { SkAutoSpinlock lock{fSpinLock}; - entry = fGraphicsPipelineCache.find(key); + sk_sp<GraphicsPipeline>* entry = fGraphicsPipelineCache.find(key); if (entry) { - if ((*entry)->didAsyncCompilationFail()) SK_UNLIKELY { + result = *entry; + + if (result->didAsyncCompilationFail()) SK_UNLIKELY { // If the pipeline failed, remove it from the cache and let it be regenerated. - this->removeGraphicsPipeline((*entry).get()); + this->removeGraphicsPipeline(result.get()); return nullptr; } #if defined(GPU_TEST_UTILS) ++fStats.fGraphicsCacheHits; #endif - if ((*entry)->epoch() != fEpochCounter) { - (*entry)->markEpoch(fEpochCounter); // update epoch due to use in a new epoch + if (result->epoch() != fEpochCounter) { + result->markEpoch(fEpochCounter); // update epoch due to use in a new epoch ++fStats.fPipelineUsesInEpoch; } - if (!forPrecompile && (*entry)->fromPrecompile() && !(*entry)->wasUsed()) { + if (!forPrecompile && result->fromPrecompile() && !result->wasUsed()) { ++fStats.fNormalPreemptedByPrecompile; } - (*entry)->updateAccessTime(); - (*entry)->markUsed(); + result->updateAccessTime(); + result->markUsed(); #if defined(SK_PIPELINE_LIFETIME_LOGGING) static const char* kNames[2] = { "CacheHitForN", "CacheHitForP" }; @@ -217,7 +219,7 @@ TRACE_STR_STATIC(kNames[forPrecompile]), TRACE_EVENT_SCOPE_THREAD, "key", key.hash(), - "compilationID", (*entry)->getPipelineInfo().fCompilationID); + "compilationID", result->getPipelineInfo().fCompilationID); #endif } else { #if defined(GPU_TEST_UTILS) @@ -242,12 +244,11 @@ } } - if (entry) { - this->invokePipelineCallback(ContextOptions::PipelineCacheOp::kPipelineFound, entry->get()); - return *entry; + if (result) { + this->invokePipelineCallback(ContextOptions::PipelineCacheOp::kPipelineFound, result.get()); } - return nullptr; + return result; } #if SK_HISTOGRAMS_ENABLED
Original Bug Report
Potential Race condition leading to Use-After-Free in Graphite GlobalCache::findGraphicsPipeline
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A race condition exists in GlobalCache::findGraphicsPipeline where a stack-local raw pointer to a cache entry is dereferenced after its protecting spinlock is released. This can result in a Use-After-Free if another thread evicts the entry from the LRU cache during the window between lock release and dereference. An attacker could potentially use this to execute an arbitrary 64-bit atomic increment, leading to RCE in the unsandboxed GPU process.
Affected files:
third_party/skia/src/gpu/graphite/GlobalCache.cpp
Estimated timestamp from git blame: 2025-10-29
Description
A potential Use-After-Free (UAF) vulnerability exists in the Graphite backend of Skia, specifically within GlobalCache::findGraphicsPipeline. The function retrieves a raw pointer to an entry in fGraphicsPipelineCache (an SkLRUCache) while holding fSpinLock. However, it releases the lock before dereferencing this pointer.
In third_party/skia/src/gpu/graphite/GlobalCache.cpp:
188: sk_sp<GraphicsPipeline>* entry = nullptr;
189: {
190: SkAutoSpinlock lock{fSpinLock};
191:
192: entry = fGraphicsPipelineCache.find(key);
... // Bookkeeping performed under lock
242: }
243: } // fSpinLock is released here
244:
245: if (entry) {
246: this->invokePipelineCallback(ContextOptions::PipelineCacheOp::kPipelineFound, entry->get());
247: return *entry;
248: }
SkLRUCache::find() returns a raw pointer to the fValue field within a heap-allocated Entry node. Because fSpinLock is released at line 243, a concurrent thread can acquire the lock and perform operations that evict or remove the entry from the cache. For example, a concurrent call to addGraphicsPipeline calls fGraphicsPipelineCache.insert(), which evicts the LRU tail if the cache size limit (default 256) is exceeded. Eviction results in the heap memory backing the Entry being deleted, leaving entry as a dangling pointer.
Impact & Exploitability
In Chromium’s GPU process, GlobalCache is shared across multiple threads (e.g., Viz compositor thread and GPU main thread). This provides a mechanism for concurrent access where one thread can trigger an eviction while another is returning a pipeline from the cache.
This issue is not protected by MiraclePtr/BRP because entry is a stack-local raw pointer within Skia code, which is currently excluded from the MiraclePtr rewriter. Furthermore, because there are no raw_ptr references to the cache entry memory, PartitionAlloc does not quarantine the memory upon deletion, making it immediately available for reallocation.
When a race occurs, the dereference at line 247 (return *entry;) accesses freed memory. This invokes the copy constructor for sk_sp<GraphicsPipeline>.
The following steps suggest how an attacker could potentially exploit this:
- The attacker creates 256 unique graphics pipelines to fill the LRU cache to its limit.
- The attacker triggers
findGraphicsPipelineon Thread 1 for an existing pipeline (Pipeline A), pausing execution immediately after thefSpinLockis released. - Concurrently on Thread 2, the attacker triggers the creation of 256 new unique pipelines via
addGraphicsPipeline. This evicts Pipeline A from the cache and deletes itsEntrynode. - The attacker sprays the GPU process heap to reclaim the freed
Entrychunk, placing a fake 64-bit pointer (P) at the exact memory offset corresponding to the formerfValuefield. - Thread 1 resumes and executes
return *entry;. - The
sk_spcopy constructor reads the attacker-controlled pointerPand callsSkSafeRef(P). SkSafeRefexecutesP->ref(). Crucially,ref()inskgpu::graphite::Resource(the base class ofGraphicsPipeline) is a non-virtual inline method. This means the compiler emits a direct static function call, completely bypassing the need for the fake object to have a valid vtable.Resource::ref()executes an atomic 64-bit addition:fRefs.fetch_add((uint64_t)1 << 2, std::memory_order_relaxed);at offset +8 fromP.
The attacker has achieved a pristine, highly reliable arbitrary 64-bit atomic increment primitive. By setting P+8 to the address of an array bound, string length, or index within a critical GPU process object, the attacker can surgically inflate the bound by 4. This provides out-of-bounds read and write capabilities, allowing them to bypass ASLR and hijack control flow. Because the Chromium GPU process is unsandboxed and has elevated privileges, achieving RCE within it constitutes a full sandbox escape (Severity 0).
Recommendation
The implementation should copy the sk_sp<GraphicsPipeline> value into a local variable while the lock is still held, and then use that local variable outside the lock scope. This is already the pattern used correctly in GlobalCache::findComputePipeline in the same file.
sk_sp<GraphicsPipeline> pipeline;
{
SkAutoSpinlock lock{fSpinLock};
if (auto* entry = fGraphicsPipelineCache.find(key)) {
// ... bookkeeping ...
pipeline = *entry;
} else {
// ... cache miss logic ...
}
}
if (pipeline) {
this->invokePipelineCallback(ContextOptions::PipelineCacheOp::kPipelineFound, pipeline.get());
return pipeline;
}
return nullptr;
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.