CVE-2026-6314
Overview
Background
- Command buffer
- A GPU command-streaming mechanism: a ring buffer in shared memory into which a client writes commands and a service consumes them, tracked by a
get_offset(read position) and put offset (write position). - `CommandBufferHelper`
- The client-side helper that manages the ring buffer, caches the service’s
get_offset, and computes free space before writing new command entries. - `get_offset` / `total_entry_count_`
get_offsetis the service’s current read index into the ring;total_entry_count_is the ring’s capacity in entries, so a valid offset must lie in[0, total_entry_count_].- Shared-memory state
- The command-buffer
State(includingget_offset,token,error,set_get_buffer_count) is published through shared memory, so a compromised peer can supply attacker-chosen values.
Root Cause Analysis
In CommandBufferHelper::UpdateCachedState (named in the commit message; the enclosing function is not shown in the @@ header), the helper copied the service-reported state.get_offset into cached_get_offset_ with no range check (except zeroing it when the buffer generation changed, service_on_old_buffer_). Per the commit, a compromised GPU process can place an invalid get_offset into the shared-memory State, and the client — here running in a more-privileged process (the commit says the Browser process) — then uses that cached offset to compute available ring-buffer space. An out-of-range get_offset makes the free-space/space-available arithmetic wrong, which the commit states “could lead to out-of-bounds writes.” The invariant violated is that cached_get_offset_ must always lie within [0, total_entry_count_].
The fix adds, immediately after the assignment, a guard: if the buffer is current (!service_on_old_buffer_) and cached_get_offset_ < 0 || cached_get_offset_ > total_entry_count_, it forces a context loss via command_buffer_->ForceLostContext(error::kGuilty), calls FreeRingBuffer, sets usable_ = false and context_lost_ = true, resets cached_get_offset_ = 0, and returns before any further space calculation runs. Because the guard runs before the sink, a malicious offset is neutralized rather than used.
get_offset without validating it against the ring capacity; the fix bounds-checks cached_get_offset_ against [0, total_entry_count_] and, on violation, loses the context and frees the ring buffer before the offset can drive any space arithmetic or write.Attack Path
- Compromise the GPU process
An attacker already controlling the GPU process (the stated precondition) can write arbitrary values into the command-buffer
Statein shared memory. Ground: the commit’s threat model (“A compromised GPU process can provide an invalid get_offset … via shared memory”). - Publish an out-of-range `get_offset`
The attacker sets
state.get_offsetbeyondtotal_entry_count_(or negative). Ground: the regression test setsSetGetOffsetForTest(TotalEntryCount() + 1). - Trigger a state refresh
A normal client operation (e.g.
RefreshCachedToken) callsUpdateCachedState, which pre-patch copied the bad offset intocached_get_offset_with no check. Ground: the test comment “calls UpdateCachedState internally.” - Corrupt free-space computation The bad cached offset feeds the helper’s available-space arithmetic, which the commit says could produce an out-of-bounds write in the client process (inferred: the specific OOB sink and primitive are not shown in this diff).
Impact Assessment
get_offset that corrupts the helper’s space calculations, which the advisory classifies as High-severity “Out of bounds write in GPU.” The precondition is an already-compromised/malicious GPU process feeding shared-memory state; the advisory does not report in-the-wild exploitation. The out-of-bounds write is asserted by the commit as the consequence, but the concrete write location/primitive is not demonstrated in this diff, so its exploitability toward code execution is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fgpu/command_buffer/client/cmd_buffer_helper_test.cc |
modified |
Files Changed
gpu/command_buffer/client/cmd_buffer_helper.ccgpu/command_buffer/client/cmd_buffer_helper_test.cc
Audit Directions
- Trusting shared-memory stateAudit every consumer of command-buffer
Statefields (get_offset,token,put_offset, counts) for missing range validation, since any of them can be attacker-controlled across the process boundary. - Offset-to-size arithmeticLook for places that compute available/remaining space or indices from an externally supplied offset without first clamping it to the buffer capacity.
- Privilege-crossing clientsPrioritize client-side code (in
gpu/command_buffer/client) that runs in Browser/privileged processes, where a compromised GPU service is the realistic attacker and input validation is most security-critical.
Patch
From dc5e20c4c055d6952854a566d520211c6d505f74 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Wed, 08 Apr 2026 16:29:57 -0700
Subject: [PATCH] [gpu] Fix OOB write due to unvalidated get_offset
A compromised GPU process can provide an invalid get_offset to the
CommandBufferHelper (e.g., via shared memory). This offset is used to
calculate available space and could lead to out-of-bounds writes in the
Browser process if not validated.
This change adds a bounds check in
CommandBufferHelper::UpdateCachedState to ensure that the cached
get_offset is within the valid range [0, total_entry_count_]. If an
invalid offset is detected, it forces a context loss, frees the ring
buffer, and marks the helper as unusable, preventing further operations.
Bug: 498782145
Test: CommandBufferHelperTest.*
Change-Id: I8c64e546ecdc90a5a22d15e57ff762a86a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7739951
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611853}
---
diff --git a/gpu/command_buffer/client/cmd_buffer_helper.cc b/gpu/command_buffer/client/cmd_buffer_helper.cc
index ccda45b1..5aea0c81 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper.cc
@@ -158,6 +158,17 @@
service_on_old_buffer_ =
(state.set_get_buffer_count != set_get_buffer_count_);
cached_get_offset_ = service_on_old_buffer_ ? 0 : state.get_offset;
+
+ if (!service_on_old_buffer_ &&
+ (cached_get_offset_ < 0 || cached_get_offset_ > total_entry_count_)) {
+ command_buffer_->ForceLostContext(error::kGuilty);
+ FreeRingBuffer();
+ usable_ = false;
+ context_lost_ = true;
+ cached_get_offset_ = 0; // Safe fallback
+ return;
+ }
+
cached_last_token_read_ = state.token;
// Don't transition from a lost context to a working context.
context_lost_ |= error::IsError(state.error);
diff --git a/gpu/command_buffer/client/cmd_buffer_helper_test.cc b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
index 5b1e5fa..31e4671 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper_test.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
@@ -70,6 +70,8 @@
return helper_->immediate_entry_count_;
}
+ int32_t TotalEntryCount() const { return helper_->total_entry_count_; }
+
// Adds a command to the buffer through the helper, while adding it as an
// expected call on the API mock.
void AddCommandWithExpect(error::Error _return,
@@ -655,6 +657,17 @@
EXPECT_TRUE(helper_->IsContextLost());
}
+TEST_F(CommandBufferHelperTest, TestInvalidGetOffset) {
+ EXPECT_FALSE(helper_->IsContextLost());
+ EXPECT_TRUE(helper_->usable());
+
+ command_buffer_->SetGetOffsetForTest(TotalEntryCount() + 1);
+ helper_->RefreshCachedToken(); // calls UpdateCachedState internally.
+
+ EXPECT_TRUE(helper_->IsContextLost());
+ EXPECT_FALSE(helper_->usable());
+}
+
// Checks helper's 'flush generation' updates.
TEST_F(CommandBufferHelperTest, TestFlushGeneration) {
// Explicit flushing only.
Regression Test / PoC
diff --git a/gpu/command_buffer/client/cmd_buffer_helper_test.cc b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
index 5b1e5fa..31e4671 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper_test.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
@@ -70,6 +70,8 @@
return helper_->immediate_entry_count_;
}
+ int32_t TotalEntryCount() const { return helper_->total_entry_count_; }
+
// Adds a command to the buffer through the helper, while adding it as an
// expected call on the API mock.
void AddCommandWithExpect(error::Error _return,
@@ -655,6 +657,17 @@
EXPECT_TRUE(helper_->IsContextLost());
}
+TEST_F(CommandBufferHelperTest, TestInvalidGetOffset) {
+ EXPECT_FALSE(helper_->IsContextLost());
+ EXPECT_TRUE(helper_->usable());
+
+ command_buffer_->SetGetOffsetForTest(TotalEntryCount() + 1);
+ helper_->RefreshCachedToken(); // calls UpdateCachedState internally.
+
+ EXPECT_TRUE(helper_->IsContextLost());
+ EXPECT_FALSE(helper_->usable());
+}
+
// Checks helper's 'flush generation' updates.
TEST_F(CommandBufferHelperTest, TestFlushGeneration) {
// Explicit flushing only.
Original Bug Report
GPU-to-Browser Sandbox Escape via Unvalidated get_offset in CommandBufferHelper
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A compromised GPU process can trigger an out-of-bounds write in the Browser process by providing a maliciously large get_offset in the CommandBuffer shared memory. The unvalidated offset artificially inflates immediate_entry_count_, causing the ring buffer to bypass wrap-around logic and write linearly past the mapped memory. This enables a sandbox escape into the highly privileged Browser process.
Affected files:
gpu/command_buffer/client/cmd_buffer_helper.ccgpu/command_buffer/client/cmd_buffer_helper.hgpu/ipc/client/command_buffer_proxy_impl.ccgpu/command_buffer/common/command_buffer_shared.hgpu/ipc/common/command_buffer_mojom_traits.cc
Estimated timestamp from git blame: 2025-09-04
Description
A potential memory corruption vulnerability exists in the Chrome GPU command buffer system that allows a compromised GPU process to perform an out-of-bounds (OOB) write in the Browser process, leading to a full sandbox escape.
The vulnerability is rooted in how gpu::CommandBufferHelper in the Browser process updates its internal state from the CommandBufferSharedState stored in shared memory. A compromised GPU process, which has write access to this shared memory, can inject a maliciously large value for get_offset (e.g., INT32_MAX).
When the browser-side CommandBufferHelper updates its cached state via UpdateCachedState (in gpu/command_buffer/client/cmd_buffer_helper.cc), it fails to validate that the provided get_offset is within the bounds of the ring buffer (total_entry_count_):
void CommandBufferHelper::UpdateCachedState(const CommandBuffer::State& state) {
...
cached_get_offset_ = service_on_old_buffer_ ? 0 : state.get_offset;
...
}
This unvalidated cached_get_offset_ is then used in CalcImmediateEntries to calculate immediate_entry_count_, which represents the number of entries available for immediate writing without waiting for the GPU:
void CommandBufferHelper::CalcImmediateEntries(int waiting_count) {
...
const int32_t curr_get = cached_get_offset_;
if (curr_get > put_) {
immediate_entry_count_ = curr_get - put_ - 1;
}
...
}
If the attacker sets get_offset to INT32_MAX, and put_ is a normal small value (e.g., 0), immediate_entry_count_ becomes INT32_MAX - 1 (roughly 2.14 billion).
For the browser’s raster context, flush_automatically_ is explicitly set to false (see content/browser/compositor/viz_process_transport_factory.cc). This bypasses the subsequent code block in CalcImmediateEntries that would otherwise clamp immediate_entry_count_ to a safe flush threshold.
In the GetSpace method (in gpu/command_buffer/client/cmd_buffer_helper.h), this inflated value causes the browser to skip the necessary WaitForAvailableEntries call, which is responsible for wrapping the put_ index back to the start of the ring buffer when it reaches the end:
if (entries > immediate_entry_count_) {
WaitForAvailableEntries(entries);
...
}
...
CommandBufferEntry* space = UNSAFE_TODO(&entries_[put_]);
put_ += entries;
immediate_entry_count_ -= entries;
Because the wrap-around is skipped, put_ is advanced past total_entry_count_, and GetSpace returns a pointer that linearly exceeds the end of the entries_ shared memory mapping.
Impact
The client code inside the Browser process immediately populates the returned pointer with command headers and payload data (e.g., via inline memcpy or Init() calls). Because the entries_ shared memory mapping is allocated directly via OS primitives (like POSIX mmap), it sits outside the PartitionAlloc pools, meaning the out-of-bounds pointer arithmetic and subsequent writes bypass MiraclePtr (BackupRefPtr) protections.
This enables a compromised GPU process to reliably overwrite adjacent mapped memory in the highly privileged Browser process, resulting in a Sandbox Escape.
Potential Reproduction Steps
Note: These steps are theoretical as our tooling does not yet execute code to build a full chain.
- From a compromised GPU process, locate the
CommandBufferSharedStatemapping for a browser-side raster context. - Write a malicious state to the shared memory: set
get_offsettoINT32_MAX, keeperroraskNoError, and increment thegenerationcounter. - Wait for the browser process to trigger a routine state update (e.g., during
CommandBufferProxyImpl::TryUpdateState()). - The browser’s
immediate_entry_count_is now corrupted and artificially inflated. - The next time the browser issues commands via
GetSpace, it will overshoot the end of the ring buffer mapping and write OOB into adjacent browser virtual address space before the next IPC flush is sent.
Suggested Fix
Add explicit bounds checking when reading get_offset from shared memory to ensure it does not exceed total_entry_count_. This validation could be added in CommandBufferHelper::UpdateCachedState or earlier in CommandBufferProxyImpl::TryUpdateState.
void CommandBufferHelper::UpdateCachedState(const CommandBuffer::State& state) {
...
int32_t safe_get_offset = state.get_offset;
if (safe_get_offset < 0 || safe_get_offset > total_entry_count_) {
safe_get_offset = 0; // or flag context as lost
}
cached_get_offset_ = service_on_old_buffer_ ? 0 : safe_get_offset;
...
}
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.