CVE-2026-95299
Overview
Background
- Graphite
- Skia’s next-generation GPU backend;
GraphiteSharedContextwraps its shared GPU context inside Chrome’s GPU service. - `AsyncReadContext`
- heap struct holding the original
old_callback,old_context, and atask_runnersoReadPixelsCallbackThreadSafecan marshal the completion back to the originating thread. - Context loss
- the GPU context can be torn down (device lost, process teardown);
delegate->IsContextLost()reports this state, after which the async caller may have already returned early.
Root Cause Analysis
When a caller of asyncRescaleAndReadPixels* bails out early because the GPU context was lost, it may abandon the async read while ReadPixelsCallbackThreadSafe is still pending. The callback and its SkImage::ReadPixelsContext (old_context) can point at state — potentially stack-allocated by the now-returned caller — that is no longer valid. When the deferred ReadPixelsCallbackThreadSafe fires and invokes std::move(context->old_callback) with context->old_context, it dereferences that freed/invalid state, producing a use-after-free. The struct previously carried no way to detect that the owning context had been lost, so the callback ran unconditionally.
The fix threads a raw_ptr<GraphiteSharedContext::Delegate> delegate into AsyncReadContext and short-circuits the callback with if (context->delegate && context->delegate->IsContextLost()) { return; } before any user callback is dispatched.
Attack Path
- Trigger async read
Web content initiates a GPU operation that drives
asyncRescaleAndReadPixels*on the Graphite shared context, scheduling aReadPixelsCallbackThreadSafecompletion. - Induce context loss
Cause the GPU context to be lost (e.g. device reset), so
IsContextLost()becomes true and the caller of the async read returns early. - Abandon callback state
The early-returned caller tears down or unwinds the stack holding the
old_context/callback state that the pending completion still references. - Fire stale callback
The deferred
ReadPixelsCallbackThreadSaferuns and invokesold_callbackwith the now-freedold_context, dereferencing dangling memory. - Exploit UAF Reclaiming that freed region between steps 3 and 4 turns the stale dereference into a controlled use-after-free in the GPU process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pgpu/command_buffer/service/graphite_shared_context_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/graphite_shared_context.ccgpu/command_buffer/service/graphite_shared_context_unittest.cc
Audit Directions
- Async completion after early returnAudit every callback posted from a GPU/Graphite path for whether its owning caller can return early (context loss, teardown) while the completion is still pending; validate liveness at fire time, not only at schedule time.
- Caller-owned callback state lifetimeFlag
SkImageReadPixelsCallback/ReadPixelsContext-style opaque contexts that may reference stack or short-lived caller state; ensure their lifetime outlives, or is guarded against, deferred cross-thread dispatch. - `raw_ptr` liveness guardsReview other
AsyncReadContext-like structs to confirm they carry adelegate/context handle and checkIsContextLost()(or equivalent) before invoking user callbacks.
Patch
From b77d42d0e0c86a2de962a7df5bdc357dd31446a8 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
Date: Wed, 16 Sep 2026 08:45:41 -0700
Subject: [PATCH] Skip Graphite async read pixels callback when context is lost
Check delegate->IsContextLost() in ReadPixelsCallbackThreadSafe and
skip invoking the callback if the context has been lost. This avoids
a potential stack use-after-free in the callback when the caller of
asyncRescaleAndReadPixels* has already returned early due to context
loss.
Bug: 561997427
Change-Id: I3d58691fd035e6f68c708353d55494f3ea40eae0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8409528
Reviewed-by: Colin Blundell <blundell@chromium.org>
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
Cr-Commit-Position: refs/heads/main@{#1698970}
---
diff --git a/gpu/command_buffer/service/graphite_shared_context.cc b/gpu/command_buffer/service/graphite_shared_context.cc
index 44a5f62..23f09eb 100644
--- a/gpu/command_buffer/service/graphite_shared_context.cc
+++ b/gpu/command_buffer/service/graphite_shared_context.cc
@@ -94,12 +94,14 @@
struct AsyncReadContext {
GraphiteSharedContext::SkImageReadPixelsCallback old_callback;
SkImage::ReadPixelsContext old_context;
+ raw_ptr<GraphiteSharedContext::Delegate> delegate;
scoped_refptr<base::SingleThreadTaskRunner> task_runner;
};
void* CreateAsyncReadContextThreadSafe(
GraphiteSharedContext::SkImageReadPixelsCallback old_callback,
SkImage::ReadPixelsContext old_callbackContext,
+ GraphiteSharedContext::Delegate* delegate,
bool is_thread_safe) {
scoped_refptr<base::SingleThreadTaskRunner> task_runner =
is_thread_safe && base::SingleThreadTaskRunner::HasCurrentDefault()
@@ -108,7 +110,7 @@
// Wrapped the old callback with a new thread safe callback.
return new AsyncReadContext(std::move(old_callback), old_callbackContext,
- std::move(task_runner));
+ delegate, std::move(task_runner));
}
static void ReadPixelsCallbackThreadSafe(
@@ -119,13 +121,17 @@
return;
}
+ if (context->delegate && context->delegate->IsContextLost()) {
+ return;
+ }
+
// Ensure callbacks are called on the original thread if only one
// graphite::Context is created and is shared by multiple threads.
base::SingleThreadTaskRunner* task_runner = context->task_runner.get();
if (task_runner && !task_runner->BelongsToCurrentThread()) {
task_runner->PostTask(
FROM_HERE,
- base::BindOnce(std::move(context->old_callback), context->old_context,
+ base::BindOnce(&ReadPixelsCallbackThreadSafe, context.release(),
std::move(async_result)));
return;
}
@@ -466,7 +472,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixels(
src, dstImageInfo, srcRect, rescaleGamma, rescaleMode,
@@ -483,7 +489,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixels(
src, dstImageInfo, srcRect, rescaleGamma, rescaleMode,
@@ -500,7 +506,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixels(
src, dstImageInfo, srcRect, rescaleGamma, rescaleMode,
@@ -519,7 +525,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixels(
src, dstImageInfo, srcRect, rescaleGamma, rescaleMode,
@@ -540,7 +546,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixelsYUV420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -559,7 +565,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixelsYUV420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -578,7 +584,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixelsYUV420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -599,7 +605,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixelsYUV420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -620,7 +626,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixelsYUVA420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -639,7 +645,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
return graphite_context_->asyncRescaleAndReadPixelsYUVA420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -658,7 +664,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixelsYUVA420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
@@ -679,7 +685,7 @@
SkImage::ReadPixelsContext callbackContext) {
AutoLock auto_lock(this);
auto* new_callbackContext = CreateAsyncReadContextThreadSafe(
- std::move(callback), callbackContext, IsThreadSafe());
+ std::move(callback), callbackContext, delegate_, IsThreadSafe());
graphite_context_->asyncRescaleAndReadPixelsYUVA420(
src, yuvColorSpace, dstColorSpace, srcRect, dstSize, rescaleGamma,
diff --git a/gpu/command_buffer/service/graphite_shared_context_unittest.cc b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
index d76cddd..0e843f9 100644
--- a/gpu/command_buffer/service/graphite_shared_context_unittest.cc
+++ b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
@@ -4,6 +4,7 @@
#include "gpu/command_buffer/service/graphite_shared_context.h"
+#include "base/test/test_future.h"
#include "base/threading/thread.h"
#include "gpu/command_buffer/common/shm_count.h"
#include "gpu/command_buffer/service/skia_utils.h"
@@ -287,6 +288,42 @@
}
}
+// Test that async read pixels callbacks are skipped when the context is lost.
+TEST_P(GraphiteSharedContextTest, ReadPixelsContextLost) {
+ auto recorder = graphite_shared_context_->makeRecorder();
+ EXPECT_TRUE(recorder);
+
+ auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType);
+ auto surface = SkSurfaces::RenderTarget(recorder.get(), ii);
+ surface->getCanvas()->clear(SK_ColorRED);
+
+ auto recording = recorder->snap();
+ EXPECT_TRUE(recording);
+
+ skgpu::graphite::InsertRecordingInfo info = {};
+ info.fRecording = recording.get();
+ EXPECT_TRUE(graphite_shared_context_->insertRecording(info));
+
+ base::test::TestFuture<void*,
+ std::unique_ptr<const SkSurface::AsyncReadResult>>
+ readback_future;
+
+ // When context is not lost, the callback should be invoked.
+ EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(false));
+ EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit(
+ surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc,
+ SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr));
+ EXPECT_TRUE(readback_future.IsReady());
+
+ // When context is lost, the callback should not be invoked.
+ readback_future.Clear();
+ EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(true));
+ EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit(
+ surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc,
+ SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr));
+ EXPECT_FALSE(readback_future.IsReady());
+}
+
INSTANTIATE_TEST_SUITE_P(, GraphiteSharedContextTest, testing::Bool());
} // namespace
Regression Test / PoC
diff --git a/gpu/command_buffer/service/graphite_shared_context_unittest.cc b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
index d76cddd..0e843f9 100644
--- a/gpu/command_buffer/service/graphite_shared_context_unittest.cc
+++ b/gpu/command_buffer/service/graphite_shared_context_unittest.cc
@@ -4,6 +4,7 @@
#include "gpu/command_buffer/service/graphite_shared_context.h"
+#include "base/test/test_future.h"
#include "base/threading/thread.h"
#include "gpu/command_buffer/common/shm_count.h"
#include "gpu/command_buffer/service/skia_utils.h"
@@ -287,6 +288,42 @@
}
}
+// Test that async read pixels callbacks are skipped when the context is lost.
+TEST_P(GraphiteSharedContextTest, ReadPixelsContextLost) {
+ auto recorder = graphite_shared_context_->makeRecorder();
+ EXPECT_TRUE(recorder);
+
+ auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType);
+ auto surface = SkSurfaces::RenderTarget(recorder.get(), ii);
+ surface->getCanvas()->clear(SK_ColorRED);
+
+ auto recording = recorder->snap();
+ EXPECT_TRUE(recording);
+
+ skgpu::graphite::InsertRecordingInfo info = {};
+ info.fRecording = recording.get();
+ EXPECT_TRUE(graphite_shared_context_->insertRecording(info));
+
+ base::test::TestFuture<void*,
+ std::unique_ptr<const SkSurface::AsyncReadResult>>
+ readback_future;
+
+ // When context is not lost, the callback should be invoked.
+ EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(false));
+ EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit(
+ surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc,
+ SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr));
+ EXPECT_TRUE(readback_future.IsReady());
+
+ // When context is lost, the callback should not be invoked.
+ readback_future.Clear();
+ EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(true));
+ EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit(
+ surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc,
+ SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr));
+ EXPECT_FALSE(readback_future.IsReady());
+}
+
INSTANTIATE_TEST_SUITE_P(, GraphiteSharedContextTest, testing::Bool());
} // namespace