Medium watch CVSS 8.3 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in GPU
DescriptionRace condition in GPU
ComponentGPU
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker496292729
Fix commit0838105ff3b5 (chromium/src) +51/-159
CWECWE-367 (TOCTOU race)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-07-24 (public git)
Disclosed2026-08-25
Patch gap32 days — fix public in source before the advisory

Background

`DoRasterCHROMIUM`
the GPU-process raster decoder routine that deserializes and replays a buffer of PaintOps (from renderer shared memory) into an SkCanvas.
`GpuYieldRasterization`
a FEATURE_DISABLED_BY_DEFAULT feature that let rasterization pause (“yield”) every kGpuYieldRasterizationOpCount ops to let the GPU scheduler run other work.
`ShouldYield()` / `deferred_raster_paint_buffer_offset_`
the yield check propagated up the decoder-client stack, and the stored offset recording how far into the paint buffer processing got before deferring.
`error::kDeferCommandUntilLater`
the command-buffer return value that reschedules the same RasterCHROMIUM command to resume later from the saved offset.

Root Cause Analysis

With GpuYieldRasterization enabled, DoRasterCHROMIUM could stop mid-buffer, store deferred_raster_paint_buffer_offset_ = raster_shm_size - paint_buffer.size(), and return kDeferCommandUntilLater; on resume it re-fetched the paint buffer from renderer shared memory and advanced by the saved offset (paint_buffer = paint_buffer.subspan(*deferred_raster_paint_buffer_offset_)). Splitting a single rasterization across scheduler turns left decoder state (the stored offset, locked font handles, partially-played canvas) live across a yield window during which the client-owned shared-memory paint buffer could change, so the resume applied a stale offset/state to possibly-different data — a race condition in the GPU process. The RasterCHROMIUM command was a Custom type precisely so its handler could return the defer code and be re-entered.

The fix deletes the entire yield mechanism: it removes the kGpuYieldRasterization feature and op-count param, the deferred_raster_paint_buffer_offset_ / check_for_yield_op_count_ members and their resume logic, the ShouldYield() method across the whole DecoderClient hierarchy, and makes RasterCHROMIUM a normal (non-Custom) command whose DoRasterCHROMIUM now returns void and always runs to completion. This works because rasterization becomes atomic within one command, eliminating the cross-turn state that could race.

Key insight
The core mistake was letting rasterization yield and later resume from a stored offset over renderer-controlled shared memory, keeping raceable state across scheduler turns; the fix removes the yield feature entirely so each RasterCHROMIUM runs atomically.

Attack Path

  1. Enable the yield path With GpuYieldRasterization active (disabled by default), a renderer issues a large RasterCHROMIUM whose op count exceeds kGpuYieldRasterizationOpCount, so DoRasterCHROMIUM will yield mid-buffer.
  2. Force a defer When client()->ShouldYield() returns true, the decoder saves deferred_raster_paint_buffer_offset_ and returns kDeferCommandUntilLater, suspending rasterization mid-buffer.
  3. Mutate state during the yield window Between defer and resume, the renderer-controlled shared-memory paint buffer (and related font/handle state) can be changed while the decoder holds only the saved offset (inferred).
  4. Resume on mismatched data The rescheduled command re-fetches the buffer and applies the stale deferred_raster_paint_buffer_offset_ subspan, replaying ops against inconsistent data — the race the fix eliminates (inferred).

Impact Assessment

An attacker driving raster commands from the renderer could, when the yield feature was on, provoke a race in the GPU process by mutating shared state across a mid-rasterization yield; the advisory classifies this as a race condition in GPU. The key precondition is the non-default GpuYieldRasterization feature being enabled, which is why the fix removes it wholesale rather than adding a guard. No exploited-in-the-wild status is stated, and any escalation to memory corruption or code execution is (inferred).

Changed Functions

FunctionChangeNotes
MockIsolationKeyProvider
gpu/command_buffer/service/mocks.h
modified
ScopedGLErrorSuppressor
gpu/command_buffer/service/raster_decoder.cc
modified
if
gpu/command_buffer/service/raster_decoder.cc
modified

