Chrome · ANGLE
CVE-2026-19157
OOB in ANGLE
Overview
Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
Texture2DTestES3_RecreateImmutableOnBaseLevelIncreasesrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/TextureGL.cppsrc/libANGLE/renderer/gl/TextureGL.hsrc/libANGLE/renderer/gl/renderergl_utils.cppsrc/tests/gl_tests/TextureTest.cpp
Patch
From 764bb891d3b8f4d3c06151d95deb5e70878cc9c5 Mon Sep 17 00:00:00 2001 From: Zhenyao Mo <[email protected]> Date: Thu, 16 Jul 2026 11:41:08 -0700 Subject: [PATCH] GL: Recreate PowerVR immutable texture on BASE_LEVEL increase PowerVR drivers size the sparse page map for NPOT immutable textures at initial residency based on level 0 dimensions. Increasing TEXTURE_BASE_LEVEL afterwards causes GetTwiddledMiptreeSparsePageMap to calculate indices using the original mip-chain offsets from the new effective base level, resulting in an out-of-bounds write into the Scudo heap. This change adds a workaround feature recreateImmutableTextureOnBaseLevelIncrease for PowerVR GPUs. When TEXTURE_BASE_LEVEL is increased on an immutable texture, the native storage is recreated and existing levels are preserved, forcing the driver to calculate sparse page map sizes correctly from scratch. Bug: chromium:534903095 Change-Id: I0329da469ab3e6b267f30f6ea10fdc1d574bf6a8 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8106040 Reviewed-by: Geoff Lang <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Zhenyao Mo <[email protected]> --- diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h index 2b4a23e..e11e4d1 100644 --- a/include/platform/autogen/FeaturesGL_autogen.h +++ b/include/platform/autogen/FeaturesGL_autogen.h @@ -260,6 +260,12 @@ &members, }; + FeatureInfo recreateImmutableTextureOnBaseLevelIncrease = { + "recreateImmutableTextureOnBaseLevelIncrease", + FeatureCategory::OpenGLWorkarounds, + &members, + }; + FeatureInfo limitMax3dArrayTextureSizeTo1024 = { "limitMax3dArrayTextureSizeTo1024", FeatureCategory::OpenGLWorkarounds, diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json index 789e621..b2acdae 100644 --- a/include/platform/gl_features.json +++ b/include/platform/gl_features.json @@ -318,6 +318,15 @@ "issue": "https://crbug.com/528131939" }, { + "name": "recreate_immutable_texture_on_base_level_increase", + "category": "Workarounds", + "description": [ + "Recreate native storage for immutable textures when TEXTURE_BASE_LEVEL is increased to ", + "work around PowerVR sparse page map OOB driver bug." + ], + "issue": "https://crbug.com/528131119" + }, + { "name": "limit_max_3d_array_texture_size_to_1024", "category": "Workarounds", "description": [ diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp index d0f10ee..b49c7f3 100644 --- a/src/libANGLE/renderer/gl/TextureGL.cpp +++ b/src/libANGLE/renderer/gl/TextureGL.cpp @@ -11,6 +11,7 @@ #include "common/bitset_utils.h" #include "common/debug.h" +#include "common/mathutil.h" #include "common/utilities.h" #include "libANGLE/Context.h" #include "libANGLE/Display.h" @@ -1832,6 +1833,24 @@ return angle::Result::Continue; } + if (dirtyBits[gl::Texture::DIRTY_BIT_BASE_LEVEL] && + GetFeaturesGL(context).recreateImmutableTextureOnBaseLevelIncrease.enabled && + mState.getImmutableFormat() && getType() == gl::TextureType::_2D) + { + const GLuint newBase = mState.getEffectiveBaseLevel(); + if (mAppliedBaseLevel != newBase) + { + const gl::Extents &levelZeroSize = mState.getLevelZeroDesc().size; + const bool isNPOT = + !gl::isPow2(levelZeroSize.width) || !gl::isPow2(levelZeroSize.height); + const FunctionsGL *functions = GetFunctionsGL(context); + if (functions->copyImageSubData && isNPOT) + { + ANGLE_TRY(recreateNativeStoragePreservingLevels(context)); + } + } + } + const FunctionsGL *functions = GetFunctionsGL(context); StateManagerGL *stateManager = GetStateManagerGL(context); @@ -2291,6 +2310,59 @@ return angle::Result::Continue; } +angle::Result TextureGL::copyTextureLevels(const gl::Context *context, + GLuint srcTexture, + GLuint dstTexture) +{ + const FunctionsGL *functions = GetFunctionsGL(context); + ASSERT(functions->copyImageSubData); + + const GLuint immutableLevels = mState.getImmutableLevels(); + for (GLuint level = 0; level < immutableLevels; ++level) + { + const gl::Extents levelSize = mState.getImageDesc(gl::TextureTarget::_2D, level).size; + ASSERT(!levelSize.empty()); + ANGLE_GL_TRY(context, functions->copyImageSubData( + srcTexture, GL_TEXTURE_2D, static_cast<GLint>(level), 0, 0, 0, + dstTexture, GL_TEXTURE_2D, static_cast<GLint>(level), 0, 0, 0, + levelSize.width, levelSize.height, 1)); + } + return angle::Result::Continue; +} + +angle::Result TextureGL::recreateNativeStoragePreservingLevels(const gl::Context *context) +{ + ASSERT(getType() == gl::TextureType::_2D); + ASSERT(mState.getImmutableFormat()); + ASSERT(!gl::isPow2(mState.getLevelZeroDesc().size.width) || + !gl::isPow2(mState.getLevelZeroDesc().size.height)); + + const FunctionsGL *functions = GetFunctionsGL(context); + StateManagerGL *stateManager = GetStateManagerGL(context); + + GLuint oldTextureID = mTextureID; + + functions->genTextures(1, &mTextureID); + stateManager->bindTexture(getType(), mTextureID); + + mAppliedSwizzle = gl::SwizzleState(); + mAppliedSampler = gl::SamplerState::CreateDefaultForTarget(getType()); + mAppliedBaseLevel = 0; + mAppliedMaxLevel = gl::kInitialMaxLevel; + + const gl::ImageDesc &levelZeroDesc = mState.getLevelZeroDesc(); + ANGLE_TRY(setStorage(context, getType(), mState.getImmutableLevels(), + levelZeroDesc.format.info->sizedInternalFormat, levelZeroDesc.size)); + ANGLE_TRY(copyTextureLevels(context, oldTextureID, mTextureID)); + + stateManager->deleteTexture(oldTextureID); + + mLocalDirtyBits = mAllModifiedDirtyBits; + onStateChange(angle::SubjectMessage::SubjectChanged); + + return angle::Result::Continue; +} + angle::Result TextureGL::syncTextureStateSwizzle(const gl::Context *context, const FunctionsGL *functions, GLenum name, diff --git a/src/libANGLE/renderer/gl/TextureGL.h b/src/libANGLE/renderer/gl/TextureGL.h index fed808a..c2eed85 100644 --- a/src/libANGLE/renderer/gl/TextureGL.h +++ b/src/libANGLE/renderer/gl/TextureGL.h @@ -224,6 +224,10 @@ private: angle::Result recreateTexture(const gl::Context *context); + angle::Result copyTextureLevels(const gl::Context *context, + GLuint srcTexture, + GLuint dstTexture); + angle::Result recreateNativeStoragePreservingLevels(const gl::Context *context); angle::Result setImageHelper(const gl::Context *context, gl::TextureTarget target, diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp index 8c78bae..05aa3dd 100644 --- a/src/libANGLE/renderer/gl/renderergl_utils.cpp +++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp @@ -2492,6 +2492,8 @@ IsApple() && isIntel && GetMacOSVersion() >= OSVersion(10, 12, 4)); ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCSubImage, IsPowerVR(vendor)); + ANGLE_FEATURE_CONDITION(features, recreateImmutableTextureOnBaseLevelIncrease, + IsPowerVR(vendor)); ANGLE_FEATURE_CONDITION(features, adjustSrcDstRegionForBlitFramebuffer, IsLinux() || (IsAndroid() && isNvidia) || (IsWindows() && isNvidia) || diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index ce9e05b..096dd99 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -7954,6 +7954,218 @@ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); } +class Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease : public Texture2DTestES3 +{ + protected:
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 ce9e05b..096dd99 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -7954,6 +7954,218 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+class Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease : public Texture2DTestES3
+{
+ protected:
+ void testSetUp() override
+ {
+ Texture2DTestES3::testSetUp();
+ setUpProgram();
+ glUseProgram(mProgram);
+ glUniform1i(mTexture2DUniformLocation, 0);
+ }
+
+ void setUpNPOT5LevelImmutableTexture(GLTexture &tex, uint32_t *widthOut, uint32_t *heightOut)
+ {
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // 17x17 is NPOT. 5 levels (17x17, 8x8, 4x4, 2x2, 1x1)
+ glTexStorage2D(GL_TEXTURE_2D, 5, GL_RGBA8, 17, 17);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ std::vector<GLColor> level0Data(17 * 17, GLColor::red);
+ std::vector<GLColor> level1Data(8 * 8, GLColor::blue);
+ std::vector<GLColor> level2Data(4 * 4, GLColor::green);
+ std::vector<GLColor> level3Data(2 * 2, GLColor::yellow);
+ std::vector<GLColor> level4Data(1 * 1, GLColor::magenta);
+
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 17, 17, GL_RGBA, GL_UNSIGNED_BYTE,
+ level0Data.data());
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, level1Data.data());
+ glTexSubImage2D(GL_TEXTURE_2D, 2, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, level2Data.data());
+ glTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, level3Data.data());
+ glTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, level4Data.data());
+
+ *widthOut = getWindowWidth();
+ *heightOut = getWindowHeight();
+ ASSERT_GE(*widthOut, 17u);
+ ASSERT_GE(*heightOut, 17u);
+ }
+};
+
+// Test that changing TEXTURE_BASE_LEVEL on an NPOT immutable texture after sampling it works
+// correctly.
+TEST_P(Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease, ImmutableNPOTTextureBaseLevelIncrease)
+{
+ GLTexture tex;
+ uint32_t w, h;
+ setUpNPOT5LevelImmutableTexture(tex, &w, &h);
+
+ // Sample at base level 0 first
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::red);
+
+ // Limit max level to 2
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
+
+ // Set base level to 1 and sample
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::blue);
+
+ // Set base level to 2 and sample
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::green);
+
+ // Set base level back to 1 and sample
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::blue);
+
+ // Sample at base level 0 again, its data should not be lost.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::red);
+
+ // Restore max level to 3, set base level to 3, and verify level 3 data
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::yellow);
+
+ EXPECT_GL_NO_ERROR();
+}
+
+// Test that changing TEXTURE_BASE_LEVEL on an NPOT immutable texture after sampling it works
+// correctly when TEXTURE_MAX_LEVEL is also set and updated.
+TEST_P(Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease,
+ ImmutableNPOTTextureBaseLevelIncreaseMaxLevel)
+{
+ GLTexture tex;
+ uint32_t w, h;
+ setUpNPOT5LevelImmutableTexture(tex, &w, &h);
+
+ // Set MAX level to 1
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+ // Draw to sync everything (BASE level = 0, MAX level = 1)
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::red);
+
+ // Set BASE level to 1.
+ // This triggers the workaround, recreating the texture.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+
+ // Draw such that the texture is minimized.
+ glViewport(0, 0, 1, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ // Restore viewport
+ glViewport(0, 0, w, h);
+
+ // Set MAX level to 1000
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1000);
+
+ // Draw again with minification
+ glViewport(0, 0, 1, 1);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::magenta);
+
+ // Restore viewport
+ glViewport(0, 0, w, h);
+
+ EXPECT_GL_NO_ERROR();
+}
+
+// Test that changing TEXTURE_BASE_LEVEL on an NPOT immutable texture after sampling it works
+// correctly when texture swizzle is also set and updated.
+TEST_P(Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease,
+ ImmutableNPOTTextureBaseLevelIncreaseSwizzle)
+{
+ GLTexture tex;
+ uint32_t w, h;
+ setUpNPOT5LevelImmutableTexture(tex, &w, &h);
+
+ // Set swizzle: R->B, B->R
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
+
+ // Draw to sync everything (BASE level = 0, swizzled).
+ // Level 0 is red (1, 0, 0, 1). Swizzled: R gets B (0), B gets R (1).
+ // So output should be blue (0, 0, 1, 1).
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::blue);
+
+ // Set BASE level to 1.
+ // This triggers the workaround, recreating the texture.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+
+ // Draw again. Level 1 is blue (0, 0, 1, 1). Swizzled: R gets B (1), B gets R (0).
+ // So output should be red (1, 0, 0, 1).
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::red);
+
+ // Set swizzle back to identity
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
+
+ // Draw again. Level 1 is blue. Output should be blue.
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::blue);
+
+ EXPECT_GL_NO_ERROR();
+}
+
+// Test that changing TEXTURE_BASE_LEVEL on an NPOT immutable texture after sampling it works
+// correctly when sampler parameters like TEXTURE_MIN_LOD are also set and updated.
+TEST_P(Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease,
+ ImmutableNPOTTextureBaseLevelIncreaseMinLOD)
+{
+ GLTexture tex;
+ uint32_t w, h;
+ setUpNPOT5LevelImmutableTexture(tex, &w, &h);
+
+ // Set MIN_LOD to 2.0.
+ // Since BASE_LEVEL = 0, this should clamp LOD to 2.0, sampling level 2 (green).
+ glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 2.0f);
+
+ // Draw to sync everything
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::green);
+
+ // Set BASE level to 1.
+ // This triggers the workaround, recreating the texture.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+
+ // Draw again.
+ // BASE_LEVEL is 1. MIN_LOD is 2.0f.
+ // So the sampled level is BASE_LEVEL + MIN_LOD = 1 + 2 = 3 (yellow).
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::yellow);
+
+ // Set MIN_LOD to 0.0f.
+ glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0.0f);
+
+ // Draw again.
+ // BASE_LEVEL is 1. MIN_LOD is 0.0f.
+ // Under magnification, it should sample BASE_LEVEL = 1 (blue).
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_PIXEL_RECT_EQ(0, 0, w, h, GLColor::blue);
+
+ EXPECT_GL_NO_ERROR();
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(
+ Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+ Texture2DTestES3_RecreateImmutableOnBaseLevelIncrease,
+ ES3_OPENGLES().enable(Feature::RecreateImmutableTextureOnBaseLevelIncrease));
+
void Texture2DTestES3::testCopyImage(const APIExtensionVersion usedExtension)
{
ASSERT(usedExtension == APIExtensionVersion::EXT || usedExtension == APIExtensionVersion::OES);
Loading diff…
Original Bug Report
reported by [email protected]
IMG: OOB heap write via TEXTURE_BASE_LEVEL and texSubImage2D
- Attack surface: WebGL2 texture state and
texSubImage2D, reachable from an untrusted web page in the default Chrome for Android configuration without user interaction. - Impact: Heap out-of-bounds write of a repeated 400+ fixed byte at the heap with the size controlled by an attacker in the unsandboxed Chrome GPU process.
Android internal bug: 530002430
References
On This Page