Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebGL
DescriptionBuffer overflow in WebGL
ComponentWebGL
Sub-componentGPU
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker548085797
Fix commit02445b5049d7 (chromium/src) +51/-1
CWECWE-121 (Stack buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-17 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-17
Patch gap21 days — fix public in source before the advisory

Background

`max_texture_image_units_`
Chrome’s cached copy of GL_MAX_TEXTURE_IMAGE_UNITS, the count of fragment-shader texture image units the underlying driver reports; internal per-unit arrays and uniform bookkeeping are sized from it.
`ContextGroup`
the gpu::gles2 object that queries driver capabilities once and shares them (including max_texture_image_units_) across the GLES2 contexts in a share group.
GPU driver bug workaround
a named flag in gpu_driver_bug_list.json / gpu_workaround_list.txt (here max_texture_image_units_13) that Chrome applies to known-broken driver/GPU combinations to override otherwise-trusted driver values.
PowerVR
the Imagination Technologies mobile GPU family whose affected drivers over-report the serviceable fragment texture unit count.

Root Cause Analysis

On affected PowerVR drivers, GL_MAX_TEXTURE_IMAGE_UNITS reports a value larger than the number of texture units the driver can actually service consistently. Chrome trusts this reported value and stores it in max_texture_image_units_, then sizes internal per-unit arrays and uniform-related bookkeeping to it. When client GL commands bind or sample texture units at indices up to the reported maximum, the driver dereferences per-unit state it does not actually back, producing reads and writes past the valid range — an out-of-bounds access rooted in the driver rather than in Chrome’s own array math. Because the trusted maximum flows into both the validating command decoder and the passthrough command decoder, the inflated bound is honored on both paths.

The fix adds the max_texture_image_units_13 workaround and clamps max_texture_image_units_ to 13u in context_group.cc (mirrored in gl_utils.cc and gles2_cmd_decoder_passthrough_doers.cc), keeping the exposed unit count within the range the driver can safely serve.

Key insight
The vulnerability is a driver-reported capability that is larger than the driver can honor; trusting a hardware-provided upper bound rather than a conservative known-safe limit turns a legitimate GL usage pattern into an out-of-bounds access.

Attack Path

  1. Reach WebGL on PowerVR A victim on an affected PowerVR device loads an attacker’s page that obtains a WebGL context, which is backed by the vulnerable driver.
  2. Read the inflated limit The page queries GL_MAX_TEXTURE_IMAGE_UNITS (or infers it) and sees the driver’s over-reported unit count that Chrome has accepted into max_texture_image_units_.
  3. Bind high texture units The page binds textures and issues draws sampling texture image units at indices near the reported maximum but beyond what the driver actually backs.
  4. Trigger the OOB The driver accesses per-unit state past its valid range, causing the out-of-bounds read/write and consequent buffer overflow in the GPU process.

Impact Assessment

Successful exploitation causes an out-of-bounds read/write (buffer overflow) inside the GPU process on affected PowerVR devices, reachable from untrusted WebGL content. Depending on driver behavior this ranges from GPU-process corruption and crashes to potential memory disclosure or a step toward code execution in the GPU process. Scope is limited to the affected PowerVR driver/GPU population.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
modified
TEST_F
gpu/command_buffer/service/context_group_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/context_group_unittest.cc
  • gpu/command_buffer/service/gl_utils.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
  • content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
  • content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
  • gpu/command_buffer/service/context_group.cc
  • gpu/config/gpu_driver_bug_list.json
  • gpu/config/gpu_workaround_list.txt

Audit Directions

  • Trusted driver limits
    Search for other GL_MAX_* / driver-reported capabilities (texture units, vertex attribs, uniform vectors, draw buffers) that Chrome caches and uses to size arrays without a conservative clamp; each is a candidate for the same over-report class of bug.
  • Workaround parity across decoders
    Verify that every capability-clamping workaround is applied identically in the validating decoder (context_group.cc), the shared GLCapabilities path (gl_utils.cc), and the passthrough path (gles2_cmd_decoder_passthrough_doers.cc); a clamp missing on one path leaves the OOB reachable there.
  • Driver-bug-list coverage
    Review gpu_driver_bug_list.json matchers for max_texture_image_units_13 and related entries to confirm they cover the full range of affected PowerVR driver versions and do not under-match vulnerable devices.
From 02445b5049d75fbc8b5a9c2af883716a41f35ae9 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <[email protected]>
Date: Thu, 27 Aug 2026 09:05:06 -0700
Subject: [PATCH] GPU: Clamp max_texture_image_units to 13 on PowerVR GPUs

Limit max_texture_image_units to 13 on PowerVR GPUs via workaround
max_texture_image_units_13 across ContextGroup, GLCapabilities,
and the Passthrough command decoder.

Bug: 548085797
Change-Id: I38766bde1399d6f40f32840439f415ee7fda8e77
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8297925
Auto-Submit: Zhenyao Mo <[email protected]>
Reviewed-by: Kenneth Russell <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1687286}
---
diff --git a/gpu/command_buffer/service/context_group.cc b/gpu/command_buffer/service/context_group.cc
index a3bd509..19066ac 100644
--- a/gpu/command_buffer/service/context_group.cc
+++ b/gpu/command_buffer/service/context_group.cc
@@ -412,6 +412,9 @@
     return was_lost ? gpu::ContextResult::kTransientFailure
                     : gpu::ContextResult::kFatalFailure;
   }