Files Changed

  • gpu/command_buffer/service/command_buffer_service.h
  • gpu/command_buffer/service/passthrough_program_cache_unittest.cc
  • gpu/command_buffer/client/client_test_helper.cc
  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/mocks.h
  • gpu/command_buffer/service/program_manager_unittest.cc
  • gpu/command_buffer/service/command_buffer_direct.h
  • gpu/command_buffer/service/memory_program_cache_unittest.cc
  • gpu/ipc/service/command_buffer_stub.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
  • gpu/command_buffer/service/raster_decoder_unittest_base.cc
  • gpu/command_buffer/service/command_buffer_direct.cc
  • gpu/command_buffer/build_raster_cmd_buffer.py
  • gpu/command_buffer/service/raster_decoder_unittest.cc
  • gpu/command_buffer/service/raster_decoder_autogen.h
  • testing/variations/fieldtrial_testing_config.json
  • gpu/command_buffer/service/command_buffer_service.cc
  • gpu/ipc/in_process_command_buffer.h
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
  • gpu/command_buffer/client/client_test_helper.h
  • gpu/ipc/service/command_buffer_stub.h
  • gpu/command_buffer/service/raster_decoder_unittest_base.h
  • gpu/ipc/in_process_command_buffer.cc
  • gpu/command_buffer/service/decoder_client.h

Audit Directions

  • Resumable IPC command handlers
    Audit any command that returns kDeferCommandUntilLater and resumes from a stored offset into client shared memory, checking for stale-state / TOCTOU races across the yield.
  • State persisted across scheduler turns
    Look for decoder members (offsets, locked handles, cached counts) that outlive a single command and are re-consumed after a pause.
  • `Custom` command types
    Review other Custom command-buffer entries whose custom handlers can defer or re-enter, since they bypass the normal atomic-per-command execution model.
From 0838105ff3b5680e38a718317903cf59a2a5e7d3 Mon Sep 17 00:00:00 2001
From: kylechar <kylechar@chromium.org>
Date: Fri, 24 Jul 2026 08:45:30 -0700
Subject: [PATCH] Remove GpuYieldRasterization feature

It's not enabled and hasn't been experimented with in over a year.

Fixed: 358591562, 496292729
Change-Id: If546e20bb58b06f679daf0036a31cf8590e3d053
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8139282
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1667883}
---
diff --git a/gpu/command_buffer/build_raster_cmd_buffer.py b/gpu/command_buffer/build_raster_cmd_buffer.py
index 81c1464..77495082 100755
--- a/gpu/command_buffer/build_raster_cmd_buffer.py
+++ b/gpu/command_buffer/build_raster_cmd_buffer.py
@@ -298,7 +298,6 @@
   },
   'RasterCHROMIUM': {
     'decoder_func': 'DoRasterCHROMIUM',
-    'type': 'Custom',
     'internal': True,
     'impl_func': True,
     'cmd_args': 'GLuint raster_shm_id, GLuint raster_shm_offset,'
diff --git a/gpu/command_buffer/client/client_test_helper.cc b/gpu/command_buffer/client/client_test_helper.cc
index 40bd2d1..14a59bfc 100644
--- a/gpu/command_buffer/client/client_test_helper.cc
+++ b/gpu/command_buffer/client/client_test_helper.cc
@@ -195,8 +195,5 @@
 void FakeDecoderClient::ScheduleGrContextCleanup() {}
 void FakeDecoderClient::SetActiveURL(GURL) {}
 void FakeDecoderClient::HandleReturnData(base::span<const uint8_t>) {}
-bool FakeDecoderClient::ShouldYield() {
-  return false;
-}
 
 }  // namespace gpu
diff --git a/gpu/command_buffer/client/client_test_helper.h b/gpu/command_buffer/client/client_test_helper.h
index 82c3fcd..5c5fe62 100644
--- a/gpu/command_buffer/client/client_test_helper.h
+++ b/gpu/command_buffer/client/client_test_helper.h
@@ -171,7 +171,6 @@
   void ScheduleGrContextCleanup() override;
   void SetActiveURL(GURL url) override;
   void HandleReturnData(base::span<const uint8_t> data) override;
-  bool ShouldYield() override;
 };
 
 }  // namespace gpu
diff --git a/gpu/command_buffer/service/command_buffer_direct.cc b/gpu/command_buffer/service/command_buffer_direct.cc
index 2286ca8..25b5e533 100644
--- a/gpu/command_buffer/service/command_buffer_direct.cc
+++ b/gpu/command_buffer/service/command_buffer_direct.cc
@@ -107,8 +107,4 @@
   NOTIMPLEMENTED();
 }
 
-bool CommandBufferDirect::ShouldYield() {
-  return service_.ShouldYield();
-}
-
 }  // namespace gpu
