Medium watch CVSS 3.4 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
ComponentGPU
Sub-componentGPU
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker513162143
Fix commit5dbcb6227c9d (chromium/src) +89/-1
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-08-19 (public git)
Disclosed2026-09-22
Patch gap34 days — fix public in source before the advisory

Background

`FramebufferCompletenessCache`
a cache in the GPU command-buffer service that memoizes the result of a GL framebuffer-completeness check, keyed by a byte signature of the framebuffer’s attachments.
`TextureAttachment`
an attachment object representing a texture bound to a framebuffer, carrying fields such as target_, level_, samples_, and layer_.
`AddToSignature` / `GetSignatureSize`
methods that serialize an attachment’s identifying state into the cache-key signature (and report that key’s byte length), determining what distinguishes one cached completeness verdict from another.
`samples_` / `layer_`
the multisample count and the array-texture layer index of an attachment; two framebuffers differing only in these fields are functionally distinct configurations.

Root Cause Analysis

TextureAttachment::AddToSignature serialized only target_ and level_ (via texture_manager->AddToSignature) but omitted the attachment’s samples_ and layer_ fields, and GetSignatureSize likewise did not account for their bytes.

As a result, two TextureAttachment objects that differed only in sample count or array layer produced identical cache signatures in FramebufferCompletenessCache. When the second configuration was looked up, the cache returned the stale completeness verdict computed for the first, bypassing a real GL completeness check. A configuration that was actually incomplete or mismatched could therefore be treated as complete. This let the GPU proceed to sample from or render into an attachment whose true state was never validated, exposing uninitialized or mismatched resource memory.

Key insight
A memoization cache is only as sound as its key: because samples_ and layer_ were excluded from the signature, distinct framebuffer configurations aliased to the same key and silently inherited each other’s cached “complete” verdict.

Attack Path

  1. Prime the cache Issue GL commands that build a framebuffer attachment configuration which passes the completeness check, caching a “complete” verdict under its signature in FramebufferCompletenessCache.
  2. Construct a colliding attachment Create a second TextureAttachment identical except for samples_ or layer_, so it hashes to the same signature despite being a genuinely different (and invalid) configuration.
  3. Trigger cache reuse Perform a framebuffer operation that consults the cache; the stale “complete” verdict is returned and the real completeness validation is skipped.
  4. Sample/render uninitialized memory Drive a draw or read against the framebuffer treated as complete, causing the GPU to access uninitialized or mismatched resource memory.

Impact Assessment

This uninitialized-memory flaw in the GPU process lets an attacker cause the GPU to sample or render resource memory that was never properly validated or initialized, potentially disclosing stale GPU memory contents. Because the completeness check is a core safety gate before rendering, its bypass undermines assumptions the pipeline relies on. Exploitation requires only issuing crafted GL command sequences reachable from a compromised or malicious renderer.

Changed Functions

FunctionChangeNotes
TEST_F
gpu/command_buffer/service/framebuffer_manager_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/framebuffer_manager.cc
  • gpu/command_buffer/service/framebuffer_manager_unittest.cc

Audit Directions

  • Incomplete cache-key serialization
    Audit every AddToSignature/GetSignatureSize pair (and analogous hash/key builders) to confirm all fields that affect the cached result are serialized and their sizes counted; a field present in the object but absent from the key is a collision bug.
  • State-differentiating fields omitted from equality
    Look for members like samples_ and layer_ that distinguish configurations but are dropped from signature, operator==, or hash implementations, allowing distinct objects to alias.
  • Completeness/validation memoization
    Scrutinize caches that memoize a validation verdict (FramebufferCompletenessCache and similar), ensuring a stale “valid/complete” result cannot be reused for a materially different configuration.
From 5dbcb6227c9d4f978c64d8d91d45ae10f62af275 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Wed, 19 Aug 2026 10:32:41 -0700
Subject: [PATCH] [agy][gpu] Fix FBO completeness cache collision

Include samples and layer in TextureAttachment signature.

TextureAttachment::AddToSignature omitted samples_ and layer_,
causing two attachments that differed only in these fields to
collide in FramebufferCompletenessCache.

