Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in GPU
DescriptionRace condition in GPU
ComponentGPU
Bug ClassRace
Tracker496292729
Fix commit0838105ff3b5 (chromium/src) +51/-159
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
MockIsolationKeyProvider
gpu/command_buffer/service/mocks.h
modified

Files Changed

  • gpu/command_buffer/build_raster_cmd_buffer.py
  • gpu/command_buffer/client/client_test_helper.cc
  • gpu/command_buffer/client/client_test_helper.h
  • gpu/command_buffer/service/command_buffer_direct.cc
  • gpu/command_buffer/service/command_buffer_direct.h
  • gpu/command_buffer/service/command_buffer_service.cc
  • gpu/command_buffer/service/command_buffer_service.h
  • gpu/command_buffer/service/decoder_client.h
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_unittest_base.h
  • gpu/command_buffer/service/memory_program_cache_unittest.cc
  • gpu/command_buffer/service/mocks.h
  • gpu/command_buffer/service/passthrough_program_cache_unittest.cc
From 0838105ff3b5680e38a718317903cf59a2a5e7d3 Mon Sep 17 00:00:00 2001
From: kylechar <[email protected]>
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 <[email protected]>
Commit-Queue: Kyle Charbonneau <[email protected]>
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/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/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/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_; }
 
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 [email protected]

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