diff --git a/gpu/command_buffer/service/command_buffer_direct.h b/gpu/command_buffer/service/command_buffer_direct.h
index 33687fb5..2967fcf 100644
--- a/gpu/command_buffer/service/command_buffer_direct.h
+++ b/gpu/command_buffer/service/command_buffer_direct.h
@@ -58,7 +58,6 @@
   void OnRescheduleAfterFinished() override;
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override;
-  bool ShouldYield() override;
 
   scoped_refptr<Buffer> CreateTransferBufferWithId(uint32_t size, int32_t id);
 
diff --git a/gpu/command_buffer/service/command_buffer_service.cc b/gpu/command_buffer/service/command_buffer_service.cc
index 7b86499..34f61fb 100644
--- a/gpu/command_buffer/service/command_buffer_service.cc
+++ b/gpu/command_buffer/service/command_buffer_service.cc
@@ -418,11 +418,6 @@
   state_.context_lost_reason = reason;
 }
 
-bool CommandBufferService::ShouldYield() {
-  return client_->OnCommandBatchProcessed() ==
-         CommandBufferServiceClient::kPauseExecution;
-}
-
 void CommandBufferService::SetScheduled(bool scheduled) {
   TRACE_EVENT2("gpu", "CommandBufferService:SetScheduled", "this",
                static_cast<void*>(this), "scheduled", scheduled);
diff --git a/gpu/command_buffer/service/command_buffer_service.h b/gpu/command_buffer/service/command_buffer_service.h
index fc8afd6f..cde2c2e 100644
--- a/gpu/command_buffer/service/command_buffer_service.h
+++ b/gpu/command_buffer/service/command_buffer_service.h
@@ -137,8 +137,6 @@
 
   size_t GetSharedMemoryBytesAllocated() const;
 
-  bool ShouldYield();
-
  private:
   raw_ptr<CommandBufferServiceClient> client_;
   std::unique_ptr<TransferBufferManager> transfer_buffer_manager_;
diff --git a/gpu/command_buffer/service/decoder_client.h b/gpu/command_buffer/service/decoder_client.h
index a6f78f4f..5245dce 100644
--- a/gpu/command_buffer/service/decoder_client.h
+++ b/gpu/command_buffer/service/decoder_client.h
@@ -55,9 +55,6 @@
 
   // Called by the decoder to pass a variable-size block of data to the client.
   virtual void HandleReturnData(base::span<const uint8_t> data) = 0;
-
-  // Returns true if rasterization should yield.
-  virtual bool ShouldYield() = 0;
 };
 
 }  // namespace gpu
diff --git a/gpu/command_buffer/service/mocks.h b/gpu/command_buffer/service/mocks.h
index e4520df74..872b98a 100644
--- a/gpu/command_buffer/service/mocks.h
+++ b/gpu/command_buffer/service/mocks.h
@@ -116,7 +116,6 @@
   MOCK_METHOD(void, ScheduleGrContextCleanup, ());
   MOCK_METHOD(void, SetActiveURL, (GURL url));
   MOCK_METHOD(void, HandleReturnData, (base::span<const uint8_t> data));
