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 <[email protected]> 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 <[email protected]> Commit-Queue: Geoff Lang <[email protected]> Auto-Submit: Zhenyao Mo <[email protected]> Reviewed-by: Geoff Lang <[email protected]> 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) {