Fixed: 513162143
Test: gpu_unittests
Change-Id: I8e66b81ebf8990c905e7075e9d9d1f8242d463d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8240171
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Tzarial <zork@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1682449}
---
diff --git a/gpu/command_buffer/service/framebuffer_manager.cc b/gpu/command_buffer/service/framebuffer_manager.cc
index 14886cd..b11de04 100644
--- a/gpu/command_buffer/service/framebuffer_manager.cc
+++ b/gpu/command_buffer/service/framebuffer_manager.cc
@@ -294,7 +294,8 @@
   }
 
   size_t GetSignatureSize(TextureManager* texture_manager) const override {
-    return texture_manager->GetSignatureSize();
+    return texture_manager->GetSignatureSize() + sizeof(samples_) +
+           sizeof(layer_);
   }
 
   void AddToSignature(TextureManager* texture_manager,
@@ -302,6 +303,9 @@
     DCHECK(signature);
     texture_manager->AddToSignature(
         texture_ref_.get(), target_, level_, signature);
+    signature->append(reinterpret_cast<const char*>(&samples_),
+                      sizeof(samples_));
+    signature->append(reinterpret_cast<const char*>(&layer_), sizeof(layer_));
   }
 
   bool FormsFeedbackLoop(TextureRef* texture,
diff --git a/gpu/command_buffer/service/framebuffer_manager_unittest.cc b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
index a9ec868..b70b561 100644
--- a/gpu/command_buffer/service/framebuffer_manager_unittest.cc
+++ b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
@@ -1668,6 +1668,90 @@
   framebuffer_->GetStatus(texture_manager_.get(), GL_READ_FRAMEBUFFER);
 }
 
+TEST_F(FramebufferInfoTest, GetStatusTextureSamples) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  const GLenum kTarget = GL_TEXTURE_2D;
+  const GLint kLevel = 0;
+  const GLint kSamples0 = 0;
+  const GLint kSamples1 = 4;
+
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), kTarget);
+
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples0);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call for the same attachment does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check that re-attaching with a different sample count calls
+  // CheckFramebufferStatus.
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples1);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check putting it back does not call CheckFramebufferStatus.
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples0);
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+}
+
+TEST_F(FramebufferInfoTest, GetStatusTextureLayer) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+  const GLint kLevel = 0;
+  const GLint kLayer0 = 0;
+  const GLint kLayer1 = 1;
+
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), kTarget);
+
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer0);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call for the same attachment does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check that re-attaching with a different layer calls
+  // CheckFramebufferStatus.
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer1);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check putting it back does not call CheckFramebufferStatus.
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer0);
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+}
+
 TEST_F(FramebufferInfoES3Test, DifferentDimensions) {
   const GLuint kRenderbufferClient1Id = 33;
   const GLuint kRenderbufferService1Id = 333;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/framebuffer_manager_unittest.cc b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
index a9ec868..b70b561 100644
--- a/gpu/command_buffer/service/framebuffer_manager_unittest.cc
+++ b/gpu/command_buffer/service/framebuffer_manager_unittest.cc
@@ -1668,6 +1668,90 @@
   framebuffer_->GetStatus(texture_manager_.get(), GL_READ_FRAMEBUFFER);
 }
 