-  MOCK_METHOD(bool, ShouldYield, ());
 };
 
 class MockIsolationKeyProvider : public IsolationKeyProvider {
diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc
index a9a7ce8..a2eb09f8 100644
--- a/gpu/command_buffer/service/raster_decoder.cc
+++ b/gpu/command_buffer/service/raster_decoder.cc
@@ -21,7 +21,6 @@
 #include "base/compiler_specific.h"
 #include "base/containers/flat_map.h"
 #include "base/debug/crash_logging.h"
-#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/logging.h"
 #include "base/memory/aligned_memory.h"
@@ -143,15 +142,6 @@
 
 base::AtomicSequenceNumber g_raster_decoder_id;
 
-// Controls whether we may yield during rasterization.
-BASE_FEATURE(kGpuYieldRasterization, base::FEATURE_DISABLED_BY_DEFAULT);
-
-// Controls how many ops are rastered before checking if we should yield.
-const base::FeatureParam<int> kGpuYieldRasterizationOpCount(
-    &kGpuYieldRasterization,
-    "gpu_yield_rasterization_op_count",
-    500);
-
 // This class prevents any GL errors that occur when it is in scope from
 // being reported to the client.
 class ScopedGLErrorSuppressor {
@@ -780,12 +770,12 @@
                              GLboolean visible,
                              GLfloat hdr_headroom,
                              const volatile GLbyte* key);
-  error::Error DoRasterCHROMIUM(GLuint raster_shm_id,
-                                GLuint raster_shm_offset,
-                                GLuint raster_shm_size,
-                                GLuint font_shm_id,
-                                GLuint font_shm_offset,
-                                GLuint font_shm_size);
+  void DoRasterCHROMIUM(GLuint raster_shm_id,
+                        GLuint raster_shm_offset,
+                        GLuint raster_shm_size,
+                        GLuint font_shm_id,
+                        GLuint font_shm_offset,
+                        GLuint font_shm_size);
   void DoEndRasterCHROMIUM();
   void DoFlushTileRasterGraphiteCommandsCHROMIUM();
   void DoCreateTransferCacheEntryINTERNAL(GLuint entry_type,
@@ -917,14 +907,6 @@
   raw_ptr<SkCanvas> raster_canvas_ = nullptr;
   std::vector<SkDiscardableHandleId> locked_handles_;
 
-  // Cached value of `kGpuYieldRasterizationOpCount`. This is only set if
-  // `kGpuYieldRasterization` is enabled.
-  std::optional<int> check_for_yield_op_count_;
-
-  // If set, indicates rasterization was deferred. The value gives how far into
-  // the buffer was processed.
-  std::optional<size_t> deferred_raster_paint_buffer_offset_;
-
   // Tracing helpers.
   int raster_chromium_id_ = 0;
 
@@ -1048,9 +1030,6 @@
   if (cmdline->HasSwitch(switches::kDisableGLDrawingForTests)) {
     no_draw_canvas_ = std::make_unique<SkNoDrawCanvas>(0, 0);
   }
-  if (base::FeatureList::IsEnabled(kGpuYieldRasterization)) {
-    check_for_yield_op_count_ = kGpuYieldRasterizationOpCount.Get();
-  }
 }
 
 RasterDecoderImpl::~RasterDecoderImpl() {
@@ -2976,19 +2955,19 @@
     shared_context_state_->progress_reporter()->ReportProgress();
 }
 
-error::Error RasterDecoderImpl::DoRasterCHROMIUM(GLuint raster_shm_id,
-                                                 GLuint raster_shm_offset,
-                                                 GLuint raster_shm_size,
-                                                 GLuint font_shm_id,
-                                                 GLuint font_shm_offset,
-                                                 GLuint font_shm_size) {
+void RasterDecoderImpl::DoRasterCHROMIUM(GLuint raster_shm_id,
+                                         GLuint raster_shm_offset,
+                                         GLuint raster_shm_size,
+                                         GLuint font_shm_id,
+                                         GLuint font_shm_offset,
+                                         GLuint font_shm_size) {
   TRACE_EVENT1("gpu", "RasterDecoderImpl::DoRasterCHROMIUM", "raster_id",
                ++raster_chromium_id_);
 
   if (!sk_surface_ && !scoped_shared_image_raster_write_) {
     LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, "glRasterCHROMIUM",
                        "RasterCHROMIUM without BeginRasterCHROMIUM");
-    return error::kNoError;
+    return;
   }
   DCHECK(transfer_cache());
 
@@ -2997,14 +2976,14 @@
   if (paint_buffer_opt.value_or({}).empty()) {
     LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
                        "Can not read paint buffer.");
-    return error::kNoError;
+    return;
   }
   base::span<uint8_t> paint_buffer = *paint_buffer_opt;
 
   if (!base::IsAligned(paint_buffer.data(), 16u)) {
     LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
                        "Buffer is not aligned with 16 bytes.");
-    return error::kNoError;
+    return;
   }
 
   cc::PlaybackParams playback_params(nullptr, SkM44());
@@ -3029,41 +3008,32 @@
   TRACE_EVENT0("gpu", "RasterDecoderImpl::DoRasterCHROMIUM::Deserializing");
 
   if (scoped_shared_image_raster_write_) {
-    DCHECK(!deferred_raster_paint_buffer_offset_.has_value());
     auto* paint_op_buffer =
         scoped_shared_image_raster_write_->paint_op_buffer();
     paint_op_buffer->Deserialize(paint_buffer, options);
-    return error::kNoError;
+    return;
   }
 
