CVE-2026-93372
Overview
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::gles2object that queries driver capabilities once and shares them (includingmax_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(heremax_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.
Attack Path
- 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.
- 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 intomax_texture_image_units_. - 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc |
modified | |
TEST_Fgpu/command_buffer/service/context_group_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/context_group_unittest.ccgpu/command_buffer/service/gl_utils.ccgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cccontent/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txtcontent/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txtgpu/command_buffer/service/context_group.ccgpu/config/gpu_driver_bug_list.jsongpu/config/gpu_workaround_list.txt
Audit Directions
- Trusted driver limitsSearch 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 decodersVerify that every capability-clamping workaround is applied identically in the validating decoder (
context_group.cc), the sharedGLCapabilitiespath (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 coverageReview
gpu_driver_bug_list.jsonmatchers formax_texture_image_units_13and related entries to confirm they cover the full range of affected PowerVR driver versions and do not under-match vulnerable devices.
Patch
From 02445b5049d75fbc8b5a9c2af883716a41f35ae9 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
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 <zmo@chromium.org>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
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;
Regression Test / PoC
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;
Original Bug Report
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