+TEST_F(FramebufferInfoTest, GetStatusTextureSamples) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  const GLenum kTarget = GL_TEXTURE_2D;
+  const GLint kLevel = 0;
+  const GLint kSamples0 = 0;
+  const GLint kSamples1 = 4;
+
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), kTarget);
+
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples0);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call for the same attachment does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check that re-attaching with a different sample count calls
+  // CheckFramebufferStatus.
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples1);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check putting it back does not call CheckFramebufferStatus.
+  framebuffer_->AttachTexture(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                              kLevel, kSamples0);
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+}
+
+TEST_F(FramebufferInfoTest, GetStatusTextureLayer) {
+  const GLuint kTextureClientId = 33;
+  const GLuint kTextureServiceId = 333;
+  const GLenum kTarget = GL_TEXTURE_2D_ARRAY;
+  const GLint kLevel = 0;
+  const GLint kLayer0 = 0;
+  const GLint kLayer1 = 1;
+
+  texture_manager_->CreateTexture(kTextureClientId, kTextureServiceId);
+  scoped_refptr<TextureRef> texture(
+      texture_manager_->GetTexture(kTextureClientId));
+  ASSERT_TRUE(texture.get() != nullptr);
+  texture_manager_->SetTarget(texture.get(), kTarget);
+
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer0);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call for the same attachment does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check that re-attaching with a different layer calls
+  // CheckFramebufferStatus.
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer1);
+  EXPECT_CALL(*gl_, CheckFramebufferStatusEXT(GL_FRAMEBUFFER))
+      .WillOnce(Return(GL_FRAMEBUFFER_COMPLETE))
+      .RetiresOnSaturation();
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check a second call does not call anything.
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+
+  // Check putting it back does not call CheckFramebufferStatus.
+  framebuffer_->AttachTextureLayer(GL_COLOR_ATTACHMENT0, texture.get(), kTarget,
+                                   kLevel, kLayer0);
+  framebuffer_->GetStatus(texture_manager_.get(), GL_FRAMEBUFFER);
+}
+
 TEST_F(FramebufferInfoES3Test, DifferentDimensions) {
   const GLuint kRenderbufferClient1Id = 33;
   const GLuint kRenderbufferService1Id = 333;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential cross-origin info leak via Framebuffer signature collision in validating decoder

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in the validating command decoder’s framebuffer completeness cache allows a compromised renderer to bypass mandatory memory zeroing. By triggering a signature collision, an incomplete framebuffer can be incorrectly treated as complete, leading to the disclosure of uninitialized GPU memory. The vulnerability stems from the omission of ‘samples’ and ’layer’ attributes in the generated framebuffer signature.

Affected files:

  • 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: 2013-07-12

Summary

A potential vulnerability in the Chromium validating command decoder allows a compromised renderer to bypass the resource-zeroing invariant for textures. The TextureAttachment::AddToSignature method, which generates a signature for the FramebufferCompletenessCache, fails to include the samples_ and layer_ fields. This leads to signature collisions, where the decoder incorrectly treats an incomplete framebuffer as complete by relying on a cached status. Consequently, an uncleared texture can be marked as cleared without its storage actually being zeroed, allowing the disclosure of uninitialized GPU memory across origins.

Root Cause Analysis

In gpu/command_buffer/service/framebuffer_manager.cc, the TextureAttachment class maintains state for samples_ (set via glFramebufferTexture2DMultisampleEXT) and layer_ (set via glFramebufferTextureLayer). However, its AddToSignature implementation fails to include these fields in the generated signature:

// gpu/command_buffer/service/framebuffer_manager.cc
void TextureAttachment::AddToSignature(TextureManager* texture_manager,
                                        std::string* signature) const {
  DCHECK(signature);
  texture_manager->AddToSignature(
      texture_ref_.get(), target_, level_, signature); // samples_ and layer_ omitted
}

The resulting signature only contains the state of the underlying texture level (dimensions, format, etc.) provided by TextureManager. When the decoder checks for framebuffer completeness via Framebuffer::GetStatus, it consults the FramebufferComboCompleteCache. If the signature matches a previously cached entry for a complete framebuffer, the decoder returns GL_FRAMEBUFFER_COMPLETE immediately without querying the actual driver status via glCheckFramebufferStatusEXT.

Potential Trigger Path (Suggested)

An attacker with control over a renderer process could potentially exploit this collision to leak cross-origin GPU memory using the following steps:

  1. Cache a Complete Signature: The attacker creates a valid framebuffer (FBO_A) with a texture (T1) using samples=0 and layer=0. A draw operation is performed, the driver confirms completeness, and the signature S is cached as complete.
  2. Trigger a Collision: The attacker creates a new framebuffer (FBO_B) with an uncleared texture (T2) and uses samples=N or a specific layer that causes the driver to consider the framebuffer incomplete (e.g., an unsupported sample count or an out-of-bounds layer index).
  3. Bypass Completeness Check: When the attacker attempts to draw to or clear FBO_B, the decoder generates a signature S’. Because samples and layer are omitted, S’ == S. The decoder hits the cache and incorrectly assumes FBO_B is complete.
  4. Metadata Corruption: The decoder attempts to clear T2’s storage using glClear. Because the framebuffer is actually incomplete, the underlying GL driver ignores the clear operation (silently failing or issuing GL_INVALID_FRAMEBUFFER_OPERATION). However, the decoder unconditionally proceeds to call MarkAttachmentsAsCleared, marking T2 as cleared in its internal metadata.
  5. Information Leak: The attacker attaches T2 to a genuine complete framebuffer and performs a read (e.g., via glReadPixels). Since the texture was never zeroed but is marked as cleared, the attacker receives uninitialized GPU memory content.

Impact

Successful exploitation allows a compromised renderer to read uninitialized GPU memory. This memory may contain sensitive data from other processes or cross-origin web content that previously occupied the same physical memory. This issue is restricted to the validating command decoder, which is active on specific configurations (e.g., Android devices on the GPU blocklist).

Suggested Fix

Modify TextureAttachment::AddToSignature in gpu/command_buffer/service/framebuffer_manager.cc to explicitly append the samples_ and layer_ values to the signature string. This ensures that different framebuffer configurations generate unique signatures.

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