-  if (deferred_raster_paint_buffer_offset_.has_value()) {
-    CHECK(*deferred_raster_paint_buffer_offset_ <= paint_buffer.size());
-    paint_buffer = paint_buffer.subspan(*deferred_raster_paint_buffer_offset_);
-    deferred_raster_paint_buffer_offset_.reset();
-  } else {
-    if (font_shm_size > 0) {
-      // Deserialize fonts before raster.
-      auto font_buffer_opt = GetSharedMemoryAsSpan<volatile uint8_t>(
-          font_shm_id, font_shm_offset, font_shm_size);
-      if (font_buffer_opt.value_or({}).empty()) {
-        LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
-                           "Can not read font buffer.");
-        return error::kNoError;
-      }
-      base::span<volatile uint8_t> font_buffer = *font_buffer_opt;
-
-      std::vector<SkDiscardableHandleId> new_locked_handles;
-      if (!font_manager_->Deserialize(font_buffer, &new_locked_handles)) {
-        LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
-                           "Invalid font buffer.");
-        return error::kNoError;
-      }
-      locked_handles_.insert(locked_handles_.end(), new_locked_handles.begin(),
-                             new_locked_handles.end());
+  if (font_shm_size > 0) {
+    // Deserialize fonts before raster.
+    auto font_buffer_opt = GetSharedMemoryAsSpan<volatile uint8_t>(
+        font_shm_id, font_shm_offset, font_shm_size);
+    if (font_buffer_opt.value_or({}).empty()) {
+      LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
+                         "Can not read font buffer.");
+      return;
     }
-  }
+    base::span<volatile uint8_t> font_buffer = *font_buffer_opt;
 
-  size_t processed_commands = 0;
+    std::vector<SkDiscardableHandleId> new_locked_handles;
+    if (!font_manager_->Deserialize(font_buffer, &new_locked_handles)) {
+      LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glRasterCHROMIUM",
+                         "Invalid font buffer.");
+      return;
+    }
+    locked_handles_.insert(locked_handles_.end(), new_locked_handles.begin(),
+                           new_locked_handles.end());
+  }
 
   while (!paint_buffer.empty()) {
     size_t skip = 0;
@@ -3072,42 +3042,14 @@
     if (!deserialized_op) {
       LOCAL_SET_GL_ERROR(GL_INVALID_OPERATION, "glRasterCHROMIUM",
                          "RasterCHROMIUM: serialization failure");
-      return error::kNoError;
+      return;
     }
 
     deserialized_op->Raster(raster_canvas_, playback_params);
     deserialized_op->DestroyThis();
 
     paint_buffer = paint_buffer.subspan(skip);
-    processed_commands++;
-
-    if (check_for_yield_op_count_.has_value() &&
-        processed_commands % check_for_yield_op_count_.value() == 0 &&
-        !paint_buffer.empty() && client()->ShouldYield()) {
-      // Pause command batch to check if we should yield execution.
-      TRACE_EVENT0("gpu", "RasterDecoderImpl::DoRasterCHROMIUM::Yield");
-      deferred_raster_paint_buffer_offset_ =
-          raster_shm_size - paint_buffer.size();
-      return error::kDeferCommandUntilLater;
-    }
   }
-
-  return error::kNoError;
-}
-
-error::Error RasterDecoderImpl::HandleRasterCHROMIUM(
-    uint32_t immediate_data_size,
-    const volatile void* cmd_data) {
-  const volatile raster::cmds::RasterCHROMIUM& c =
-      *static_cast<const volatile raster::cmds::RasterCHROMIUM*>(cmd_data);
-  GLuint raster_shm_id = static_cast<GLuint>(c.raster_shm_id);
-  GLuint raster_shm_offset = static_cast<GLuint>(c.raster_shm_offset);
-  GLuint raster_shm_size = static_cast<GLuint>(c.raster_shm_size);
-  GLuint font_shm_id = static_cast<GLuint>(c.font_shm_id);
-  GLuint font_shm_offset = static_cast<GLuint>(c.font_shm_offset);
-  GLuint font_shm_size = static_cast<GLuint>(c.font_shm_size);
-  return DoRasterCHROMIUM(raster_shm_id, raster_shm_offset, raster_shm_size,
-                          font_shm_id, font_shm_offset, font_shm_size);
 }
 
 void RasterDecoderImpl::DoEndRasterCHROMIUM() {
diff --git a/gpu/ipc/in_process_command_buffer.cc b/gpu/ipc/in_process_command_buffer.cc
index 115e4d67..0bc3c6eb4 100644
--- a/gpu/ipc/in_process_command_buffer.cc
+++ b/gpu/ipc/in_process_command_buffer.cc
@@ -896,10 +896,6 @@
                      client_thread_weak_ptr_, std::move(vec)));
 }
 
