CVE-2026-79071
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
MockIsolationKeyProvidergpu/command_buffer/service/mocks.h |
modified |
Files Changed
gpu/command_buffer/build_raster_cmd_buffer.pygpu/command_buffer/client/client_test_helper.ccgpu/command_buffer/client/client_test_helper.hgpu/command_buffer/service/command_buffer_direct.ccgpu/command_buffer/service/command_buffer_direct.hgpu/command_buffer/service/command_buffer_service.ccgpu/command_buffer/service/command_buffer_service.hgpu/command_buffer/service/decoder_client.hgpu/command_buffer/service/gles2_cmd_decoder_unittest_base.ccgpu/command_buffer/service/gles2_cmd_decoder_unittest_base.hgpu/command_buffer/service/memory_program_cache_unittest.ccgpu/command_buffer/service/mocks.hgpu/command_buffer/service/passthrough_program_cache_unittest.cc
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/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_; }
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.