CVE-2026-79071
Overview
Background
- `DoRasterCHROMIUM`
- the GPU-process raster decoder routine that deserializes and replays a buffer of
PaintOps (from renderer shared memory) into anSkCanvas. - `GpuYieldRasterization`
- a
FEATURE_DISABLED_BY_DEFAULTfeature that let rasterization pause (“yield”) everykGpuYieldRasterizationOpCountops 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
RasterCHROMIUMcommand 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.
RasterCHROMIUM runs atomically.Attack Path
- Enable the yield path
With
GpuYieldRasterizationactive (disabled by default), a renderer issues a largeRasterCHROMIUMwhose op count exceedskGpuYieldRasterizationOpCount, soDoRasterCHROMIUMwill yield mid-buffer. - Force a defer
When
client()->ShouldYield()returns true, the decoder savesdeferred_raster_paint_buffer_offset_and returnskDeferCommandUntilLater, suspending rasterization mid-buffer. - 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). - 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
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
| Function | Change | Notes |
|---|---|---|
MockIsolationKeyProvidergpu/command_buffer/service/mocks.h |
modified | |
ScopedGLErrorSuppressorgpu/command_buffer/service/raster_decoder.cc |
modified | |
ifgpu/command_buffer/service/raster_decoder.cc |
modified |
Files Changed
gpu/command_buffer/service/command_buffer_service.hgpu/command_buffer/service/passthrough_program_cache_unittest.ccgpu/command_buffer/client/client_test_helper.ccgpu/command_buffer/service/raster_decoder.ccgpu/command_buffer/service/mocks.hgpu/command_buffer/service/program_manager_unittest.ccgpu/command_buffer/service/command_buffer_direct.hgpu/command_buffer/service/memory_program_cache_unittest.ccgpu/ipc/service/command_buffer_stub.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_base.ccgpu/command_buffer/service/raster_decoder_unittest_base.ccgpu/command_buffer/service/command_buffer_direct.ccgpu/command_buffer/build_raster_cmd_buffer.pygpu/command_buffer/service/raster_decoder_unittest.ccgpu/command_buffer/service/raster_decoder_autogen.htesting/variations/fieldtrial_testing_config.jsongpu/command_buffer/service/command_buffer_service.ccgpu/ipc/in_process_command_buffer.hgpu/command_buffer/service/gles2_cmd_decoder_unittest_base.hgpu/command_buffer/client/client_test_helper.hgpu/ipc/service/command_buffer_stub.hgpu/command_buffer/service/raster_decoder_unittest_base.hgpu/ipc/in_process_command_buffer.ccgpu/command_buffer/service/decoder_client.h
Audit Directions
- Resumable IPC command handlersAudit any command that returns
kDeferCommandUntilLaterand resumes from a stored offset into client shared memory, checking for stale-state / TOCTOU races across the yield. - State persisted across scheduler turnsLook for decoder members (offsets, locked handles, cached counts) that outlive a single command and are re-consumed after a pause.
- `Custom` command typesReview other
Customcommand-buffer entries whose custom handlers can defer or re-enter, since they bypass the normal atomic-per-command execution model.
Patch
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/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)
Regression Test / PoC
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>
Original Bug Report
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.ccgpu/command_buffer/service/command_buffer_service.ccgpu/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):
-
Initial Execution (Yield):
- A compromised renderer process prepares a large shared memory transfer buffer containing valid
cc::PaintOps. - The renderer issues a
RasterCHROMIUMcommand, setting the command argumentsraster_shm_idto the buffer andraster_shm_sizeto a large value (e.g., 65536 bytes) in the volatile command buffer. - The GPU process reads the
RasterCHROMIUMcommand.RasterDecoderImpl::HandleRasterCHROMIUMreads the arguments directly from the volatilecmd_dataprovided by the renderer. - The decoder calls
RasterDecoderImpl::DoRasterCHROMIUM(gpu/command_buffer/service/raster_decoder.cc:3067). DoRasterCHROMIUMmaps the transfer buffer usingGetSharedMemoryAs<char*>, validating that the 65536 size fits within the transfer buffer. The pointer is stored inpaint_buffer_memory.paint_buffer_sizeis initialized to the requestedraster_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) intodeferred_raster_paint_buffer_offset_(gpu/command_buffer/service/raster_decoder.cc:3047) and returnserror::kDeferCommandUntilLater(gpu/command_buffer/service/raster_decoder.cc:3049).
- A compromised renderer process prepares a large shared memory transfer buffer containing valid
-
The Race Window:
- Because the command was deferred, the command buffer’s
process_pospointer is not advanced (gpu/command_buffer/service/raster_decoder.cc:1545). TheRasterCHROMIUMcommand 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_sizefield 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).
- Because the command was deferred, the command buffer’s
-
Resume (The Vulnerability):
- When the command is resumed,
HandleRasterCHROMIUMis called again and re-reads the command arguments from the volatile shared memory. Crucially, it now reads the new, maliciously smallraster_shm_size(16). HandleRasterCHROMIUMcallsDoRasterCHROMIUMwith the new, small size.DoRasterCHROMIUMcallsGetSharedMemoryAs<char*>to validate the new size (16 bytes) against the smaller transfer buffer (32 bytes). This check passes.paint_buffer_sizeis initialized to the new, smallerraster_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). Becausepaint_buffer_sizeis an unsignedsize_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.
- When the command is resumed,
-
Exploitation (OOB Read):
- The decoder enters the
while (paint_buffer_size > 0)loop. - The OOB pointer and massive
paint_buffer_sizeare passed tocc::PaintOp::Deserialize. - The GPU process begins interpreting arbitrary heap memory as serialized
PaintOps. Sincepaint_buffer_sizeis 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
PaintOpheader and payload at the OOB location. For example, a fakeDrawImageOpcould reference a validgpu::Mailboxcorresponding to a cross-originSharedImage. - The deserializer successfully parses the fake
PaintOpand returns it. - The decoder calls
deserialized_op->Raster(raster_canvas_, playback_params), causing the cross-origin image to be drawn to the attacker-controlledraster_canvas_. - The renderer can execute a
ReadPixelscommand to read back the canvas’s contents, achieving a cross-origin information leak and bypassing the renderer sandbox.
- The decoder enters the
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.