Chrome · WebGL
CVE-2026-76034
OOB in WebGL
Overview
Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/TextureGL.cpp
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/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h index 6c4f57d..42c3e4a 100644 --- a/include/platform/autogen/FeaturesGL_autogen.h +++ b/include/platform/autogen/FeaturesGL_autogen.h @@ -266,6 +266,12 @@ &members, }; + FeatureInfo useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData = { + "useTempForNonZeroBaseLevelGenMipmapUsingCopyImageSubData", + FeatureCategory::OpenGLWorkarounds, + &members, + }; + FeatureInfo limitMax3dArrayTextureSizeTo1024 = { "limitMax3dArrayTextureSizeTo1024", FeatureCategory::OpenGLWorkarounds, 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)); +
Loading diff…
Regression Test / PoC
shipped with the fix
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),
Loading diff…
Original Bug Report
reported by [email protected]
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
References
On This Page