Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in GPU
DescriptionOut of bounds write in GPU
ComponentGPU
Bug ClassOOB
Tracker498782145
Fix commitdc5e20c4c055 (chromium/src) +24/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
TEST_F
gpu/command_buffer/client/cmd_buffer_helper_test.cc
modified

Files Changed

  • gpu/command_buffer/client/cmd_buffer_helper.cc
  • gpu/command_buffer/client/cmd_buffer_helper_test.cc
From dc5e20c4c055d6952854a566d520211c6d505f74 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <[email protected]>
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 <[email protected]>
Auto-Submit: Sunny Sachanandani <[email protected]>
Commit-Queue: Sunny Sachanandani <[email protected]>
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.
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • gpu/command_buffer/client/cmd_buffer_helper.h
  • gpu/ipc/client/command_buffer_proxy_impl.cc
  • gpu/command_buffer/common/command_buffer_shared.h
  • gpu/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.

  1. From a compromised GPU process, locate the CommandBufferSharedState mapping for a browser-side raster context.
  2. Write a malicious state to the shared memory: set get_offset to INT32_MAX, keep error as kNoError, and increment the generation counter.
  3. Wait for the browser process to trigger a routine state update (e.g., during CommandBufferProxyImpl::TryUpdateState()).
  4. The browser’s immediate_entry_count_ is now corrupted and artificially inflated.
  5. 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.

View on issue tracker