CVE-2026-76034
Overview
Background
- `glGenerateMipmap`
- GL entry point that derives the full mip chain of a texture from its base level image; ANGLE routes it through
TextureGL::generateMipmap. - Immutable texture
- storage allocated once (e.g.
glTexStorage2D) with fixed levels/format;mState.getImmutableFormat()reports this, and levels cannot be reallocated afterward. - `TEXTURE_BASE_LEVEL`
- the lowest mip level used for sampling/generation; when
effectiveBaseLevel > 0mipmap generation starts partway down the chain rather than at level 0. - `glCopyImageSubData`
- GL copy that transfers pixel data between image levels of two textures without going through the framebuffer, used here to move generated levels back to the original texture.
Root Cause Analysis
On Imagination’s GLES drivers, calling glGenerateMipmap for an immutable TEXTURE_2D whose TEXTURE_BASE_LEVEL is greater than zero triggers a driver bug that miscomputes per-level dimensions and byte offsets. Because the driver assumes generation begins at level 0, its offset math is wrong when the base level is non-zero, so it writes generated mip data past the end of the texture’s fixed immutable storage. Since immutable storage is sized exactly for the declared level range, this out-of-bounds write is a buffer overflow within GPU-backed memory that the application cannot resize or bound. The condition is fully reachable from ordinary WebGL content that binds an immutable texture, sets a non-zero base level, and requests mipmap generation.
The fix does not correct the driver arithmetic but sidesteps it by never asking the driver to generate mipmaps from a non-zero base level. Instead, generation happens on a freshly created temporary texture rooted at base level 0, where the driver’s offset computation is correct.
glCopyImageSubData faithfully returns each generated level to the original immutable texture.Attack Path
- Create immutable texture
Web content allocates an immutable
TEXTURE_2DviaglTexStorage2Dwith a full mip level count. - Raise the base level
Set
TEXTURE_BASE_LEVELgreater than zero soeffectiveBaseLevel > 0when generation is later requested. - Request mipmap generation
Call
glGenerateMipmap, which on an affected Imagination device reachesTextureGL::generateMipmapand, pre-fix, the driver’s native path. - Trigger miscomputed offsets The driver miscalculates level dimensions/offsets for the non-zero base level and writes mip data outside the fixed immutable storage.
- Corrupt adjacent memory The out-of-bounds write overflows the texture’s backing storage, corrupting neighbouring GPU-accessible memory.
Impact Assessment
useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData workaround is enabled.Changed Functions
| Function | Change | Notes |
|---|---|---|
Texture2DTestES3_NonZeroBaseLevelGenMipmapssrc/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/tests/angle_end2end_tests_expectations.txtsrc/libANGLE/renderer/gl/TextureGL.cppinclude/platform/gl_features.json
Audit Directions
- Driver-specific level/offset mathReview other paths that let a non-zero
TEXTURE_BASE_LEVELor non-default level parameters flow into native driver calls on immutable textures, since the same offset miscomputation could recur outside_2Dgeneration. - Immutable-storage write boundariesAudit operations that write into immutable textures (
glGenerateMipmap, copies, sub-image uploads) to confirm level dimensions and offsets stay within the fixed allocated storage. - Feature-gated workaroundsVerify the guard conditions on
useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData(immutable,_2D,effectiveBaseLevel > 0,copyImageSubDatapresent) fully cover the vulnerable cases and that the copy-back path preserves format/level correctness.
Patch
From ef03123d07332bc76db13395b6d3f3e8ac24a3b6 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Thu, 16 Jul 2026 18:39:46 -0700 Subject: [PATCH] GL: add mipmap generation and base level workaround. When generating mipmaps for immutable 2D textures with a base level greater than zero, add a workaround which generates the mipmaps against a new temporary texture starting at base level 0, and then copies each level back to the original texture. Apply this workaround to Imagination's GLES drivers. Require glCopyImageSubData be available for this workaround, which is not a problem for the affected GPUs. Skip the workaround if glCopyImageSubData is not available. Skip the new test on NVIDIA's OpenGL and GLES drivers, as it seems there's a bug in the glCopyImageSubData implementation. TAG: agy CONV: ca9eecd3-cca0-4c6d-94a8-9a4eabe3ccd4 Test: angle_end2end_tests --gtest_filter="*Texture2DTestES3_NonZeroBaseLevelGenMipmaps*" Bug: chromium:534923522 Change-Id: I83c2bbf863f183f286745883cc4f95311944d1b9 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8113050 Reviewed-by: Geoff Lang <[email protected]> Auto-Submit: Kenneth Russell <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Kenneth Russell <[email protected]> --- diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json index 3d77686..32dffc1 100644 --- a/include/platform/gl_features.json +++ b/include/platform/gl_features.json @@ -327,6 +327,15 @@ "issue": "https://crbug.com/528131119" }, { + "name": "use_temp_for_non_zero_base_level_GenMipmap_using_CopyImageSubData", + "category": "Workarounds", + "description": [ + "Allocate a temporary texture when generating mipmaps on an immutable texture with a ", + "non-zero base level to avoid driver bugs." + ], + "issue": "https://crbug.com/534923522" + }, + { "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 b49c7f3..697ff2b 100644 --- a/src/libANGLE/renderer/gl/TextureGL.cpp +++ b/src/libANGLE/renderer/gl/TextureGL.cpp @@ -1608,12 +1608,18 @@ const LevelInfoGL &baseLevelInfo = getBaseLevelInfo(); - if (getType() == gl::TextureType::_2D && - ((baseLevelInternalFormat.colorEncoding == GL_SRGB && - features.decodeEncodeSRGBForGenerateMipmap.enabled) || - (features.useIntermediateTextureForGenerateMipmap.enabled && - nativegl::SupportsNativeRendering(functions, mState.getType(), - baseLevelInfo.nativeInternalFormat)))) + if (features.useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData.enabled && + functions->copyImageSubData != nullptr && mState.getImmutableFormat() && + getType() == gl::TextureType::_2D && effectiveBaseLevel > 0) + { + ANGLE_TRY(useTempForNonZeroBaseLevelGenmipmap(context)); + } + else if (getType() == gl::TextureType::_2D && + ((baseLevelInternalFormat.colorEncoding == GL_SRGB && + features.decodeEncodeSRGBForGenerateMipmap.enabled) || + (features.useIntermediateTextureForGenerateMipmap.enabled && + nativegl::SupportsNativeRendering(functions, mState.getType(), + baseLevelInfo.nativeInternalFormat)))) { // Manually allocate the mip levels of this texture if they don't exist // This might already be done above if recreateMipmapLevelsBeforeGenerate is in effect. @@ -2310,23 +2316,41 @@ return angle::Result::Continue; } -angle::Result TextureGL::copyTextureLevels(const gl::Context *context, - GLuint srcTexture, - GLuint dstTexture) +angle::Result TextureGL::copyLevelsBetweenTextures(const gl::Context *context, + GLuint sourceTexture, + size_t sourceLevel, + GLuint destTexture, + size_t destLevel, + size_t levelCount) { - const FunctionsGL *functions = GetFunctionsGL(context); - ASSERT(functions->copyImageSubData); + ASSERT(sourceTexture == mTextureID || destTexture == mTextureID); - const GLuint immutableLevels = mState.getImmutableLevels(); - for (GLuint level = 0; level < immutableLevels; ++level) + // Checking mState for the level size only works here because the + // levels are already defined. If this was a mutable texture in a + // glGenerateMipMaps call, the level sizes would not be set until + // after the call finishes. + ASSERT(mState.getImmutableFormat()); + + ContextGL *contextGL = GetImplAs<ContextGL>(context); + const FunctionsGL *functions = GetFunctionsGL(context); + + ANGLE_CHECK(contextGL, functions->copyImageSubData != nullptr, + "glCopyImageSubData is not available.", GL_INVALID_OPERATION); + + for (size_t t = 0; t < levelCount; ++t) { - const gl::Extents levelSize = mState.getImageDesc(gl::TextureTarget::_2D, level).size; - ASSERT(!levelSize.empty()); + const size_t srcLevel = sourceLevel + t; + const size_t dstLevel = destLevel + t; + const size_t levelInState = (sourceTexture == mTextureID ? srcLevel : dstLevel); + const gl::Extents levelSize = + mState.getImageDesc(gl::TextureTarget::_2D, levelInState).size; + 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)); + sourceTexture, GL_TEXTURE_2D, static_cast<GLint>(srcLevel), 0, 0, + 0, destTexture, GL_TEXTURE_2D, static_cast<GLint>(dstLevel), 0, 0, + 0, levelSize.width, levelSize.height, 1)); } + return angle::Result::Continue; } @@ -2353,7 +2377,8 @@ 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)); + ANGLE_TRY(copyLevelsBetweenTextures(context, oldTextureID, 0, mTextureID, 0, + mState.getImmutableLevels())); stateManager->deleteTexture(oldTextureID); @@ -2363,6 +2388,55 @@ return angle::Result::Continue; } +angle::Result TextureGL::useTempForNonZeroBaseLevelGenmipmap(const gl::Context *context) +{ + // Will need to be updated to support other texture types. + ASSERT(getType() == gl::TextureType::_2D); + + const FunctionsGL *functions = GetFunctionsGL(context); + StateManagerGL *stateManager = GetStateManagerGL(context); + const angle::FeaturesGL &features = GetFeaturesGL(context); + + ASSERT(functions->copyImageSubData != nullptr); + + const GLuint effectiveBaseLevel = mState.getEffectiveBaseLevel(); + const GLuint maxLevel = mState.getMipmapMaxLevel(); + const GLuint immutableLevels = mState.getImmutableLevels(); + ASSERT(immutableLevels > effectiveBaseLevel); + ASSERT(maxLevel >= effectiveBaseLevel); + const GLuint tempLevels = immutableLevels - effectiveBaseLevel; + const GLuint tempMax = maxLevel - effectiveBaseLevel; + + const gl::ImageDesc &baseLevelDesc = mState.getBaseLevelDesc(); + const gl::InternalFormat &baseLevelInternalFormat = *baseLevelDesc.format.info; + nativegl::TexStorageFormat texStorageFormat = nativegl::GetTexStorageFormat( + functions, features, baseLevelInternalFormat.sizedInternalFormat); + + GLuint tempTextureID = 0; + functions->genTextures(1, &tempTextureID); + stateManager->bindTexture(gl::TextureType::_2D, tempTextureID); + + ANGLE_GL_TRY_ALWAYS_CHECK( + context, functions->texStorage2D(GL_TEXTURE_2D, static_cast<GLsizei>(tempLevels), + texStorageFormat.internalFormat, baseLevelDesc.size.width, + baseLevelDesc.size.height)); + + functions->texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(tempMax)); + + ANGLE_TRY( + copyLevelsBetweenTextures(context, mTextureID, effectiveBaseLevel, tempTextureID, 0, 1)); + + ANGLE_GL_TRY_ALWAYS_CHECK(context, functions->generateMipmap(GL_TEXTURE_2D)); + + ANGLE_TRY(copyLevelsBetweenTextures(context, tempTextureID, 1, mTextureID, + effectiveBaseLevel + 1, tempMax)); + + stateManager->deleteTexture(tempTextureID); + stateManager->bindTexture(getType(), mTextureID); + + 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 7fe8c9a..33933f3 100644 --- a/src/libANGLE/renderer/gl/TextureGL.h +++ b/src/libANGLE/renderer/gl/TextureGL.h @@ -221,10 +221,15 @@ private: angle::Result recreateTexture(const gl::Context *context); - angle::Result copyTextureLevels(const gl::Context *context, - GLuint srcTexture, - GLuint dstTexture); + // One of sourceTexture or destTexture must be mTextureID. + angle::Result copyLevelsBetweenTextures(const gl::Context *context, + GLuint sourceTexture, + size_t sourceLevel, + GLuint destTexture, + size_t destLevel, + size_t levelCount); angle::Result recreateNativeStoragePreservingLevels(const gl::Context *context); + angle::Result useTempForNonZeroBaseLevelGenmipmap(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 08284c3..63d9131 100644 --- a/src/libANGLE/renderer/gl/renderergl_utils.cpp +++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp @@ -2435,6 +2435,9 @@ ANGLE_FEATURE_CONDITION(features, recreateImmutableTextureOnBaseLevelIncrease, IsPowerVR(vendor)); + ANGLE_FEATURE_CONDITION(features, useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData, + IsPowerVR(vendor)); + ANGLE_FEATURE_CONDITION(features, adjustSrcDstRegionForBlitFramebuffer, IsLinux() || (IsAndroid() && isNvidia) || (IsWindows() && isNvidia) || (IsApple() && functions->standard == STANDARD_GL_ES)); diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index 1080c35..57e276e 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -21,6 +21,7 @@ 42266349 VULKAN : MultithreadingTestES3.UnsynchronizedTextureReads2/* = SKIP 377614665 VULKAN : GLSLValidationTest.StructSamplerVsComma/* = SKIP 410584007 VULKAN : ImageTestES31.UseSourceTextureAsStorageImage/* = SKIP +535696694 VULKAN : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.UploadAfterDrawShouldNotCrash/* = SKIP 514710696 : PbufferTest.BindTexImageAndGenerateMipmap/* = SKIP // Fails anywhere RGBA4 is emulated with RGBA8 534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceZero/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP @@ -346,6 +347,9 @@ 536046166 NVIDIA VULKAN : FramebufferTest_ES31.MultisampleResolveWithBlitIntoMixedTexturesAfterFinish/* = SKIP 536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleResolveWithBlitIntoMixedTextures/* = SKIP 536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP +// NVIDIA's glCopyImageSubData may be broken +534923522 NVIDIA OPENGL : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.VerifyMipmapContents/* = SKIP +534923522 NVIDIA GLES : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.VerifyMipmapContents/* = SKIP 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP 42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP 537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 096dd99..d9dc0a3 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -22989,6 +22989,141 @@ ASSERT_GL_NO_ERROR(); } +class Texture2DTestES3_NonZeroBaseLevelGenMipmaps : public Texture2DTestES3 +{}; + +// Test that performing an upload after draw, and glGenerateMipmap +// with non-zero base level, does not crash. +TEST_P(Texture2DTestES3_NonZeroBaseLevelGenMipmaps, UploadAfterDrawShouldNotCrash) +{ + for (int k = 0; k < 4; ++k) + { + GLTexture tex; + glBindTexture(GL_TEXTURE_2D, tex); + glTexStorage2D(GL_TEXTURE_2D, 12, GL_RGBA8, 3, 2048); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + drawQuad(mProgram, "position", 0.5f); + glFinish(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10); + glGenerateMipmap(GL_TEXTURE_2D); + glFinish(); + EXPECT_GL_NO_ERROR(); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + std::vector<uint8_t> data(1 * 1024 * 4, 0x41); + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 1, 1024, GL_RGBA, GL_UNSIGNED_BYTE, data.data()); + EXPECT_GL_NO_ERROR(); + } +} + +// Test that generating mipmaps with non-zero base level preserves texture contents across all mip +// levels. +TEST_P(Texture2DTestES3_NonZeroBaseLevelGenMipmaps, VerifyMipmapContents) +{ + GLTexture texture; + glBindTexture(GL_TEXTURE_2D, texture); + + // Allocate 6 levels (levels 0 to 5) for a 32x32 immutable RGBA8 texture. + glTexStorage2D(GL_TEXTURE_2D, 6, GL_RGBA8, 32, 32); + + // Set base level to 1 and max level to 4. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Upload different data (e.g. solid blue) to level 0 (32x32). + std::vector<GLColor> level0Data(32 * 32, GLColor::blue); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 32, 32, GL_RGBA, GL_UNSIGNED_BYTE, level0Data.data()); + + // Upload distinct data (solid cyan) to level 5 (1x1) to verify it is untouched. + std::vector<GLColor> level5Data(1 * 1, GLColor::cyan); + glTexSubImage2D(GL_TEXTURE_2D, 5, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, level5Data.data()); + + // Upload a square 2x2 checkerboard pattern to level 1 (16x16). + // Top-left: Red, Top-right: Green, Bottom-left: Yellow, Bottom-right: Magenta. + std::vector<GLColor> level1Data(16 * 16); + for (int y = 0; y < 16; ++y) + { + for (int x = 0; x < 16; ++x) + { + if (x < 8 && y < 8) + { + level1Data[y * 16 + x] = GLColor::red; + } + else if (x >= 8 && y < 8) + { + level1Data[y * 16 + x] = GLColor::green; + } + else if (x < 8 && y >= 8) + { + level1Data[y * 16 + x] = GLColor::yellow; + } + else + { + level1Data[y * 16 + x] = GLColor::magenta; + } + } + } + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, level1Data.data()); + + // Generate mipmaps with base level = 1 and max level = 4. + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GL_NO_ERROR(); + + // Verify that the four colors are preserved correctly in each quadrant up to max level (level + // 4). + GLFramebuffer fbo; + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + + for (int level = 1; level <= 4; ++level) + { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + int dim = 32 >> level; // level 1: 16, level 2: 8, level 3: 4, level 4: 2 + int half = dim / 2; + + // Top-left quadrant (Red) + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + EXPECT_PIXEL_COLOR_EQ(half - 1, half - 1, GLColor::red); + + // Top-right quadrant (Green) + EXPECT_PIXEL_COLOR_EQ(half, 0, GLColor::green); + EXPECT_PIXEL_COLOR_EQ(dim - 1, half - 1, GLColor::green); + + // Bottom-left quadrant (Yellow) + EXPECT_PIXEL_COLOR_EQ(0, half, GLColor::yellow); + EXPECT_PIXEL_COLOR_EQ(half - 1, dim - 1, GLColor::yellow); + + // Bottom-right quadrant (Magenta) + EXPECT_PIXEL_COLOR_EQ(half, half, GLColor::magenta); + EXPECT_PIXEL_COLOR_EQ(dim - 1, dim - 1, GLColor::magenta); + } + + // At the end of the test, verify that level 0's contents and level 5's contents were preserved. + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue); + EXPECT_PIXEL_COLOR_EQ(15, 15, GLColor::blue); + EXPECT_PIXEL_COLOR_EQ(31, 31, GLColor::blue); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 5); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan); + + EXPECT_GL_NO_ERROR(); +} + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DTestES3_NonZeroBaseLevelGenMipmaps); +ANGLE_INSTANTIATE_TEST_ES3_AND( + Texture2DTestES3_NonZeroBaseLevelGenMipmaps, + ES3_OPENGL().enable(Feature::UseTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData), + ES3_OPENGLES().enable(Feature::UseTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData)); ... (file diff truncated)
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 1080c35..57e276e 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -21,6 +21,7 @@
42266349 VULKAN : MultithreadingTestES3.UnsynchronizedTextureReads2/* = SKIP
377614665 VULKAN : GLSLValidationTest.StructSamplerVsComma/* = SKIP
410584007 VULKAN : ImageTestES31.UseSourceTextureAsStorageImage/* = SKIP
+535696694 VULKAN : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.UploadAfterDrawShouldNotCrash/* = SKIP
514710696 : PbufferTest.BindTexImageAndGenerateMipmap/* = SKIP
// Fails anywhere RGBA4 is emulated with RGBA8
534823410 VULKAN : FramebufferTestWithFormatFallback.R4G4B4A4_CubeTexImageRedefinedFaceZero/ES3_Vulkan_SwiftShader_ForceRenderableFallbackFormat = SKIP
@@ -346,6 +347,9 @@
536046166 NVIDIA VULKAN : FramebufferTest_ES31.MultisampleResolveWithBlitIntoMixedTexturesAfterFinish/* = SKIP
536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleResolveWithBlitIntoMixedTextures/* = SKIP
536046166 NVIDIA VULKAN : FramebufferTest_ES31_MSAA.MultisampleStencilSampling/*EmulatedPrerotation* = SKIP
+// NVIDIA's glCopyImageSubData may be broken
+534923522 NVIDIA OPENGL : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.VerifyMipmapContents/* = SKIP
+534923522 NVIDIA GLES : Texture2DTestES3_NonZeroBaseLevelGenMipmaps.VerifyMipmapContents/* = SKIP
42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixCopyBug/* = SKIP
42265709 NVIDIA VULKAN : GLSLTest_ES31.TessellationControlShaderMatrixMultiplicationBug/* = SKIP
537235696 NVIDIA OPENGL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 096dd99..d9dc0a3 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -22989,6 +22989,141 @@
ASSERT_GL_NO_ERROR();
}
+class Texture2DTestES3_NonZeroBaseLevelGenMipmaps : public Texture2DTestES3
+{};
+
+// Test that performing an upload after draw, and glGenerateMipmap
+// with non-zero base level, does not crash.
+TEST_P(Texture2DTestES3_NonZeroBaseLevelGenMipmaps, UploadAfterDrawShouldNotCrash)
+{
+ for (int k = 0; k < 4; ++k)
+ {
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexStorage2D(GL_TEXTURE_2D, 12, GL_RGBA8, 3, 2048);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ drawQuad(mProgram, "position", 0.5f);
+ glFinish();
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10);
+ glGenerateMipmap(GL_TEXTURE_2D);
+ glFinish();
+ EXPECT_GL_NO_ERROR();
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ std::vector<uint8_t> data(1 * 1024 * 4, 0x41);
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 1, 1024, GL_RGBA, GL_UNSIGNED_BYTE, data.data());
+ EXPECT_GL_NO_ERROR();
+ }
+}
+
+// Test that generating mipmaps with non-zero base level preserves texture contents across all mip
+// levels.
+TEST_P(Texture2DTestES3_NonZeroBaseLevelGenMipmaps, VerifyMipmapContents)
+{
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+
+ // Allocate 6 levels (levels 0 to 5) for a 32x32 immutable RGBA8 texture.
+ glTexStorage2D(GL_TEXTURE_2D, 6, GL_RGBA8, 32, 32);
+
+ // Set base level to 1 and max level to 4.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Upload different data (e.g. solid blue) to level 0 (32x32).
+ std::vector<GLColor> level0Data(32 * 32, GLColor::blue);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 32, 32, GL_RGBA, GL_UNSIGNED_BYTE, level0Data.data());
+
+ // Upload distinct data (solid cyan) to level 5 (1x1) to verify it is untouched.
+ std::vector<GLColor> level5Data(1 * 1, GLColor::cyan);
+ glTexSubImage2D(GL_TEXTURE_2D, 5, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, level5Data.data());
+
+ // Upload a square 2x2 checkerboard pattern to level 1 (16x16).
+ // Top-left: Red, Top-right: Green, Bottom-left: Yellow, Bottom-right: Magenta.
+ std::vector<GLColor> level1Data(16 * 16);
+ for (int y = 0; y < 16; ++y)
+ {
+ for (int x = 0; x < 16; ++x)
+ {
+ if (x < 8 && y < 8)
+ {
+ level1Data[y * 16 + x] = GLColor::red;
+ }
+ else if (x >= 8 && y < 8)
+ {
+ level1Data[y * 16 + x] = GLColor::green;
+ }
+ else if (x < 8 && y >= 8)
+ {
+ level1Data[y * 16 + x] = GLColor::yellow;
+ }
+ else
+ {
+ level1Data[y * 16 + x] = GLColor::magenta;
+ }
+ }
+ }
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, level1Data.data());
+
+ // Generate mipmaps with base level = 1 and max level = 4.
+ glGenerateMipmap(GL_TEXTURE_2D);
+ ASSERT_GL_NO_ERROR();
+
+ // Verify that the four colors are preserved correctly in each quadrant up to max level (level
+ // 4).
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+ for (int level = 1; level <= 4; ++level)
+ {
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ int dim = 32 >> level; // level 1: 16, level 2: 8, level 3: 4, level 4: 2
+ int half = dim / 2;
+
+ // Top-left quadrant (Red)
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+ EXPECT_PIXEL_COLOR_EQ(half - 1, half - 1, GLColor::red);
+
+ // Top-right quadrant (Green)
+ EXPECT_PIXEL_COLOR_EQ(half, 0, GLColor::green);
+ EXPECT_PIXEL_COLOR_EQ(dim - 1, half - 1, GLColor::green);
+
+ // Bottom-left quadrant (Yellow)
+ EXPECT_PIXEL_COLOR_EQ(0, half, GLColor::yellow);
+ EXPECT_PIXEL_COLOR_EQ(half - 1, dim - 1, GLColor::yellow);
+
+ // Bottom-right quadrant (Magenta)
+ EXPECT_PIXEL_COLOR_EQ(half, half, GLColor::magenta);
+ EXPECT_PIXEL_COLOR_EQ(dim - 1, dim - 1, GLColor::magenta);
+ }
+
+ // At the end of the test, verify that level 0's contents and level 5's contents were preserved.
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+ EXPECT_PIXEL_COLOR_EQ(15, 15, GLColor::blue);
+ EXPECT_PIXEL_COLOR_EQ(31, 31, GLColor::blue);
+
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 5);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
+
+ EXPECT_GL_NO_ERROR();
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DTestES3_NonZeroBaseLevelGenMipmaps);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+ Texture2DTestES3_NonZeroBaseLevelGenMipmaps,
+ ES3_OPENGL().enable(Feature::UseTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData),
+ ES3_OPENGLES().enable(Feature::UseTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData));
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),
Original Bug Report
IMG: OOB heap write via generateMipmaps, immutable textures and TEXTURE_BASE_LEVEL
- Attack surface: WebGL2 texture state,
generateMipmap, andtexSubImage2D, reachable from an untrusted web page in the default Chrome for Android configuration. - Impact: Out-of-bounds write of attacker-controlled value and length into a device-memory-mapping arena in the unsandboxed Chrome GPU process
Android internal bug: 533545538