CVE-2026-7912
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchgpu/command_buffer/service/transform_feedback_manager.cc |
modified |
Files Changed
gpu/command_buffer/service/buffer_manager.ccgpu/command_buffer/service/transform_feedback_manager.cc
Patch
From 39eb06c88d286abf6714065181f84c2be0a7be18 Mon Sep 17 00:00:00 2001
From: Francisco Ochoa <frankchavez@google.com>
Date: Thu, 02 Apr 2026 04:43:30 -0700
Subject: [PATCH] Fix potential integer overflow and add size check.
Perform vertex addition in GetVerticesNeededForDraw within
base::CheckedNumeric to catch overflows that wrap to negative values and
bypass buffer bounds checks.
Add size(count variable) check in RequestBuffersAccess.
See (internal-only): go/code-terracotta-review-explainer
This is a phase 1 quick fix.
Bug: 497639714
Change-Id: Iecb9fe274109013e7d7383e409609d13ea2c7003
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718624
Reviewed-by: Stephen Nusko <nuskos@chromium.org>
Commit-Queue: Francisco Ochoa <frankchavez@google.com>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Reviewed-by: Colin Blundell <blundell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609104}
---
diff --git a/gpu/command_buffer/service/buffer_manager.cc b/gpu/command_buffer/service/buffer_manager.cc
index 49fd7c7f..109ef77 100644
--- a/gpu/command_buffer/service/buffer_manager.cc
+++ b/gpu/command_buffer/service/buffer_manager.cc
@@ -889,8 +889,9 @@
GLsizei count,
const char* func_name,
const char* message_tag) {
- DCHECK(error_state);
- DCHECK(bindings);
+ CHECK(error_state);
+ CHECK(bindings);
+ CHECK_GE(count, 0);
for (size_t ii = 0; ii < variable_sizes.size(); ++ii) {
if (variable_sizes[ii] == 0)
diff --git a/gpu/command_buffer/service/transform_feedback_manager.cc b/gpu/command_buffer/service/transform_feedback_manager.cc
index 3c24101c..0c9dd74 100644
--- a/gpu/command_buffer/service/transform_feedback_manager.cc
+++ b/gpu/command_buffer/service/transform_feedback_manager.cc
@@ -109,8 +109,8 @@
// Transform feedback only outputs complete primitives, so we need to round
// down to the nearest complete primitive before multiplying by the number of
// instances.
- base::CheckedNumeric<GLsizei> checked_vertices =
- vertices_drawn_ + pending_vertices_drawn;
+ base::CheckedNumeric<GLsizei> checked_vertices = vertices_drawn_;
+ checked_vertices += pending_vertices_drawn;
base::CheckedNumeric<GLsizei> checked_count = count;
base::CheckedNumeric<GLsizei> checked_primcount = primcount;
switch (mode) {
Original Bug Report
Potential out-of-bounds write in GPU memory via Transform Feedback integer overflow
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A combination of an accumulation logic bug and an unchecked signed integer overflow in the validating command decoder allows an attacker to bypass Transform Feedback buffer bounds checks. By using multi-draw commands, a compromised renderer can force the required buffer size calculation to wrap to a negative value. This bypasses the buffer size validation, leading to a potential out-of-bounds write in the GPU process.
Affected files:
gpu/command_buffer/service/transform_feedback_manager.ccgpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/buffer_manager.cc
Estimated timestamp from git blame: 2019-01-18
Vulnerability Details
A potential security vulnerability exists in the Chromium validating command decoder’s handling of Transform Feedback (TF) during multi-draw operations. The issue stems from two interacting flaws: a logic bug that repeatedly accumulates previously drawn vertices, and an improper initialization of base::CheckedNumeric that fails to detect a resulting integer overflow.
1. The Logic Flaw in CheckTransformFeedback
In gpu/command_buffer/service/gles2_cmd_decoder.cc, the function GLES2DecoderImpl::CheckTransformFeedback validates that the bound TF buffer is large enough for a multi-draw command. It loops over the drawcount and repeatedly calls GetVerticesNeededForDraw:
for (GLsizei draw_id = 0; draw_id < drawcount; ++draw_id) {
// ...
bool valid = state_.bound_transform_feedback->GetVerticesNeededForDraw(
mode, count, primcount, *vertices_drawn, vertices_drawn);
// ...
}
Here, *vertices_drawn acts as an accumulator. It is passed as the pending_vertices_drawn input argument and receives the updated total via the vertices_out pointer.
2. The Integer Overflow in GetVerticesNeededForDraw
Inside gpu/command_buffer/service/transform_feedback_manager.cc, TransformFeedback::GetVerticesNeededForDraw calculates the total vertices using the following expression:
base::CheckedNumeric<GLsizei> checked_vertices =
vertices_drawn_ + pending_vertices_drawn;
Because vertices_drawn_ (the class member tracking vertices drawn in previous commands) is added to pending_vertices_drawn (the loop’s accumulator) on every iteration, vertices_drawn_ is incorrectly accumulated drawcount times.
Crucially, both vertices_drawn_ and pending_vertices_drawn are of type GLsizei (a 32-bit signed integer). The addition is performed using standard C++ signed arithmetic before the result is passed to the base::CheckedNumeric constructor. Chromium compiles with -fno-strict-overflow, defining signed overflow to wrap. If an attacker provides a massive drawcount, the sum $N \times \text{vertices_drawn_}$ will exceed INT_MAX and wrap to a large negative number. Because this wrapped negative number is a valid 32-bit integer, the base::CheckedNumeric constructor accepts it without flagging an invalid state.
3. Buffer Bounds Check Bypass
The wrapped negative vertex count is returned to CheckTransformFeedback and subsequently passed to BufferManager::RequestBuffersAccess (gpu/command_buffer/service/buffer_manager.cc):
if (!base::CheckMul(variable_sizes[ii], count)
.AssignIfValid(&required_size) ||
size < required_size) {
// error...
}
Since count is negative, base::CheckMul correctly produces a negative required_size (as GLsizeiptr is a signed 64-bit type). The comparison size < required_size (where size is the positive actual buffer size and required_size is negative) evaluates to false. This completely bypasses the transform feedback buffer size check.
Suggested Exploit Path
Note: Our tooling agent cannot run code yet, so these are theoretical steps based on code analysis.
- Initial Setup: From a compromised renderer, create an OpenGL ES 3 / WebGL 2 context using the validating command decoder.
- Bind Transform Feedback: Bind a small buffer for Transform Feedback and call
glBeginTransformFeedback(GL_POINTS). - Populate
vertices_drawn_: Issue a standardglDrawArrayscall to draw a moderate number of vertices (e.g., $V = 10,000$). This sets theTransformFeedbackobject’s internalvertices_drawn_state to $V$. - Trigger the Overflow: Issue a
WEBGL_multi_drawcommand (e.g.,glMultiDrawArraysWEBGL) with a massivedrawcount(e.g., $N = 300,000$). - The Bypass: The validation loop in
CheckTransformFeedbackrepeatedly adds $V$ to the accumulator $N$ times. The sum $10,000 \times 300,000$ overflows the 32-bit signed integer, wrapping to a negative value. TheCheckedNumericinitialization fails to detect this. The negative count is passed toRequestBuffersAccess, where the negativerequired_sizecauses the bounds check (size < required_size) to evaluate to false. - Out-of-Bounds Write: The decoder dispatches the massive multi-draw command to the native OpenGL driver. The hardware Transform Feedback unit sequentially writes the varying outputs into the bound buffer, quickly writing past the end of the allocation and corrupting adjacent GPU memory.
- Persistent State Corruption: After dispatch,
OnVerticesDrawnis called with the negative accumulator, permanently corruptingvertices_drawn_. Subsequent standard draw calls will now also bypass buffer bounds checks.
Suggested Fix
- Fix the accumulation logic: In
GLES2DecoderImpl::CheckTransformFeedback, do not repeatedly addvertices_drawn_inside the loop. TheTransformFeedback::GetVerticesNeededForDrawmethod should only calculate the vertices needed for the current draw parameters, and the caller should accumulate them.vertices_drawn_should only be added once, outside the loop. - Fix the CheckedNumeric initialization: In
TransformFeedback::GetVerticesNeededForDraw, perform the addition usingbase::CheckedNumericmethods to catch overflows:base::CheckedNumeric<GLsizei> checked_vertices = vertices_drawn_; checked_vertices += pending_vertices_drawn; - Validate
count: InBufferManager::RequestBuffersAccess, ensure thatcount >= 0before proceeding with calculations.
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
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.