+  if (feature_info_->workarounds().max_texture_image_units_13) {
+    max_texture_image_units_ = std::min(max_texture_image_units_, 13u);
+  }
   if (!QueryGLFeatureU(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS,
                        kMinVertexTextureImageUnits,
                        &max_vertex_texture_image_units_)) {
diff --git a/gpu/command_buffer/service/gl_utils.cc b/gpu/command_buffer/service/gl_utils.cc
index e64b4d8..5618036 100644
--- a/gpu/command_buffer/service/gl_utils.cc
+++ b/gpu/command_buffer/service/gl_utils.cc
@@ -167,6 +167,9 @@
                 &caps->max_fragment_uniform_vectors);
   glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &caps->max_renderbuffer_size);
   glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &caps->max_texture_image_units);
+  if (feature_info->workarounds().max_texture_image_units_13) {
+    caps->max_texture_image_units = std::min(caps->max_texture_image_units, 13);
+  }
   glGetIntegerv(GL_MAX_VARYING_VECTORS, &caps->max_varying_vectors);
   glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &caps->max_vertex_attribs);
   glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS,
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
index 9019316..4aeff350 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
@@ -1638,11 +1638,18 @@
                                                         GLsizei bufsize,
                                                         GLsizei* length,
                                                         GLint* params) {
-  return GetNumericHelper(
+  error::Error result = GetNumericHelper(
       pname, bufsize, length, params,
       [this](GLenum pname, GLsizei bufsize, GLsizei* length, GLint* params) {
         api()->glGetIntegervRobustANGLEFn(pname, bufsize, length, params);
       });
+  if (result == error::kNoError && pname == GL_MAX_TEXTURE_IMAGE_UNITS &&
+      feature_info_->workarounds().max_texture_image_units_13) {
+    if (bufsize >= 1 && params) {
+      params[0] = std::min(params[0], 13);
+    }
+  }
+  return result;
 }
 
 error::Error GLES2DecoderPassthroughImpl::DoGetInternalformativ(GLenum target,
diff --git a/gpu/config/gpu_driver_bug_list.json b/gpu/config/gpu_driver_bug_list.json
index 1ec0662..d7f3e66a 100644
--- a/gpu/config/gpu_driver_bug_list.json
+++ b/gpu/config/gpu_driver_bug_list.json
@@ -3852,6 +3852,18 @@
       "features": [
         "reset_tex_storage_base_level"
       ]
+    },
+    {
+      "id": 491,
+      "cr_bugs": [548085797],
+      "description": "Limit max texture image units to 13 on PowerVR GPUs.",
+      "os": {
+        "type": "android"
+      },
+      "gl_vendor": "Imagination.*",
+      "features": [
+        "max_texture_image_units_13"
+      ]
     }
   ]
 }
