Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker561997427
Fix commitb77d42d0e0c8 (chromium/src) +57/-14
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-09-15 (per vendor advisory)
Fix landed2026-09-16 (public git)
Disclosed2026-09-22
Patch gap6 days — fix public in source before the advisory

Background

Graphite
Skia’s next-generation GPU backend; GraphiteSharedContext wraps its shared GPU context inside Chrome’s GPU service.
`AsyncReadContext`
heap struct holding the original old_callback, old_context, and a task_runner so ReadPixelsCallbackThreadSafe can 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.

Key insight
Async GPU read-back completions must be validated against current context liveness at the moment they fire, because the caller who owns the callback state may have already unwound after a context-loss early return, leaving that state (possibly on the stack) dangling.

Attack Path

  1. Trigger async read Web content initiates a GPU operation that drives asyncRescaleAndReadPixels* on the Graphite shared context, scheduling a ReadPixelsCallbackThreadSafe completion.
  2. 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.
  3. 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.
  4. Fire stale callback The deferred ReadPixelsCallbackThreadSafe runs and invokes old_callback with the now-freed old_context, dereferencing dangling memory.
  5. 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

This is a use-after-free in the GPU process reachable through the Graphite async pixel read-back path. Because the callback state can be stack-allocated by a caller that returned early on context loss, an attacker who can trigger GPU context loss during a pending async read may corrupt memory, potentially leading to code execution within the GPU process.

Changed Functions

FunctionChangeNotes
TEST_P
gpu/command_buffer/service/graphite_shared_context_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/graphite_shared_context.cc
  • gpu/command_buffer/service/graphite_shared_context_unittest.cc

Audit Directions

  • Async completion after early return
    Audit 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 lifetime
    Flag 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 guards
    Review other AsyncReadContext-like structs to confirm they carry a delegate/context handle and check IsContextLost() (or equivalent) before invoking user callbacks.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.