CVE-2026-87527
Overview
Background
- WebGL
- a browser API that exposes GPU-accelerated 3D rendering to untrusted JavaScript by routing draw and texture commands through Chrome’s GPU process.
- GPU command buffer
- the IPC layer in
gpu/command_buffer/servicethat validates client-issued GL commands before replaying them against the real driver. - `TEXTURE_BASE_LEVEL`
- a per-texture parameter (
base_level_) that sets the lowest mipmap level the GL considers part of the texture for sampling and framebuffer completeness. - Framebuffer completeness
- the OpenGL ES rule set that a framebuffer object must satisfy before it may be rendered to, per ES 3.0 section 4.4.4.1.
Root Cause Analysis
The vulnerable path is Texture::CanRenderTo in gpu/command_buffer/service/texture_manager.cc, which decides whether a given texture level may serve as a color/depth attachment for framebuffer rendering. The method validated cube completeness, that level fell inside the valid mip range, and (only for level > base_level_) texture completeness, but it never rejected the case where level < base_level_. ES 3.0 section 4.4.4.1 requires that a framebuffer attachment whose texture level is below TEXTURE_BASE_LEVEL be treated as incomplete, so the missing check violated the invariant that only levels within the base-through-max range are renderable. Because SetLevelInfo can define storage for level 0 while TEXTURE_BASE_LEVEL is subsequently raised to 1, the attachment at level 0 could be attached and rendered to even though it is not a valid, dimensionally-consistent target.
The fix adds an explicit if (level < base_level_) return false; guard so any below-base level is rejected as an incomplete attachment before rendering proceeds.
CanRenderTo only guarded the level > base_level_ branch and silently accepted level < base_level_, leaving a gap in framebuffer-completeness validation; the fix closes it by unconditionally rejecting levels below base_level_.Attack Path
- Create a texture and define a low level
From WebGL, allocate a texture and call the equivalent of
SetLevelInfoon level 0 to give it storage and dimensions. - Raise the base level
Set
TEXTURE_BASE_LEVELto 1 (or higher) so level 0 now lies belowbase_level_and is outside the logically valid mip range. - Attach the below-base level
Bind level 0 of the texture as a framebuffer color attachment, which the unpatched
CanRenderTowrongly reports as renderable. - Render to the stale target Issue draw or clear commands so the GPU writes into a level whose completeness invariants were never enforced, producing an out-of-bounds buffer write.
Impact Assessment
TEXTURE_BASE_LEVEL relative to a defined level, which is available to any web page. The metadata classes this as a critical buffer overflow, consistent with attacker-controlled rendering into an under-validated texture level.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/texture_manager.cc |
modified | |
TEST_Fgpu/command_buffer/service/texture_manager_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/texture_manager.ccgpu/command_buffer/service/texture_manager_unittest.cc
Audit Directions
- Range-partial validationFlag completeness or bounds checks that only guard one side of a range (here only
level > base_level_), since the untestedlevel < base_level_case is exactly where the invariant leaked. - Mutable parameters versus cached stateReview paths where a parameter like
base_level_can change after level storage is defined viaSetLevelInfo, ensuring every consumer re-validates against the current base/max rather than assuming level 0 is always valid. - Spec-mandated attachment rulesCross-check
CanRenderToand related framebuffer-attachment validators against ES 3.0 section 4.4.4.1 completeness conditions to catch other omitted incompleteness cases.
Patch
From 3b907e173dc4a643728a928f3d239fd99e1f2950 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Mon, 24 Aug 2026 14:23:54 -0700
Subject: [PATCH] gpu: Reject FBO attachment levels below TEXTURE_BASE_LEVEL
Per ES 3.0 section 4.4.4.1, a framebuffer attachment is incomplete if
its texture level is less than TEXTURE_BASE_LEVEL. Chrome was missing
this check in Texture::CanRenderTo, potentially allowing a level <
base_level to be rendered to.
Bug: 548130125
Change-Id: Ie13bb528873610aaf61e1768a91928574d5d4653
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8280604
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1685010}
---
diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc
index c9a764c..bc6a850 100644
--- a/gpu/command_buffer/service/texture_manager.cc
+++ b/gpu/command_buffer/service/texture_manager.cc
@@ -1768,6 +1768,9 @@
if (face_infos_.size() == 6 && !cube_complete())
return false;
DCHECK(level >= 0 && level < static_cast<GLint>(MaxValidMipLevel()));
+ if (level < base_level_) {
+ return false;
+ }
if (level > base_level_ && !texture_complete()) {
return false;
}
diff --git a/gpu/command_buffer/service/texture_manager_unittest.cc b/gpu/command_buffer/service/texture_manager_unittest.cc
index 8d06b15..50fa6397 100644
--- a/gpu/command_buffer/service/texture_manager_unittest.cc
+++ b/gpu/command_buffer/service/texture_manager_unittest.cc
@@ -742,6 +742,17 @@
manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1,
0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect());
EXPECT_TRUE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 0));
+
+ // Verify that rendering to a level < base_level is not allowed.
+ EXPECT_CALL(*gl_, TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1))
+ .Times(1)
+ .RetiresOnSaturation();
+ manager_->SetParameteri("", error_state_.get(), texture_ref_.get(),
+ GL_TEXTURE_BASE_LEVEL, 1);
+ manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 1, GL_RGBA, 0, 0, 1,
+ 0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect());
+ EXPECT_FALSE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 0));
+ EXPECT_TRUE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 1));
}
TEST_F(TextureTest, CanNotRenderTo) {
Regression Test / PoC
diff --git a/gpu/command_buffer/service/texture_manager_unittest.cc b/gpu/command_buffer/service/texture_manager_unittest.cc
index 8d06b15..50fa6397 100644
--- a/gpu/command_buffer/service/texture_manager_unittest.cc
+++ b/gpu/command_buffer/service/texture_manager_unittest.cc
@@ -742,6 +742,17 @@
manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1,
0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect());
EXPECT_TRUE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 0));
+
+ // Verify that rendering to a level < base_level is not allowed.
+ EXPECT_CALL(*gl_, TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1))
+ .Times(1)
+ .RetiresOnSaturation();
+ manager_->SetParameteri("", error_state_.get(), texture_ref_.get(),
+ GL_TEXTURE_BASE_LEVEL, 1);
+ manager_->SetLevelInfo(texture_ref_.get(), GL_TEXTURE_2D, 1, GL_RGBA, 0, 0, 1,
+ 0, GL_RGBA, GL_UNSIGNED_BYTE, gfx::Rect());
+ EXPECT_FALSE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 0));
+ EXPECT_TRUE(texture_ref_->texture()->CanRenderTo(feature_info.get(), 1));
}
TEST_F(TextureTest, CanNotRenderTo) {