Chrome · WebGL
CVE-2026-87650
OOB in WebGL
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/libANGLE/renderer/gl/TextureGL.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From 0e80263dfe758c132ec20c5ef649d587de3942f0 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Thu, 16 Jul 2026 15:24:02 +0000 Subject: [PATCH] GL: Clamp BASE/MAX texture levels Clamp base_level and max_level to device limits in TextureGL before passing to the driver. This prevents driver crashes (heap OOB) when wild values are set for these parameters. Handles 2D, 2DArray, 3D, CubeMap, and CubeMapArray textures. Bug: b/534912743 Test: angle_unittests, angle_end2end_tests --gtest_filter=*CubeMapArrayBaseLevelOutOfRange* Change-Id: I8ca8c53a9902d3a9af40ccda98e792655a2d79da Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8110757 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp index 44bbb71..be2ccee 100644 --- a/src/libANGLE/renderer/gl/TextureGL.cpp +++ b/src/libANGLE/renderer/gl/TextureGL.cpp @@ -7,6 +7,7 @@ // TextureGL.cpp: Implements the class methods for TextureGL. #include "libANGLE/renderer/gl/TextureGL.h" +#include "common/mathutil.h" #include "common/unsafe_buffers.h" #include "common/bitset_utils.h" @@ -42,6 +43,23 @@ namespace { +GLuint GetMaxMipmapLevel(const gl::Caps &caps, gl::TextureType target) +{ + switch (target) + { + case gl::TextureType::_2D: + case gl::TextureType::_2DArray: + return static_cast<GLuint>(gl::log2(caps.max2DTextureSize)); + case gl::TextureType::_3D: + return static_cast<GLuint>(gl::log2(caps.max3DTextureSize)); + case gl::TextureType::CubeMap: + case gl::TextureType::CubeMapArray: + return static_cast<GLuint>(gl::log2(caps.maxCubeMapTextureSize)); + default: + return 0u; + } +} + // For use with the uploadTextureDataInChunks feature. See http://crbug.com/1181068 constexpr const size_t kUploadTextureDataInChunksUploadSize = (120 * 1024) - 1; @@ -2055,21 +2073,31 @@ &mAppliedSwizzle.swizzleAlpha)); break; case gl::Texture::DIRTY_BIT_BASE_LEVEL: - if (mAppliedBaseLevel != mState.getEffectiveBaseLevel()) { - mAppliedBaseLevel = mState.getEffectiveBaseLevel(); - ANGLE_GL_TRY(context, functions->texParameteri( - nativegl::GetTextureBindingTarget(getType()), - GL_TEXTURE_BASE_LEVEL, mAppliedBaseLevel)); + const GLuint maxLevelLimit = GetMaxMipmapLevel(context->getCaps(), getType()); + const GLuint clampedBaseLevel = + std::min(mState.getEffectiveBaseLevel(), maxLevelLimit); + if (mAppliedBaseLevel != clampedBaseLevel) + { + mAppliedBaseLevel = clampedBaseLevel; + ANGLE_GL_TRY(context, functions->texParameteri( + nativegl::GetTextureBindingTarget(getType()), + GL_TEXTURE_BASE_LEVEL, mAppliedBaseLevel)); + } } break; case gl::Texture::DIRTY_BIT_MAX_LEVEL: - if (mAppliedMaxLevel != mState.getEffectiveMaxLevel()) { - mAppliedMaxLevel = mState.getEffectiveMaxLevel(); - ANGLE_GL_TRY(context, functions->texParameteri( - nativegl::GetTextureBindingTarget(getType()), - GL_TEXTURE_MAX_LEVEL, mAppliedMaxLevel)); + const GLuint maxLevelLimit = GetMaxMipmapLevel(context->getCaps(), getType()); + const GLuint clampedMaxLevel = + std::min(mState.getEffectiveMaxLevel(), maxLevelLimit); + if (mAppliedMaxLevel != clampedMaxLevel) + { + mAppliedMaxLevel = clampedMaxLevel; + ANGLE_GL_TRY(context, functions->texParameteri( + nativegl::GetTextureBindingTarget(getType()), + GL_TEXTURE_MAX_LEVEL, mAppliedMaxLevel)); + } } break; case gl::Texture::DIRTY_BIT_DEPTH_STENCIL_TEXTURE_MODE: @@ -2124,12 +2152,14 @@ angle::Result TextureGL::setBaseLevel(const gl::Context *context, GLuint baseLevel) { - if (baseLevel != mAppliedBaseLevel) + const GLuint maxLevelLimit = GetMaxMipmapLevel(context->getCaps(), getType()); + const GLuint clampedBaseLevel = std::min(baseLevel, maxLevelLimit); + if (clampedBaseLevel != mAppliedBaseLevel) { const FunctionsGL *functions = GetFunctionsGL(context); StateManagerGL *stateManager = GetStateManagerGL(context); - mAppliedBaseLevel = baseLevel; + mAppliedBaseLevel = clampedBaseLevel; mLocalDirtyBits.set(gl::Texture::DIRTY_BIT_BASE_LEVEL); // Signal to the GL layer that the Impl has dirty bits. @@ -2137,27 +2167,29 @@ stateManager->bindTexture(getType(), mTextureID); ANGLE_GL_TRY(context, functions->texParameteri(ToGLenum(getType()), GL_TEXTURE_BASE_LEVEL, - baseLevel)); + clampedBaseLevel)); } return angle::Result::Continue; } angle::Result TextureGL::setMaxLevel(const gl::Context *context, GLuint maxLevel) { - if (maxLevel != mAppliedMaxLevel) + const GLuint maxLevelLimit = GetMaxMipmapLevel(context->getCaps(), getType()); + const GLuint clampedMaxLevel = std::min(maxLevel, maxLevelLimit); + if (clampedMaxLevel != mAppliedMaxLevel) { const FunctionsGL *functions = GetFunctionsGL(context); StateManagerGL *stateManager = GetStateManagerGL(context); - mAppliedMaxLevel = maxLevel; + mAppliedMaxLevel = clampedMaxLevel; mLocalDirtyBits.set(gl::Texture::DIRTY_BIT_MAX_LEVEL); // Signal to the GL layer that the Impl has dirty bits. onStateChange(angle::SubjectMessage::DirtyBitsFlagged); stateManager->bindTexture(getType(), mTextureID); - ANGLE_GL_TRY(context, - functions->texParameteri(ToGLenum(getType()), GL_TEXTURE_MAX_LEVEL, maxLevel)); + ANGLE_GL_TRY(context, functions->texParameteri(ToGLenum(getType()), GL_TEXTURE_MAX_LEVEL, + clampedMaxLevel)); } return angle::Result::Continue; } @@ -2297,7 +2329,7 @@ mAppliedSampler = gl::SamplerState::CreateDefaultForTarget(getType()); mAppliedBaseLevel = 0; - mAppliedBaseLevel = gl::kInitialMaxLevel; + mAppliedMaxLevel = gl::kInitialMaxLevel; mLocalDirtyBits = mAllModifiedDirtyBits; diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 8ebcd4d..579c4be 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -16639,6 +16639,55 @@ EXPECT_GL_NO_ERROR(); } +// Test that cube map array texture base level and max level are clamped. +TEST_P(TextureCubeTestES32, CubeMapArrayBaseLevelOutOfRange) +{ + // Create a program with samplerCubeArray. + const char *essl32_fs = + R"(#version 320 es + precision highp float; + precision highp samplerCubeArray; + out vec4 my_FragColor; + uniform samplerCubeArray texCubeArray; + void main() + { + my_FragColor = texture(texCubeArray, vec4(0.0, 0.0, 0.0, 0.0)); + })"; + + const char *essl32_vs = + R"(#version 320 es + in vec4 position; + void main() + { + gl_Position = position; + })"; + + ANGLE_GL_PROGRAM(program, essl32_vs, essl32_fs); + glUseProgram(program); + GLint textureLocation = glGetUniformLocation(program, "texCubeArray"); + ASSERT_NE(-1, textureLocation); + glUniform1i(textureLocation, 0); + + GLTexture cubeMapArrayTexture; + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, cubeMapArrayTexture); + + // Define level 0. + glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 2, 2, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE,
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 8ebcd4d..579c4be 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -16639,6 +16639,55 @@
EXPECT_GL_NO_ERROR();
}
+// Test that cube map array texture base level and max level are clamped.
+TEST_P(TextureCubeTestES32, CubeMapArrayBaseLevelOutOfRange)
+{
+ // Create a program with samplerCubeArray.
+ const char *essl32_fs =
+ R"(#version 320 es
+ precision highp float;
+ precision highp samplerCubeArray;
+ out vec4 my_FragColor;
+ uniform samplerCubeArray texCubeArray;
+ void main()
+ {
+ my_FragColor = texture(texCubeArray, vec4(0.0, 0.0, 0.0, 0.0));
+ })";
+
+ const char *essl32_vs =
+ R"(#version 320 es
+ in vec4 position;
+ void main()
+ {
+ gl_Position = position;
+ })";
+
+ ANGLE_GL_PROGRAM(program, essl32_vs, essl32_fs);
+ glUseProgram(program);
+ GLint textureLocation = glGetUniformLocation(program, "texCubeArray");
+ ASSERT_NE(-1, textureLocation);
+ glUniform1i(textureLocation, 0);
+
+ GLTexture cubeMapArrayTexture;
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, cubeMapArrayTexture);
+
+ // Define level 0.
+ glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RGBA8, 2, 2, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+
+ // Set wild base/max level.
+ glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_BASE_LEVEL, 10000);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAX_LEVEL, 10000);
+
+ EXPECT_GL_NO_ERROR();
+
+ // Draw to trigger syncState.
+ drawQuad(program, "position", 0.5f);
+
+ EXPECT_GL_NO_ERROR();
+}
+
// Tests defining a cube map array texture using glTexStorage3D() and filling all levels using
// glTexSubImage3D().
TEST_P(TextureCubeTestES32, ValidateCubeMapArrayTexStorage)
Loading diff…
Original Bug Report
reported by [email protected]
IMG: OOB heap read via copyTexImage2D and TEXTURE_BASE_LEVEL
- Attack surface: WebGL2 texture state and
copyTexImage2D, reachable from an untrusted web page in the default Chrome for Android configuration. - Impact: Heap out-of-bounds read in the unsandboxed Chrome GPU process, convertible to an out-of-bounds write with adjacent-allocation grooming.
Android internal bug: b/533551753
References
On This Page