Critical chrome OOB 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebGL
DescriptionBuffer overflow in WebGL
ComponentWebGL
Bug ClassOOB
Tracker548130125
Fix commit3b907e173dc4 (chromium/src) +14/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

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/service that 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.

Key insight
The single core mistake was that 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

  1. Create a texture and define a low level From WebGL, allocate a texture and call the equivalent of SetLevelInfo on level 0 to give it storage and dimensions.
  2. Raise the base level Set TEXTURE_BASE_LEVEL to 1 (or higher) so level 0 now lies below base_level_ and is outside the logically valid mip range.
  3. Attach the below-base level Bind level 0 of the texture as a framebuffer color attachment, which the unpatched CanRenderTo wrongly reports as renderable.
  4. 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

An attacker running WebGL-capable JavaScript gains an out-of-bounds write inside Chrome’s GPU process by rendering into a framebuffer attachment that should have been rejected as incomplete. The corruption occurs in the sandboxed GPU process, and the primary precondition is the ability to run WebGL and manipulate 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

FunctionChangeNotes
if
gpu/command_buffer/service/texture_manager.cc
modified
TEST_F
gpu/command_buffer/service/texture_manager_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/texture_manager.cc
  • gpu/command_buffer/service/texture_manager_unittest.cc

Audit Directions

  • Range-partial validation
    Flag completeness or bounds checks that only guard one side of a range (here only level > base_level_), since the untested level < base_level_ case is exactly where the invariant leaked.
  • Mutable parameters versus cached state
    Review paths where a parameter like base_level_ can change after level storage is defined via SetLevelInfo, ensuring every consumer re-validates against the current base/max rather than assuming level 0 is always valid.
  • Spec-mandated attachment rules
    Cross-check CanRenderTo and related framebuffer-attachment validators against ES 3.0 section 4.4.4.1 completeness conditions to catch other omitted incompleteness cases.
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.