CVE-2026-95359
Overview
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_, andlayer_. - `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.
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
- 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. - Construct a colliding attachment
Create a second
TextureAttachmentidentical except forsamples_orlayer_, so it hashes to the same signature despite being a genuinely different (and invalid) configuration. - Trigger cache reuse Perform a framebuffer operation that consults the cache; the stale “complete” verdict is returned and the real completeness validation is skipped.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fgpu/command_buffer/service/framebuffer_manager_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/framebuffer_manager.ccgpu/command_buffer/service/framebuffer_manager_unittest.cc
Audit Directions
- Incomplete cache-key serializationAudit every
AddToSignature/GetSignatureSizepair (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 equalityLook for members like
samples_andlayer_that distinguish configurations but are dropped from signature,operator==, or hash implementations, allowing distinct objects to alias. - Completeness/validation memoizationScrutinize caches that memoize a validation verdict (
FramebufferCompletenessCacheand similar), ensuring a stale “valid/complete” result cannot be reused for a materially different configuration.
Patch
From 5dbcb6227c9d4f978c64d8d91d45ae10f62af275 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> 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 <[email protected]> Commit-Queue: Tzarial <[email protected]> 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;
Regression Test / PoC
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;
Original Bug Report
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.ccgpu/command_buffer/service/texture_manager.ccgpu/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:
- Cache a Complete Signature: The attacker creates a valid framebuffer (FBO_A) with a texture (T1) using
samples=0andlayer=0. A draw operation is performed, the driver confirms completeness, and the signature S is cached as complete. - Trigger a Collision: The attacker creates a new framebuffer (FBO_B) with an uncleared texture (T2) and uses
samples=Nor a specificlayerthat causes the driver to consider the framebuffer incomplete (e.g., an unsupported sample count or an out-of-bounds layer index). - Bypass Completeness Check: When the attacker attempts to draw to or clear FBO_B, the decoder generates a signature S’. Because
samplesandlayerare omitted, S’ == S. The decoder hits the cache and incorrectly assumes FBO_B is complete. - 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 issuingGL_INVALID_FRAMEBUFFER_OPERATION). However, the decoder unconditionally proceeds to callMarkAttachmentsAsCleared, marking T2 as cleared in its internal metadata. - 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.