diff --git a/gpu/config/gpu_workaround_list.txt b/gpu/config/gpu_workaround_list.txt
index f001823c..83b8bfc 100644
--- a/gpu/config/gpu_workaround_list.txt
+++ b/gpu/config/gpu_workaround_list.txt
@@ -107,6 +107,7 @@
 max_msaa_sample_count_2
 max_msaa_sample_count_4
 max_num_hw_video_decoders_10
+max_texture_image_units_13
 msaa_is_slow
 msaa_is_slow_2
 multisample_renderbuffer_resize_emulation
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
index 8b668d5f..d3489b2 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
@@ -818,6 +818,7 @@
 crbug.com/446122639 [ android android-pixel-10 ] WebglExtension_OES_texture_float_linear [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/context/context-size-change.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/glsl/functions/glsl-function-cross.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] conformance/glsl/variables/glsl-built-ins.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/ogles/GL/build/build_009_to_016.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/rendering/gl-scissor-fbo-test.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/textures/canvas/tex-2d-rgb-rgb-unsigned_byte.html [ Failure ]
@@ -846,6 +847,8 @@
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/compressed-tex-image.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/copy-texture-image-same-texture.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/tex-mipmap-levels.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] deqp/functional/gles3/integerstatequery.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] deqp/functional/gles3/shaderbuiltinvar.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] deqp/functional/gles3/shaderstruct.html [ Failure ]
 crbug.com/527094637 [ android android-pixel-10 ] deqp/functional/gles3/uniformbuffers/random.html [ Failure ]
 
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
index 004a9286..b01e4fa5 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
@@ -569,6 +569,7 @@
 crbug.com/446122639 [ android android-pixel-10 ] WebglExtension_OES_texture_float_linear [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/context/context-size-change.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/glsl/functions/glsl-function-cross.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] conformance/glsl/variables/glsl-built-ins.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/ogles/GL/build/build_009_to_016.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/rendering/gl-scissor-fbo-test.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/textures/canvas/tex-2d-rgb-rgb-unsigned_byte.html [ Failure ]
diff --git a/gpu/command_buffer/service/context_group_unittest.cc b/gpu/command_buffer/service/context_group_unittest.cc
index cf518a5..3bf1ede2 100644
--- a/gpu/command_buffer/service/context_group_unittest.cc
+++ b/gpu/command_buffer/service/context_group_unittest.cc
@@ -109,6 +109,26 @@
   EXPECT_TRUE(group_->shader_manager() == nullptr);
 }
 
