CVE-2026-19157
Overview
Background
- Immutable texture
- a texture whose storage layout (level count, dimensions, format) is fixed at creation via
glTexStorage2Dand cannot be reallocated, tracked in ANGLE bymState.getImmutableFormat(). - `TEXTURE_BASE_LEVEL`
- the sampler parameter selecting the smallest (highest-resolution) mip level a shader may sample; raising it changes the effective base level (
mState.getEffectiveBaseLevel()) without touching the underlying storage. - Sparse page map / twiddled miptree
- a PowerVR driver-internal table (built by the driver routine named
GetTwiddledMiptreeSparsePageMapin the commit message) that maps mip levels to physical pages; per the commit message it is sized once at initial residency from the level-0 dimensions. - NPOT
- a non-power-of-two texture, detected here by
gl::isPow2(levelZeroSize.width)/gl::isPow2(levelZeroSize.height); the sparse page map miscalculation is specific to NPOT immutable textures.
Root Cause Analysis
Per the patch commit message, PowerVR GPU drivers size the sparse page map for an NPOT immutable texture exactly once, at initial residency, using the level-0 dimensions. When an application later increases TEXTURE_BASE_LEVEL on that texture, the driver’s GetTwiddledMiptreeSparsePageMap recomputes page indices from the new effective base level while still using the original full mip-chain offsets, so it indexes past the end of the page map that was sized for level 0 — an out-of-bounds write into the Scudo heap. The invariant violated is that the driver’s page-map bookkeeping assumes the base level never moves after residency is established, which ANGLE’s normal TEXTURE_BASE_LEVEL handling (a plain state sync, with no storage recreation) does not enforce.
The fix adds the recreateImmutableTextureOnBaseLevelIncrease workaround: when the gl::Texture::DIRTY_BIT_BASE_LEVEL dirty bit is set on a 2D immutable NPOT texture and the applied base level actually changed (mAppliedBaseLevel != newBase), ANGLE calls recreateNativeStoragePreservingLevels, which allocates a fresh native texture via setStorage, copies every level across with copyTextureLevels (using copyImageSubData), and deletes the old one. Because the new storage is created from scratch, the driver rebuilds its sparse page map correctly and no OOB write occurs. The feature is gated to PowerVR vendors only (ANGLE_FEATURE_CONDITION(features, recreateImmutableTextureOnBaseLevelIncrease, IsPowerVR(vendor))), so other GPUs keep the cheap path.
TEXTURE_BASE_LEVEL moves the effective base — it does not, and the stale offsets drive an OOB heap write inside the driver. The fix sidesteps the buggy code path entirely by recreating the native storage on base-level increase so the driver recomputes the page map from a fresh initial-residency state.Attack Path
- Create an NPOT immutable texture
A page (via WebGL2/
glTexStorage2D) allocates an immutable 2D texture with non-power-of-two dimensions and multiple mip levels (the test uses 17x17 with 5 levels), which is the exact shape the PowerVR sparse page map mis-sizes. - Establish residency at base level 0 The texture is sampled/drawn at least once so the PowerVR driver commits the sparse page map sized from the level-0 dimensions.
- Increase `TEXTURE_BASE_LEVEL`
The page calls
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, n)to raise the effective base level; before the patch ANGLE forwards this as an ordinary state change with no storage recreation. - Trigger the driver page-map recompute
On the next draw/sample,
GetTwiddledMiptreeSparsePageMaprecalculates indices from the new base level using the original mip-chain offsets and writes out of bounds of the level-0-sized page map, corrupting the Scudo heap.(inferred)— the OOB write is in closed PowerVR driver code and is described only by the commit message, not shown in the diff. - Escalate via heap corruption
Controlled adjacent heap contents could be corrupted to gain a stronger read/write primitive or code execution.
(inferred)— no such primitive or escalation is demonstrated by the patch.
Impact Assessment
TEXTURE_BASE_LEVEL increase. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory reports only the OOB write, and the patch does not state exploited-in-the-wild status or a severity beyond the OOB-write classification.Changed Functions
| Function | Change | Notes |
|---|---|---|
Texture2DTestES3_RecreateImmutableOnBaseLevelIncreasesrc/tests/gl_tests/TextureTest.cpp |
modified |
Files Changed
src/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/gl/TextureGL.hutil/autogen/angle_features_autogen.hsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hutil/autogen/angle_features_autogen.cppsrc/libANGLE/renderer/gl/TextureGL.cppinclude/platform/gl_features.json
Audit Directions
- Immutable-storage assumptions after post-creation state changesAudit every ANGLE
TextureGLpath that mutates sampler/level state on an immutable texture (base level, max level, LOD clamp) for the assumption that native storage created once stays valid; look for other parameters whose change should, but does not, trigger driver-side reallocation on quirky vendors. - NPOT and vendor-specific driver quirksReview other
IsPowerVR-gated and NPOT-conditioned code paths (e.g.resetBaseLevelForASTCSubImage) for similar page-map / miptree sizing bugs where the driver computes layout once and never revisits it. - Dirty-bit handlers that forward raw state to the driverExamine dirty-bit dispatch in the GL backend for cases that pass state through without checking whether the underlying native object must be recreated to keep driver-internal bookkeeping consistent, since the missing recreation on
DIRTY_BIT_BASE_LEVELis what exposed this bug.
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/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: + 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); 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, ... (file diff truncated)
Regression Test / PoC
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);
Original Bug Report
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