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 <[email protected]> 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 <[email protected]> Reviewed-by: Kyle Charbonneau <[email protected]> Commit-Queue: Quyen Le <[email protected]> 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