+TEST_F(ContextGroupTest, MaxTextureImageUnits13Workaround) {
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.max_texture_image_units_13 = true;
+  auto feature_info =
+      base::MakeRefCounted<FeatureInfo>(workarounds, GpuFeatureInfo());
+  auto group = base::MakeRefCounted<ContextGroup>(
+      gpu_preferences_, /*memory_tracker=*/nullptr,
+      /*shader_translator_cache=*/nullptr,
+      /*framebuffer_completeness_cache=*/nullptr, feature_info,
+      /*progress_reporter=*/nullptr, GpuFeatureInfo(), &shared_image_manager_);
+
+  TestHelper::SetupContextGroupInitExpectations(
+      gl_.get(), "ANGLE", "OpenGL ES 2.0", CONTEXT_TYPE_OPENGLES2);
+  group->Initialize(decoder_.get(), CONTEXT_TYPE_OPENGLES2);
+  EXPECT_EQ(static_cast<uint32_t>(TestHelper::kMaxTextureImageUnits),
+            group->max_texture_image_units());
+
+  group->Destroy(decoder_.get(), false);
+}
+
 TEST_F(ContextGroupTest, MultipleContexts) {
   FakeCommandBufferServiceBase command_buffer_service2;
   FakeDecoderClient client2;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
index 8b668d5f..d3489b2 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
@@ -818,6 +818,7 @@
 crbug.com/446122639 [ android android-pixel-10 ] WebglExtension_OES_texture_float_linear [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/context/context-size-change.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/glsl/functions/glsl-function-cross.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] conformance/glsl/variables/glsl-built-ins.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/ogles/GL/build/build_009_to_016.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/rendering/gl-scissor-fbo-test.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/textures/canvas/tex-2d-rgb-rgb-unsigned_byte.html [ Failure ]
@@ -846,6 +847,8 @@
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/compressed-tex-image.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/copy-texture-image-same-texture.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] conformance2/textures/misc/tex-mipmap-levels.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] deqp/functional/gles3/integerstatequery.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] deqp/functional/gles3/shaderbuiltinvar.html [ Failure ]
 crbug.com/446941342 [ android android-pixel-10 ] deqp/functional/gles3/shaderstruct.html [ Failure ]
 crbug.com/527094637 [ android android-pixel-10 ] deqp/functional/gles3/uniformbuffers/random.html [ Failure ]
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
index 004a9286..b01e4fa5 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
@@ -569,6 +569,7 @@
 crbug.com/446122639 [ android android-pixel-10 ] WebglExtension_OES_texture_float_linear [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/context/context-size-change.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/glsl/functions/glsl-function-cross.html [ Failure ]
+crbug.com/551573368 [ android android-pixel-10 ] conformance/glsl/variables/glsl-built-ins.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/ogles/GL/build/build_009_to_016.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/rendering/gl-scissor-fbo-test.html [ Failure ]
 crbug.com/446122639 [ android android-pixel-10 ] conformance/textures/canvas/tex-2d-rgb-rgb-unsigned_byte.html [ Failure ]
diff --git a/gpu/command_buffer/service/context_group_unittest.cc b/gpu/command_buffer/service/context_group_unittest.cc
index cf518a5..3bf1ede2 100644
--- a/gpu/command_buffer/service/context_group_unittest.cc
+++ b/gpu/command_buffer/service/context_group_unittest.cc
@@ -109,6 +109,26 @@
   EXPECT_TRUE(group_->shader_manager() == nullptr);
 }
 
+TEST_F(ContextGroupTest, MaxTextureImageUnits13Workaround) {
+  GpuDriverBugWorkarounds workarounds;
+  workarounds.max_texture_image_units_13 = true;
+  auto feature_info =
+      base::MakeRefCounted<FeatureInfo>(workarounds, GpuFeatureInfo());
+  auto group = base::MakeRefCounted<ContextGroup>(
+      gpu_preferences_, /*memory_tracker=*/nullptr,
+      /*shader_translator_cache=*/nullptr,
+      /*framebuffer_completeness_cache=*/nullptr, feature_info,
+      /*progress_reporter=*/nullptr, GpuFeatureInfo(), &shared_image_manager_);
+
+  TestHelper::SetupContextGroupInitExpectations(
+      gl_.get(), "ANGLE", "OpenGL ES 2.0", CONTEXT_TYPE_OPENGLES2);
+  group->Initialize(decoder_.get(), CONTEXT_TYPE_OPENGLES2);
+  EXPECT_EQ(static_cast<uint32_t>(TestHelper::kMaxTextureImageUnits),
+            group->max_texture_image_units());
+
+  group->Destroy(decoder_.get(), false);
+}
+
 TEST_F(ContextGroupTest, MultipleContexts) {
   FakeCommandBufferServiceBase command_buffer_service2;
   FakeDecoderClient client2;
Loading diff…

Original Bug Report

reported by [email protected]

IMG: an out-of-bounds stack write when a shader uses many samplers under heavy uniform pressure

  • Attack surface: WebGL2 shader compile and link plus one draw call, from an untrusted web page.
  • Impact: Deterministic, extent-steerable out-of-bounds stack write in the unsandboxed GPU process.

Android internal bug: b/548085134

View on issue tracker