Medium watch CVSS 9.6 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in GPU
DescriptionInteger overflow in GPU
ComponentGPU
Sub-componentGPU
Bug ClassInteger Overflow
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513416699
Fix commitc031348f90c6 (chromium/src) +5/-5
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-09-08
Patch gap42 days — fix public in source before the advisory

Background

`Framebuffer` completeness
a framebuffer object is “complete” when its attachments satisfy the GL rules, and the GPU service caches this result so it does not re-validate on every draw.
`framebuffer_state_change_count_`
a monotonically-incremented counter on FramebufferManager that is bumped by IncFramebufferStateChangeCount() whenever anything that could affect framebuffer completeness (e.g. a texture-level update) changes.
`framebuffer_complete_state_count_id_`
a per-Framebuffer snapshot of that counter, taken in MarkAsComplete() at the moment the framebuffer last passed a full completeness check.
Draw buffer masks
cached bitmasks recording which color attachments a framebuffer writes to, refreshed by UpdateDrawBufferMasks() and consumed by the command decoder when servicing draws.

Root Cause Analysis

IsComplete() decides whether it can skip the expensive IsPossiblyComplete() re-validation by comparing the framebuffer’s cached framebuffer_complete_state_count_id_ for equality against the manager’s current framebuffer_state_change_count_; if they match, the framebuffer is assumed still complete and its cached state (including draw buffer masks) is trusted. The counter was a 32-bit unsigned and IncFramebufferStateChangeCount() pinned the high bit with | 0x80000000U, leaving only 31 varying bits, so the sequence repeats every 2^31 increments. An attacker who drives enough state changes (each texture-level update bumps the counter) can wrap the counter back to the exact snapshot value recorded when a now-stale framebuffer was last marked complete, so the equality test spuriously succeeds. The invariant that “equal counters imply the framebuffer’s attachments are unchanged since the last full check” is violated by the aliasing collision, so IsComplete() returns true, IsPossiblyComplete() and the UpdateDrawBufferMasks() refresh are skipped, and draws proceed against stale draw buffer masks describing attachments that have since changed.

The fix widens both the counter and the per-framebuffer snapshot to uint64_t (pinning bit 63 instead of bit 31), making the 2^63-increment period unreachable in practice.

Key insight
The single mistake is using a narrow 32-bit counter with one bit pinned for a value that must be unique-per-state-change, so equality after a 2^31 wraparound falsely reports a changed framebuffer as still complete; widening the counter and its snapshot to uint64_t pushes the collision period beyond any reachable increment count.

Attack Path

  1. Establish a completed framebuffer Bind and validate a framebuffer via the GPU command channel so MarkAsComplete() records the current counter into framebuffer_complete_state_count_id_ and computes its draw buffer masks.
  2. Mutate an attachment Change one of the framebuffer’s attachments (for example a texture level or format) so its true completeness / draw buffer masks would differ, then issue draws or state changes that continue to call IncFramebufferStateChangeCount().
  3. Wrap the counter Drive roughly 2^31 state-changing operations so framebuffer_state_change_count_ cycles back to the exact snapshot value stored in the stale framebuffer, exploiting the 32-bit range and the pinned high bit.
  4. Trigger the stale fast path On the next draw, IsComplete() finds equal counters, returns true, and skips IsPossiblyComplete() and the draw-buffer-mask refresh, so the decoder acts on masks that no longer match the real attachments.
  5. Downstream corruption (inferred) Draws executed against mismatched draw buffer masks / stale attachment state can lead to out-of-bounds or type-confused GPU accesses; the specific sink and any escalation are not shown by the patch.

Impact Assessment

An attacker running attacker-controlled GL/WebGL commands (e.g. from a compromised or malicious renderer, or via WebGL from a web page) can make the GPU process trust stale framebuffer completeness and draw buffer masks in the gpu::gles2 command decoder, undermining a validation invariant inside the GPU process. Preconditions are the ability to issue on the order of 2^31 state-changing GPU commands to force the 32-bit counter to alias a prior snapshot value. Whether this yields memory corruption, information disclosure, or a sandbox escape is not demonstrated by the diff and is (inferred); the advisory classifies the issue as an integer overflow in GPU.

Files Changed

  • gpu/command_buffer/service/framebuffer_manager.h

Audit Directions

  • Fixed-width state/generation counters compared for equality
    audit any monotonic counter or “generation id” that is snapshotted and later compared with == to decide whether cached validation can be skipped; a 32-bit width (worse still with pinned bits) makes wraparound-aliasing a live correctness/security bug.
  • Bit-pinning that shrinks the effective range
    flag counters ORed with a constant high bit (| 0x80000000U and similar), since pinning bits reduces the number of varying bits and shortens the aliasing period below the notional type width.
  • Cache-skip fast paths in the GPU command decoder
    review other Is*Complete()-style shortcuts (framebuffers, textures, VAOs, programs) where an equality check against a cached count gates skipping full re-validation, and confirm the keying value is wide enough that it cannot repeat within an attacker-reachable operation count.
From c031348f90c6d59f704721af18eb7915e538b345 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
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 <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
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.
Loading diff…

Original Bug Report

reported by [email protected]

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.h
  • gpu/command_buffer/service/framebuffer_manager.cc
  • gpu/command_buffer/service/texture_manager.cc
  • gpu/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:

  1. Setup: Create a Framebuffer (FBO_A) and attach a FLOAT-renderable texture (Tex_1).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Undefined Operation: The ValidateAndAdjustDrawBuffers check 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

  1. Update the Texture class to track its framebuffer attachment points, similar to the Renderbuffer class, and ensure UnmarkAsComplete() is called on all attached framebuffers whenever texture state changes.
  2. 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.

View on issue tracker