-bool InProcessCommandBuffer::ShouldYield() {
-  return task_sequence_->ShouldYield();
-}
-
 void InProcessCommandBuffer::PostOrRunClientCallback(
     base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(gpu_sequence_checker_);
diff --git a/gpu/ipc/in_process_command_buffer.h b/gpu/ipc/in_process_command_buffer.h
index 9fd63bc..0e8ce00 100644
--- a/gpu/ipc/in_process_command_buffer.h
+++ b/gpu/ipc/in_process_command_buffer.h
@@ -156,7 +156,6 @@
   void OnRescheduleAfterFinished() override;
   void ScheduleGrContextCleanup() override;
   void HandleReturnData(base::span<const uint8_t> data) override;
-  bool ShouldYield() override;
 
   const gles2::FeatureInfo* GetFeatureInfo() const;
 
diff --git a/gpu/ipc/service/command_buffer_stub.cc b/gpu/ipc/service/command_buffer_stub.cc
index e2e94b1..67abe95 100644
--- a/gpu/ipc/service/command_buffer_stub.cc
+++ b/gpu/ipc/service/command_buffer_stub.cc
@@ -633,10 +633,6 @@
   client_->OnReturnData(std::vector<uint8_t>(data.begin(), data.end()));
 }
 
-bool CommandBufferStub::ShouldYield() {
-  return channel_->scheduler()->ShouldYield(sequence_id_);
-}
-
 void CommandBufferStub::OnConsoleMessage(int32_t id,
                                          const std::string& message) {
   client_->OnConsoleMessage(message);
diff --git a/gpu/ipc/service/command_buffer_stub.h b/gpu/ipc/service/command_buffer_stub.h
index 4dc2341..f3bd4c8a 100644
--- a/gpu/ipc/service/command_buffer_stub.h
+++ b/gpu/ipc/service/command_buffer_stub.h
@@ -150,7 +150,6 @@
   void OnRescheduleAfterFinished() override;
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
index 00d3e85f..381bbcd5 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
@@ -154,9 +154,6 @@
 void GLES2DecoderTestBase::OnFenceSyncRelease(uint64_t release) {}
 void GLES2DecoderTestBase::OnDescheduleUntilFinished() {}
 void GLES2DecoderTestBase::OnRescheduleAfterFinished() {}
-bool GLES2DecoderTestBase::ShouldYield() {
-  return false;
-}
 
 void GLES2DecoderTestBase::SetUp() {
   InitState init;
@@ -2298,9 +2295,6 @@
 void GLES2DecoderPassthroughTestBase::OnFenceSyncRelease(uint64_t release) {}
 void GLES2DecoderPassthroughTestBase::OnDescheduleUntilFinished() {}
 void GLES2DecoderPassthroughTestBase::OnRescheduleAfterFinished() {}
-bool GLES2DecoderPassthroughTestBase::ShouldYield() {
-  return false;
-}
 
 void GLES2DecoderPassthroughTestBase::SetUp() {
   base::CommandLine::Init(0, nullptr);
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
index daba28d..d7270d35 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
@@ -67,7 +67,6 @@
   void OnRescheduleAfterFinished() override;
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override;
 
   // Template to call glGenXXX functions.
   template <typename T>
@@ -839,7 +838,6 @@
   void OnRescheduleAfterFinished() override;
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override;
 
   void SetUp() override;
   void TearDown() override;
diff --git a/gpu/command_buffer/service/memory_program_cache_unittest.cc b/gpu/command_buffer/service/memory_program_cache_unittest.cc
index cb19559d..b8fc3ea9 100644
--- a/gpu/command_buffer/service/memory_program_cache_unittest.cc
+++ b/gpu/command_buffer/service/memory_program_cache_unittest.cc
@@ -134,7 +134,6 @@
   void OnRescheduleAfterFinished() override {}
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override { return false; }
 
   int32_t shader_cache_count() { return shader_cache_count_; }
   const std::string& shader_cache_shader() { return shader_cache_shader_; }
diff --git a/gpu/command_buffer/service/passthrough_program_cache_unittest.cc b/gpu/command_buffer/service/passthrough_program_cache_unittest.cc
index 3cd9ebd..aca83ae 100644
--- a/gpu/command_buffer/service/passthrough_program_cache_unittest.cc
+++ b/gpu/command_buffer/service/passthrough_program_cache_unittest.cc
@@ -55,7 +55,6 @@
   void OnRescheduleAfterFinished() override {}
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override { return false; }
 
   int32_t blob_count() { return blob_count_; }
diff --git a/gpu/command_buffer/service/program_manager_unittest.cc b/gpu/command_buffer/service/program_manager_unittest.cc
index b744ff3b..d91b859 100644
--- a/gpu/command_buffer/service/program_manager_unittest.cc
+++ b/gpu/command_buffer/service/program_manager_unittest.cc
@@ -98,7 +98,6 @@
   void OnRescheduleAfterFinished() override {}
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override { return false; }
 
   std::unique_ptr<ProgramManager> manager_;
   GpuPreferences gpu_preferences_;
diff --git a/gpu/command_buffer/service/raster_decoder_unittest.cc b/gpu/command_buffer/service/raster_decoder_unittest.cc
index 038e47c..e039ad8 100644
--- a/gpu/command_buffer/service/raster_decoder_unittest.cc
+++ b/gpu/command_buffer/service/raster_decoder_unittest.cc
@@ -256,7 +256,6 @@
   void OnRescheduleAfterFinished() override {}
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override { return false; }
 
   std::unique_ptr<RasterDecoder> CreateDecoder() {
     command_buffer_service_ = std::make_unique<FakeCommandBufferServiceBase>();
diff --git a/gpu/command_buffer/service/raster_decoder_unittest_base.cc b/gpu/command_buffer/service/raster_decoder_unittest_base.cc
index d6ed512..bfa66c4a 100644
--- a/gpu/command_buffer/service/raster_decoder_unittest_base.cc
+++ b/gpu/command_buffer/service/raster_decoder_unittest_base.cc
@@ -76,9 +76,6 @@
 void RasterDecoderTestBase::OnFenceSyncRelease(uint64_t release) {}
 void RasterDecoderTestBase::OnDescheduleUntilFinished() {}
 void RasterDecoderTestBase::OnRescheduleAfterFinished() {}
-bool RasterDecoderTestBase::ShouldYield() {
-  return false;
-}
 
 void RasterDecoderTestBase::SetUp() {
   InitDecoder(InitState());
diff --git a/gpu/command_buffer/service/raster_decoder_unittest_base.h b/gpu/command_buffer/service/raster_decoder_unittest_base.h
index 698cfae1..06bd28c 100644
--- a/gpu/command_buffer/service/raster_decoder_unittest_base.h
+++ b/gpu/command_buffer/service/raster_decoder_unittest_base.h
@@ -52,7 +52,6 @@
   void OnRescheduleAfterFinished() override;
   void ScheduleGrContextCleanup() override {}
   void HandleReturnData(base::span<const uint8_t> data) override {}
-  bool ShouldYield() override;
 
   // Template to call glGenXXX functions.
   template <typename T>
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential TOCTOU and size_t underflow in RasterDecoderImpl::DoRasterCHROMIUM leads to OOB Read

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A TOCTOU vulnerability in RasterDecoderImpl::DoRasterCHROMIUM allows a compromised renderer to cause a size_t underflow and OOB pointer calculation when GPU rasterization yielding is enabled. This results in the GPU process deserializing and executing arbitrary heap memory as PaintOps, leading to an unbounded OOB read and potential cross-origin information leak.

Affected files:

  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/command_buffer_service.cc
  • gpu/ipc/service/gpu_channel.cc

Estimated timestamp from git blame: 2025-11-28

Summary

When the kGpuYieldRasterization feature is enabled (e.g., via Finch), RasterDecoderImpl::DoRasterCHROMIUM can yield execution mid-command if the scheduler indicates it should pause rasterization. To support resuming the command later, it saves a byte offset into deferred_raster_paint_buffer_offset_. However, on resumption, the command arguments (including the shared memory size) are re-read from renderer-writable volatile shared memory. A compromised renderer can shrink the raster_shm_size between the yield and the resume, causing a size_t underflow and an out-of-bounds (OOB) pointer calculation.

Technical Details

This is a Time-of-Check to Time-of-Use (TOCTOU) vulnerability existing in the interaction between the command buffer’s deferral logic and the handling of the RasterCHROMIUM command in gpu/command_buffer/service/raster_decoder.cc.

Step-by-step Exploitation Scenario (Suggested):

  1. Initial Execution (Yield):

    • A compromised renderer process prepares a large shared memory transfer buffer containing valid cc::PaintOps.
    • The renderer issues a RasterCHROMIUM command, setting the command arguments raster_shm_id to the buffer and raster_shm_size to a large value (e.g., 65536 bytes) in the volatile command buffer.
    • The GPU process reads the RasterCHROMIUM command. RasterDecoderImpl::HandleRasterCHROMIUM reads the arguments directly from the volatile cmd_data provided by the renderer.
    • The decoder calls RasterDecoderImpl::DoRasterCHROMIUM (gpu/command_buffer/service/raster_decoder.cc:3067).
    • DoRasterCHROMIUM maps the transfer buffer using GetSharedMemoryAs<char*>, validating that the 65536 size fits within the transfer buffer. The pointer is stored in paint_buffer_memory.
    • paint_buffer_size is initialized to the requested raster_shm_size (65536).
    • The decoder begins processing paint operations in a while (paint_buffer_size > 0) loop.
    • If client()->ShouldYield() returns true (e.g., after processing 64000 bytes), the decoder saves the current offset (raster_shm_size - paint_buffer_size, which is 64000) into deferred_raster_paint_buffer_offset_ (gpu/command_buffer/service/raster_decoder.cc:3047) and returns error::kDeferCommandUntilLater (gpu/command_buffer/service/raster_decoder.cc:3049).
  2. The Race Window:

    • Because the command was deferred, the command buffer’s process_pos pointer is not advanced (gpu/command_buffer/service/raster_decoder.cc:1545). The RasterCHROMIUM command remains in the command buffer to be re-executed on the next pass.
    • The GPU process yields back to the message loop.
    • The compromised renderer observes the stall and modifies the raster_shm_size field in the volatile command buffer to a much smaller value (e.g., 16).
    • The attacker also shrinks the underlying transfer buffer or reallocates it to a smaller size (e.g., 32 bytes).
  3. Resume (The Vulnerability):

    • When the command is resumed, HandleRasterCHROMIUM is called again and re-reads the command arguments from the volatile shared memory. Crucially, it now reads the new, maliciously small raster_shm_size (16).
    • HandleRasterCHROMIUM calls DoRasterCHROMIUM with the new, small size.
    • DoRasterCHROMIUM calls GetSharedMemoryAs<char*> to validate the new size (16 bytes) against the smaller transfer buffer (32 bytes). This check passes.
    • paint_buffer_size is initialized to the new, smaller raster_shm_size (16).
    • The decoder checks deferred_raster_paint_buffer_offset_.has_value(). Since it was set during the yield, this is true.
    • Underflow: The decoder subtracts the saved offset (64000) from the new, small paint_buffer_size (16): paint_buffer_size -= *deferred_raster_paint_buffer_offset_; (gpu/command_buffer/service/raster_decoder.cc:2997). Because paint_buffer_size is an unsigned size_t, this results in an integer underflow, producing a massive positive value (approximately 2^64).
    • OOB Pointer: The decoder advances the pointer by the offset: paint_buffer_memory += *deferred_raster_paint_buffer_offset_; (gpu/command_buffer/service/raster_decoder.cc:2998). Because the new transfer buffer is small, this pointer is pushed far out-of-bounds, landing deep into the GPU process heap.
  4. Exploitation (OOB Read):

    • The decoder enters the while (paint_buffer_size > 0) loop.
    • The OOB pointer and massive paint_buffer_size are passed to cc::PaintOp::Deserialize.
    • The GPU process begins interpreting arbitrary heap memory as serialized PaintOps. Since paint_buffer_size is effectively unbounded, it will keep reading until it crashes or successfully deserializes ops.
    • By controlling the GPU process heap layout, an attacker can position a fake PaintOp header and payload at the OOB location. For example, a fake DrawImageOp could reference a valid gpu::Mailbox corresponding to a cross-origin SharedImage.
    • The deserializer successfully parses the fake PaintOp and returns it.
    • The decoder calls deserialized_op->Raster(raster_canvas_, playback_params), causing the cross-origin image to be drawn to the attacker-controlled raster_canvas_.
    • The renderer can execute a ReadPixels command to read back the canvas’s contents, achieving a cross-origin information leak and bypassing the renderer sandbox.

Impact

This is a high-severity (S1) vulnerability in the GPU process. While kGpuYieldRasterization is an active experiment (e.g., via Finch), when enabled, a compromised renderer can leverage this TOCTOU bug to escape the renderer sandbox and leak sensitive cross-origin information from the GPU process.

Suggested Fix

In RasterDecoderImpl::DoRasterCHROMIUM, validate that *deferred_raster_paint_buffer_offset_ <= raster_shm_size before applying the offset upon resumption. If the offset is greater than the newly read raster_shm_size, it indicates a hostile modification of the command buffer arguments between the yield and the resume, and the command should be aborted with an error (e.g., error::kInvalidArguments).

Evaluated with Chrome root at commit: a3f5fcb392f2902650ca2b71820e7e418787e18b


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker