CVE-2026-87643
Overview
Files Changed
gpu/command_buffer/service/framebuffer_manager.h
Patch
From c031348f90c6d59f704721af18eb7915e538b345 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Tue, 28 Jul 2026 05:34:11 -0700
Subject: [PATCH] gpu: Widen FramebufferManager state-change counter to 64 bits
FramebufferManager::IncFramebufferStateChangeCount() uses a counter that
is compared for equality against a snapshot taken when a framebuffer was
last marked complete. With a 32-bit counter (and the high bit pinned),
the value repeats every 2^31 increments, so after enough texture-level
updates IsComplete() can return true for a framebuffer whose attachments
have since changed, skipping IsPossiblyComplete() and leaving stale draw
buffer masks.
Widen the counter and per-framebuffer snapshot to uint64_t so the period
is unreachable in practice.
Fixed: 513416699
Change-Id: Ifd2211b5ec3d0dffeec19e01bb6b98444d94a77f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8156419
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669380}
---
diff --git a/gpu/command_buffer/service/framebuffer_manager.h b/gpu/command_buffer/service/framebuffer_manager.h
index 6b750cf2..66065a13 100644
--- a/gpu/command_buffer/service/framebuffer_manager.h
+++ b/gpu/command_buffer/service/framebuffer_manager.h
@@ -274,12 +274,12 @@
TextureManager* texture_manager,
bool cleared);
- void MarkAsComplete(unsigned state_id) {
+ void MarkAsComplete(uint64_t state_id) {
UpdateDrawBufferMasks();
framebuffer_complete_state_count_id_ = state_id;
}
- unsigned framebuffer_complete_state_count_id() const {
+ uint64_t framebuffer_complete_state_count_id() const {
return framebuffer_complete_state_count_id_;
}
@@ -303,7 +303,7 @@
bool has_been_bound_;
// state count when this framebuffer was last checked for completeness.
- unsigned framebuffer_complete_state_count_id_;
+ uint64_t framebuffer_complete_state_count_id_;
// A map of attachments.
using AttachmentMap =
@@ -400,7 +400,7 @@
void IncFramebufferStateChangeCount() {
// make sure this is never 0.
framebuffer_state_change_count_ =
- (framebuffer_state_change_count_ + 1) | 0x80000000U;
+ (framebuffer_state_change_count_ + 1) | (uint64_t{1} << 63);
}
private:
@@ -419,7 +419,7 @@
// Incremented anytime anything changes that might effect framebuffer
// state.
- unsigned framebuffer_state_change_count_;
+ uint64_t framebuffer_state_change_count_;
// Counts the number of Framebuffer allocated with 'this' as its manager.
// Allows to check no Framebuffer will outlive this.
Original Bug Report
Potential validation bypass in GLES2 FramebufferManager via 31-bit counter wrap
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A 31-bit rolling counter in the GLES2 validating command decoder can be wrapped by an attacker, leading to stale framebuffer state snapshots being treated as current. This allows a bypass of critical validation checks that ensure fragment shader output types match bound attachment formats. On platforms using the validating decoder, such as certain Android devices, this can result in undefined GPU operations and potential memory corruption in the unsandboxed GPU process.
Affected files:
gpu/command_buffer/service/framebuffer_manager.hgpu/command_buffer/service/framebuffer_manager.ccgpu/command_buffer/service/texture_manager.ccgpu/command_buffer/service/gles2_cmd_decoder.cc
Estimated timestamp from git blame: 2011-12-20
Summary
A potential logic flaw in the GLES2 validating command decoder’s FramebufferManager allows an attacker to bypass framebuffer validation through a 31-bit integer wrap. By wrapping a rolling state counter, an attacker can make a stale framebuffer state snapshot appear current. This bypasses the re-calculation of internal state masks (such as draw_buffer_type_mask_), allowing fragment shaders to perform undefined operations against bound attachments, which can lead to memory corruption in native GPU drivers.
Root Cause Analysis
The FramebufferManager maintains a global rolling counter, framebuffer_state_change_count_, which is incremented whenever a resource (texture or renderbuffer) is modified in a way that might affect framebuffer completeness. Framebuffers store a snapshot of this counter (framebuffer_complete_state_count_id_) when they are validated. If the snapshot matches the global counter, the decoder short-circuits validation and uses cached state masks.
The counter increment logic in gpu/command_buffer/service/framebuffer_manager.h ensures the 31st bit is always set:
void IncFramebufferStateChangeCount() {
framebuffer_state_change_count_ =
(framebuffer_state_change_count_ + 1) | 0x80000000U;
}
This results in a cycle of exactly 2^31 distinct values.
A vulnerability arises from an asymmetry in how textures and renderbuffers handle invalidation. When a renderbuffer is modified, it iterates through its attachment points and explicitly calls UnmarkAsComplete() on attached framebuffers. However, when a texture’s level information is re-specified (e.g., via glTexImage2D), Texture::SetLevelInfo in gpu/command_buffer/service/texture_manager.cc only increments the global counter and fails to invalidate the attached framebuffers directly. This allows a framebuffer to retain a stale snapshot that can eventually be collided with if the global counter wraps.
Potential Attack Sequence
An attacker could potentially follow these steps to trigger the bypass:
- Setup: Create a Framebuffer (FBO_A) and attach a FLOAT-renderable texture (Tex_1).
- Initialize Snapshot: Trigger validation of FBO_A (e.g., via a dummy draw call). The decoder caches the “FLOAT” type in
draw_buffer_type_mask_and stores the current global counter (G) as a snapshot. - Modify Texture: Re-specify Tex_1 to an INTEGER format using
glTexImage2D. This increments the global counter but does not invalidate FBO_A’s snapshot. - Wrap Counter: Repeatedly modify a second, dummy texture to increment the global counter 2^31 - 1 times until it wraps back to the value G.
- Bypass Validation: Issue a draw call with FBO_A bound using a shader that outputs FLOAT values. The decoder sees the snapshot matches the current counter G, skips validation, and relies on the stale “FLOAT” mask.
- Undefined Operation: The
ValidateAndAdjustDrawBufferscheck incorrectly passes, and the FLOAT shader output is forwarded to the INTEGER attachment. This undefined operation can trigger memory corruption in the native GPU driver.
Impact
On Android, the GPU process is effectively unsandboxed. A successful exploit leading to memory corruption in a native GPU driver could result in a full sandbox escape and device compromise. While the validating decoder is primarily used on specific hardware where the passthrough decoder is blocklisted, it remains a critical security boundary for those devices.
Suggested Fix
- Update the
Textureclass to track its framebuffer attachment points, similar to theRenderbufferclass, and ensureUnmarkAsComplete()is called on all attached framebuffers whenever texture state changes. - Alternatively, increase the size of the
framebuffer_state_change_count_to 64 bits to make wrapping computationally infeasible.
Note: These steps and the resulting vulnerability are potential findings based on code analysis; a functional proof of concept has not yet been executed.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.