CVE-2026-102325
Overview
Background
- Shared image
- A GPU-backed image resource shared between command-buffer decoders (raster, WebGPU, GL) that carries a “cleared” state so consumers know whether its backing memory has been initialized.
- `SetCleared()` / `IsCleared()`
- Methods on the shared image that mark, and query, whether the resource’s backing memory has been fully initialized; an uncleared region read before initialization exposes stale GPU memory.
- Ganesh flush/submit
- Skia’s Ganesh (GL/Vulkan) backend records drawing lazily;
GrDirectContext::flush()/submit()(orskgpu::ganesh::Flush()) push the recorded work to the GPU and can fail (e.g. context loss, allocation failure). - `should_clear_shared_image_`
- A deferred flag in
RasterDecoderImplrecording that the shared image may be marked cleared once the pending GPU work is confirmed flushed successfully.
Root Cause Analysis
In the raster clear path (the enclosing function in raster_decoder.cc, whose name is not shown in the @@ -2972 hunk), when needs_clear was true the decoder drew the clear color into raster_canvas_ and then, for the non-Graphite/Ganesh case, immediately called shared_image_->SetCleared() — marking the resource initialized before the deferred GPU clear had actually been flushed and submitted. The flush helpers themselves discarded their success signal: the function in shared_context_state.cc called gr_context()->flush() then returned gr_context()->submit(...) (dropping the flush result) and ignored the return of skgpu::ganesh::Flush(surface), and webgpu_decoder_impl.cc likewise ignored skgpu::ganesh::Flush(surface). Consequently, if the GPU flush failed the clear draw never landed, yet the shared image was still recorded as cleared, so a later consumer could read uninitialized GPU memory while IsCleared() returned true.
The fix defers the cleared mark by setting should_clear_shared_image_ = true and only marking the image cleared after a successful flush, and propagates flush/submit success up every call site (flushAndSubmit(...).fSuccess, &= ...Flush(surface).fSuccess, and an early return false in the WebGPU clear path). This restores the invariant that a shared image is marked cleared only after its initializing GPU work has demonstrably completed.
cleared state before confirming that the deferred GPU clear actually flushed, combined with flush helpers that silently swallowed their success/failure result; the fix threads the flush success value through and gates the SetCleared() on it.Attack Path
- Allocate a shared image needing initialization
An attacker in the renderer (via WebGL/2D-canvas/WebGPU as appropriate) causes creation of a shared image whose region requires a clear, driving the
needs_clearbranch inraster_decoder.cc. - Induce a GPU flush failure
The attacker arranges conditions under which
gr_context()’s flush/submit orskgpu::ganesh::Flush(surface)fails (e.g. resource exhaustion or context loss), so the deferred clear never reaches the GPU(inferred). - Image marked cleared despite failed clear
Pre-patch, the code still called
shared_image_->SetCleared()(and theDCHECK(... IsCleared())masked it in debug builds only), soIsCleared()returns true over uninitialized backing memory. - Read back uninitialized memory
The attacker samples/copies the shared image (e.g. drawImage, texImage, readback), obtaining the contents of uninitialized GPU memory and leaking data left by other contexts
(inferred).
Impact Assessment
(inferred). The precondition is the ability to reach the clear path and to make the underlying Ganesh flush fail.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/raster_decoder.cc |
modified | |
forgpu/command_buffer/service/shared_context_state.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_context_state.ccgpu/command_buffer/service/webgpu_decoder_impl.ccgpu/command_buffer/service/raster_decoder.cc
Audit Directions
- State-before-completionLook for places that set a “cleared”/“initialized”/“valid” flag (e.g.
SetCleared()) before the asynchronous or deferred work that establishes that state has been confirmed to complete. - Discarded success resultsAudit call sites of
flush(),submit(),flushAndSubmit(), andskgpu::ganesh::Flush()whose returnedGrSubmitInfo/fSuccessis ignored, especially where the outcome should gate a security-relevant state transition. - DCHECK-masked invariantsReview invariants enforced only via
DCHECK(like the removedIsCleared()assertion) that are silently violated in release builds, particularly around GPU resource initialization.
Patch
From de4926eaa4d86714638bb61ee68221a03c64ffbd Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Wed, 23 Sep 2026 15:05:30 -0700
Subject: [PATCH] [skia] Update Ganesh flush calls to propagate success
Ganesh's flush now reports success, so update call sites to include that
in their `success` results that were already being propagated up.
Additionaly, raster_decoder's SetCleared() logic is unified now between
Ganesh and Graphite to only mark the shared image as cleared after the
flush is successful.
Bug: 561994362
Fixed: 561994362
Change-Id: I8dbe91d8f6481af4542ff8f4023811efa6966f87
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8449772
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1703817}
---
diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc
index 02d7ada..e5770e00 100644
--- a/gpu/command_buffer/service/raster_decoder.cc
+++ b/gpu/command_buffer/service/raster_decoder.cc
@@ -2972,13 +2972,10 @@
// incorrect.
if (needs_clear) {
raster_canvas_->drawColor(sk_color_4f, SkBlendMode::kSrc);
- if (graphite_shared_context()) {
- should_clear_shared_image_ = true;
- } else {
- shared_image_->SetCleared();
- }
+ // Remember that we can mark the shared image as cleared if the deferred
+ // GPU work is flushed successfully.
+ should_clear_shared_image_ = true;
}
- DCHECK(graphite_shared_context() || shared_image_->IsCleared());
}
scoped_refptr<Buffer> RasterDecoderImpl::GetShmBuffer(uint32_t shm_id) {
diff --git a/gpu/command_buffer/service/shared_context_state.cc b/gpu/command_buffer/service/shared_context_state.cc
index 78dfbec..836f4e9c 100644
--- a/gpu/command_buffer/service/shared_context_state.cc
+++ b/gpu/command_buffer/service/shared_context_state.cc
@@ -880,8 +880,9 @@
: skgpu::graphite::SyncToCpu::kNo);
return flush_succeeded;
} else if (gr_context()) {
- gr_context()->flush();
- return gr_context()->submit(sync_to_cpu ? GrSyncCpu::kYes : GrSyncCpu::kNo);
+ return gr_context()
+ ->flushAndSubmit(sync_to_cpu ? GrSyncCpu::kYes : GrSyncCpu::kNo)
+ .fSuccess;
}
return true;
}
@@ -907,7 +908,7 @@
for (int plane_index = 0; plane_index < num_planes; plane_index++) {
auto* surface = access->surface(plane_index);
DCHECK(surface);
- skgpu::ganesh::Flush(surface);
+ success &= skgpu::ganesh::Flush(surface).fSuccess;
}
}
}
diff --git a/gpu/command_buffer/service/webgpu_decoder_impl.cc b/gpu/command_buffer/service/webgpu_decoder_impl.cc
index cc965727..fea1fd4c 100644
--- a/gpu/command_buffer/service/webgpu_decoder_impl.cc
+++ b/gpu/command_buffer/service/webgpu_decoder_impl.cc
@@ -847,7 +847,7 @@
// will populate it with semaphores and call GrDirectContext::flush.
success = true;
if (shared_context_state_->gr_context()) {
- skgpu::ganesh::Flush(surface);
+ success = skgpu::ganesh::Flush(surface).fSuccess;
} else {
DCHECK(shared_context_state_->graphite_shared_context());
DCHECK(shared_context_state_->gpu_main_graphite_recorder());
@@ -2479,7 +2479,10 @@
// It's ok to pass in empty GrFlushInfo here since SignalSemaphores()
// will populate it with semaphores and call GrDirectContext::flush.
if (shared_context_state_->gr_context()) {
- skgpu::ganesh::Flush(surface);
+ if (!skgpu::ganesh::Flush(surface).fSuccess) {
+ DLOG(ERROR) << "ClearSharedImage: skgpu::ganesh::Flush failed";
+ return false;
+ }
} else {
DCHECK(shared_context_state_->graphite_shared_context());
DCHECK(shared_context_state_->gpu_main_graphite_recorder());