High watch CVSS 8.8 chrome OOB 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in GPU
DescriptionHeap buffer overflow in GPU
ComponentGPU
Sub-componentGL
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker491732188
Fix commitc7ac96e8c670 (angle/angle) +191/-27
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedinspector-ambitious
Reported2026-03-11 (per vendor advisory)
Fix landed2026-03-22 (public git)
Disclosed2026-03-31
Patch gap9 days — fix public in source before the advisory

Background

ANGLE GL backend (`TextureGL`)
ANGLE’s translation of WebGL/GLES calls onto a native OpenGL driver. It keeps its own shadow of each texture level’s size and format (mState.getImageDesc) and validates WebGL calls against that shadow, not the driver’s state.
`glGenerateMipmap` level range
Generating mipmaps should redefine every level from effectiveBaseLevel + 1 through getMipmapMaxLevel() to the base level’s format with halved dimensions, and must not touch levels outside that range.
Mutable vs immutable textures
Textures defined with glTexImage* (mutable, !mState.getImmutableFormat()) can have each level independently redefined with arbitrary sizes and formats, including compressed formats like GL_COMPRESSED_RED_RGTC1_EXT. Immutable (glTexStorage*) textures cannot.
ANGLE feature workaround
A named flag in gl_features.json/FeaturesGL that is turned on for specific drivers via ANGLE_FEATURE_CONDITION. Here recreateMipmapLevelsBeforeGenerate is enabled when isMesa.

Root Cause Analysis

The new test and the renderergl_utils.cpp comment describe a Mesa driver bug. With mismatched level formats, glGenerateMipmap can silently no-op. ANGLE then believes the levels were recreated in the base format, while the driver keeps the old shape and format (here RGTC1 at level 1). On a later glGenerateMipmap, issued with GL_TEXTURE_BASE_LEVEL = 1, ANGLE’s validation lets the call through because it thinks level 1 is RGBA. Mesa instead processes the call as RGTC1 and, per the test comment, “mistakenly process[es] levels 5 and 6 above (out of mipmap range)”. The test shows a crash, which the advisory classifies as a heap buffer overflow in the GPU process. The broken invariant is that ANGLE’s shadow level state must match the driver’s real level allocations whenever glGenerateMipmap is called.

The fix extracts the existing manual level-allocation loop into TextureGL::allocateMipmapLevelsForGeneration, which is no longer 2D-only and now handles _3D, _2DArray, CubeMapArray and every CubeMap face. When the Mesa workaround is enabled on a mutable texture, it calls this function before any generation. Every level in [effectiveBaseLevel+1, maxLevel] whose size or format differs from the expected value is then explicitly re-specified with texImage2D/texImage3D (with no pixel unpack buffer bound), so the driver’s level state matches ANGLE’s before mipmap generation runs.

Key insight
ANGLE trusted the driver’s glGenerateMipmap to redefine all in-range levels. A silent Mesa no-op left driver-side levels with stale compressed formats or sizes that ANGLE’s validation no longer saw. The fix explicitly recreates every in-range level in the base format before calling the driver on Mesa, so ANGLE and driver state cannot diverge.

Attack Path

  1. Craft mismatched mip levels WebGL2 content defines a mutable TEXTURE_2D with out-of-range levels (5, 6), a compressed RGTC1 image at level 1, then RGBA images at levels 0 and 2, as in MipmapTestES3.MismatchingLevelFormats.
  2. Trigger the silent no-op The first glGenerateMipmap is dropped by Mesa. ANGLE now records level 1 as RGBA while the driver keeps it as RGTC1 16x12.
  3. Pass validation with stale driver state Script re-uploads level 0, sets GL_TEXTURE_BASE_LEVEL to 1, and calls glGenerateMipmap again. ANGLE allows it, but Mesa generates from an RGTC1 base and walks past the mipmap range into levels 5 and 6.
  4. GPU-process memory corruption The driver’s mismatched level processing overflows a heap buffer in the GPU process, according to the advisory and the test’s “crashes” comment. Turning this into controlled corruption or code execution is not shown (inferred).

Impact Assessment

A malicious page with WebGL2 access, on systems where ANGLE’s GL backend runs on Mesa (Linux, and some ChromeOS/Android configurations), can trigger a heap buffer overflow in the GPU process. That process is privileged relative to the renderer, so this could serve as a sandbox-escape stage (inferred). The precondition is a Mesa driver plus support for GL_EXT_texture_compression_rgtc, the extension the test needs. The advisory rates it High (“Heap buffer overflow in GPU”) and does not say it was exploited in the wild.

Files Changed

  • include/platform/autogen/FeaturesGL_autogen.h
  • include/platform/gl_features.json
  • src/libANGLE/renderer/gl/TextureGL.cpp
  • src/libANGLE/renderer/gl/TextureGL.h
  • src/libANGLE/renderer/gl/renderergl_utils.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/MipmapTest.cpp
  • util/autogen/angle_features_autogen.cpp
  • util/autogen/angle_features_autogen.h

Audit Directions

  • Shadow-state vs driver-state divergence
    Look for other ANGLE GL-backend paths that update mState assuming a driver call succeeded (copyTexImage, generateMipmap with useIntermediateTextureForGenerateMipmap, texSubImage with compressed formats, glCopyTexSubImage after redefinition). A silent driver no-op there would leave validation trusting stale level metadata.
  • Mixed compressed/uncompressed levels
    Fuzz mutable textures whose levels mix compressed (RGTC, ETC, ASTC, S3TC) and uncompressed formats, combined with TEXTURE_BASE_LEVEL/MAX_LEVEL changes, across all ANGLE backends (GL, Vulkan, Metal, D3D). Out-of-range levels are a recurring trigger for driver-side miscalculation.
  • Workaround scoping
    The fix is gated on isMesa and !getImmutableFormat(). Check whether other drivers (the test is already skipped on Windows Intel OpenGL and Mac OpenGL/Metal) show similar silent-failure behavior that remains unmitigated.
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index c7bd3fb..41f6567 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -878,6 +878,15 @@
             "issue": "http://crbug.com/475877320"
         },
         {
+            "name": "recreate_mipmap_levels_before_generate",
+            "category": "Workarounds",
+            "description": [
+                "Before calling glGenerateMipmap on a mutable texture, recreate levels that the driver is ",
+                "supposed to recreate to work around driver bugs."
+            ],
+            "issue": "http://crbug.com/491732188"
+        },
+        {
             "name": "limit_max_buffer_size_to_1gb",
             "category": "Workarounds",
             "description": [
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index 9a2c293..05299c9 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -1497,6 +1497,13 @@
     StateManagerGL *stateManager      = GetStateManagerGL(context);
     const angle::FeaturesGL &features = GetFeaturesGL(context);
 
+    bool recreateMipmapLevelsBeforeGenerate =
+        features.recreateMipmapLevelsBeforeGenerate.enabled && !mState.getImmutableFormat();
+    if (recreateMipmapLevelsBeforeGenerate)
+    {
+        ANGLE_TRY(allocateMipmapLevelsForGeneration(context));
+    }
+
     const GLuint effectiveBaseLevel = mState.getEffectiveBaseLevel();
     const GLuint maxLevel           = mState.getMipmapMaxLevel();
 
@@ -1513,34 +1520,19 @@
           nativegl::SupportsNativeRendering(functions, mState.getType(),
                                             baseLevelInfo.nativeInternalFormat))))
     {
-        nativegl::TexImageFormat texImageFormat = nativegl::GetTexImageFormat(
-            functions, features, baseLevelInternalFormat.internalFormat,
-            baseLevelInternalFormat.format, baseLevelInternalFormat.type);
-
         // Manually allocate the mip levels of this texture if they don't exist
-        GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
-        for (GLuint levelIdx = 1; levelIdx < levelCount; levelIdx++)
+        // This might already be done above if recreateMipmapLevelsBeforeGenerate is in effect.
+        if (!recreateMipmapLevelsBeforeGenerate)
         {
-            gl::Extents levelSize(std::max(baseLevelDesc.size.width >> levelIdx, 1),
-                                  std::max(baseLevelDesc.size.height >> levelIdx, 1), 1);
-
-            const gl::ImageDesc &levelDesc =
-                mState.getImageDesc(gl::TextureTarget::_2D, effectiveBaseLevel + levelIdx);
-
-            if (levelDesc.size != levelSize || *levelDesc.format.info != baseLevelInternalFormat)
-            {
-                // Make sure no pixel unpack buffer is bound
-                stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, 0);
-
-                ANGLE_GL_TRY_ALWAYS_CHECK(
-                    context, functions->texImage2D(
-                                 ToGLenum(getType()), effectiveBaseLevel + levelIdx,
-                                 texImageFormat.internalFormat, levelSize.width, levelSize.height,
-                                 0, texImageFormat.format, texImageFormat.type, nullptr));
-            }
+            ANGLE_TRY(allocateMipmapLevelsForGeneration(context));
         }
 
         // Use the blitter to generate the mips
+        const nativegl::TexImageFormat texImageFormat = nativegl::GetTexImageFormat(
+            functions, features, baseLevelInternalFormat.internalFormat,
+            baseLevelInternalFormat.format, baseLevelInternalFormat.type);
+        const GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
+
         BlitGL *blitter = GetBlitGL(context);
         if (baseLevelInternalFormat.colorEncoding == GL_SRGB)
         {
@@ -1565,6 +1557,84 @@
     return angle::Result::Continue;
 }
 
+angle::Result TextureGL::allocateMipmapLevelsForGeneration(const gl::Context *context)
+{
+    const FunctionsGL *functions      = GetFunctionsGL(context);
+    StateManagerGL *stateManager      = GetStateManagerGL(context);
+    const angle::FeaturesGL &features = GetFeaturesGL(context);
+
+    const GLuint effectiveBaseLevel = mState.getEffectiveBaseLevel();
+    const GLuint maxLevel           = mState.getMipmapMaxLevel();
+
+    const gl::ImageDesc &baseLevelDesc                = mState.getBaseLevelDesc();
+    const gl::InternalFormat &baseLevelInternalFormat = *baseLevelDesc.format.info;
+
+    nativegl::TexImageFormat texImageFormat =
+        nativegl::GetTexImageFormat(functions, features, baseLevelInternalFormat.internalFormat,
+                                    baseLevelInternalFormat.format, baseLevelInternalFormat.type);
+
+    const bool is3D                = getType() == gl::TextureType::_3D;
+    const gl::TextureTarget target = getType() == gl::TextureType::CubeMap
+                                         ? gl::TextureTarget::CubeMapPositiveX
+                                         : NonCubeTextureTypeToTarget(getType());
+
+    // Manually allocate the mip levels of this texture if they don't exist
+    GLuint levelCount = maxLevel - effectiveBaseLevel + 1;
+    for (GLuint levelIdx = 1; levelIdx < levelCount; levelIdx++)
+    {
+        gl::Extents levelSize(
+            std::max(baseLevelDesc.size.width >> levelIdx, 1),
+            std::max(baseLevelDesc.size.height >> levelIdx, 1),
+            is3D ? std::max(baseLevelDesc.size.depth >> levelIdx, 1) : baseLevelDesc.size.depth);
+
+        const gl::ImageDesc &levelDesc = mState.getImageDesc(target, effectiveBaseLevel + levelIdx);
+
+        if (levelDesc.size != levelSize || *levelDesc.format.info != baseLevelInternalFormat)
+        {
+            // Make sure no pixel unpack buffer is bound
+            stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, 0);
+
+            switch (getType())
+            {
+                case gl::TextureType::_2D:
+                    ANGLE_GL_TRY_ALWAYS_CHECK(
+                        context,
+                        functions->texImage2D(ToGLenum(getType()), effectiveBaseLevel + levelIdx,
+                                              texImageFormat.internalFormat, levelSize.width,
+                                              levelSize.height, 0, texImageFormat.format,
+                                              texImageFormat.type, nullptr));
+                    break;
+                case gl::TextureType::_3D:
+                case gl::TextureType::_2DArray:
+                case gl::TextureType::CubeMapArray:
+                    ANGLE_GL_TRY_ALWAYS_CHECK(
+                        context,
+                        functions->texImage3D(ToGLenum(getType()), effectiveBaseLevel + levelIdx,
+                                              texImageFormat.internalFormat, levelSize.width,
+                                              levelSize.height, levelSize.depth, 0,
+                                              texImageFormat.format, texImageFormat.type, nullptr));
+                    break;
+                case gl::TextureType::CubeMap:
+                    for (gl::TextureTarget face : gl::AllCubeFaceTextureTargets())
+                    {
+                        ANGLE_GL_TRY_ALWAYS_CHECK(
+                            context,
+                            functions->texImage2D(ToGLenum(face), effectiveBaseLevel + levelIdx,
+                                                  texImageFormat.internalFormat, levelSize.width,
+                                                  levelSize.height, 0, texImageFormat.format,
+                                                  texImageFormat.type, nullptr));
+                    }
+                    break;
+                default:
+                    // Cannot call glGenerateMipmap with any other texture type
+                    UNREACHABLE();
+                    break;
+            }
+        }
+    }
+    return angle::Result::Continue;
+}
+
 angle::Result TextureGL::clearImage(const gl::Context *context,
                                     GLint level,
                                     GLenum format,
diff --git a/src/libANGLE/renderer/gl/TextureGL.h b/src/libANGLE/renderer/gl/TextureGL.h
index 95af317..03f4711 100644
--- a/src/libANGLE/renderer/gl/TextureGL.h
+++ b/src/libANGLE/renderer/gl/TextureGL.h
@@ -267,6 +267,7 @@
                                                const gl::PixelUnpackState &unpack,
                                                const gl::Buffer *unpackBuffer,
                                                const uint8_t *pixels);
+    angle::Result allocateMipmapLevelsForGeneration(const gl::Context *context);
 
     angle::Result syncTextureStateSwizzle(const gl::Context *context,
                                           const FunctionsGL *functions,
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index a4cd6db..f9334e8 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2690,6 +2690,10 @@
     // exposed.
     ANGLE_FEATURE_CONDITION(features, bgraTexImageFormatsBroken, !isMesa && isQualcomm);
 
+    // glGenerateMipmap may silently fail on mesa, leaving mips that are expected to be recreated to
+    // match the base level in their original shape, hidden from ANGLE and its validation.
+    ANGLE_FEATURE_CONDITION(features, recreateMipmapLevelsBeforeGenerate, isMesa);
+
     // https://github.com/flutter/flutter/issues/47164
     // https://github.com/flutter/flutter/issues/47804
     // Some devices expose the QCOM tiled memory extension string but don't actually provide the
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index aa9ac1f..71c19db 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -49,7 +49,6 @@
 493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchInHelper/* = SKIP
 493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchWithSamplerInStruct/* = SKIP
 
-
 // Advanced blend ops currently fail on Windows/Intel.
 376899587 WIN INTEL VULKAN : AdvancedBlendTest.*/* = SKIP
 
@@ -171,6 +170,7 @@
 42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockArray/* = SKIP
 42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockNestedArray/* = SKIP
 463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
+494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 
 // Failures on Intel Alder Lake S UHD Graphics 770 (8086:4680-31.0.101.5333)
 352085732 WIN INTEL OPENGL : GeometryShaderTest.LayeredFramebufferClear2DArrayColor/* = SKIP
@@ -473,6 +473,8 @@
 464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackSnorm2x16/* = SKIP
 464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackUnorm2x16/* = SKIP
 42266214 MAC APPLE METAL : Texture2DTest.ManySupersedingTextureUpdates/* = SKIP
+494392011 MAC METAL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
+494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index ce2a53d..cdcc7e2 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2470,12 +2470,76 @@
     }
 }
 
+// Test glGenerateMipmap in the presence of mismatching level formats.  Regression test for a bug in
+// mesa.
+TEST_P(MipmapTestES3, MismatchingLevelFormats)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_rgtc"));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    const std::vector<GLColor> kAllGreen(1000, GLColor::green);
+    const std::vector<GLColor> kAllBlue(1000, GLColor::blue);
+    const std::vector<GLColor> kAllCyan(1000, GLColor::cyan);
+
+    // Create mips at levels that should not be touched by glGenerateMipmap.
+    glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllCyan.data());
+    glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 20, 30, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+
+    // Create RGTC1 at level 1.  A 16x12 image has 4x3 blocks of 8 bytes each.
+    const std::vector<uint8_t> redRGTC1((16 / 4) * (12 / 4) * 8, 0xFF);
+    glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED_RGTC1_EXT, 16, 12, 0,
+                           static_cast<GLsizei>(redRGTC1.size()), redRGTC1.data());
+
+    // Trigger mesa bug: After uploading a 4x4 image at level 0 and 1x2 at level 2, calling
+    // glGenerateMipmap silently gets nooped.  While ANGLE considers mip 1 now to be in RGBA format,
+    // mesa still thinks it's RGTC1 due to the noop.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllGreen.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    // Now dirty mesa's tracking again by reuploading to level 0, then call glGenerateMipmap on
+    // level 1, which ANGLE lets through because that mip is supposed to be RGBA now.  Mesa supports
+    // glGenerateMipmap on RGTC1 textures.  Where the bug is present, this glGenerateMipmap crashes
+    // by mistakenly processing levels 5 and 6 above (out of mipmap range).
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    // Verify correctness
+    ANGLE_GL_PROGRAM(verify, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+    glUseProgram(verify);
+    const GLint lodLoc = glGetUniformLocation(verify, essl3_shaders::LodUniform());
+
+    glUniform1i(glGetUniformLocation(verify, essl3_shaders::Texture2DUniform()), 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+    glUniform1f(lodLoc, 0);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+    glUniform1f(lodLoc, 1);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    glUniform1f(lodLoc, 2);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // Use this to select which configurations (e.g. which renderer, which GLES major version) these
 // tests should be run against.
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     MipmapTest,
     ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass),
-    ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap));
+    ES2_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+    ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap),
+    ES2_OPENGLES()
+        .enable(Feature::UseIntermediateTextureForGenerateMipmap)
+        .enable(Feature::UseIntermediateTextureForGenerateMipmap));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Mipmap3DBoxFilterTest);
 ANGLE_INSTANTIATE_TEST(Mipmap3DBoxFilterTest,
@@ -2483,7 +2547,13 @@
                        ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES3);
-ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3, ES3_WEBGPU());
+ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3,
+                               ES3_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+                               ES3_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+                               ES3_WEBGPU());
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES31);
-ANGLE_INSTANTIATE_TEST_ES31(MipmapTestES31);
+ANGLE_INSTANTIATE_TEST_ES31_AND(
+    MipmapTestES31,
+    ES31_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+    ES31_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate));
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index d04f3d5..bac58e9 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -674,6 +674,12 @@
         &members,
     };
 
+    FeatureInfo recreateMipmapLevelsBeforeGenerate = {
+        "recreateMipmapLevelsBeforeGenerate",
+        FeatureCategory::OpenGLWorkarounds,
+        &members,
+    };
+
     FeatureInfo limitMaxBufferSizeTo1gb = {
         "limitMaxBufferSizeTo1gb",
         FeatureCategory::OpenGLWorkarounds,
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index 7028061..5485ec3 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -283,6 +283,7 @@
     {Feature::QueryCounterBitsGeneratesErrors, "queryCounterBitsGeneratesErrors"},
     {Feature::ReadPixelsUsingImplementationColorReadFormatForNorm16, "readPixelsUsingImplementationColorReadFormatForNorm16"},
     {Feature::ReapplyUBOBindingsAfterUsingBinaryProgram, "reapplyUBOBindingsAfterUsingBinaryProgram"},
+    {Feature::RecreateMipmapLevelsBeforeGenerate, "recreateMipmapLevelsBeforeGenerate"},
     {Feature::RecycleVkEvent, "recycleVkEvent"},
     {Feature::RegenerateStructNames, "regenerateStructNames"},
     {Feature::RemoveDynamicIndexingOfSwizzledVector, "removeDynamicIndexingOfSwizzledVector"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 89ac914..fdc51c8 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -283,6 +283,7 @@
     QueryCounterBitsGeneratesErrors,
     ReadPixelsUsingImplementationColorReadFormatForNorm16,
     ReapplyUBOBindingsAfterUsingBinaryProgram,
+    RecreateMipmapLevelsBeforeGenerate,
     RecycleVkEvent,
     RegenerateStructNames,
     RemoveDynamicIndexingOfSwizzledVector,
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 aa9ac1f..71c19db 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -49,7 +49,6 @@
 493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchInHelper/* = SKIP
 493505759 PIXEL10 GLES : SRGBTextureTestES3.SRGBDecodeTexelFetchWithSamplerInStruct/* = SKIP
 
-
 // Advanced blend ops currently fail on Windows/Intel.
 376899587 WIN INTEL VULKAN : AdvancedBlendTest.*/* = SKIP
 
@@ -171,6 +170,7 @@
 42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockArray/* = SKIP
 42266866 WIN INTEL OPENGL : GLSLTest_ES3.LargeInterfaceBlockNestedArray/* = SKIP
 463961767 WIN INTEL OPENGL : EGLSurfacelessContextTest.Switcheroo/* = SKIP
+494350632 WIN INTEL OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 
 // Failures on Intel Alder Lake S UHD Graphics 770 (8086:4680-31.0.101.5333)
 352085732 WIN INTEL OPENGL : GeometryShaderTest.LayeredFramebufferClear2DArrayColor/* = SKIP
@@ -473,6 +473,8 @@
 464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackSnorm2x16/* = SKIP
 464927523 METAL : GLSLTest_ES3_PackUnpackEmulation.PackUnorm2x16/* = SKIP
 42266214 MAC APPLE METAL : Texture2DTest.ManySupersedingTextureUpdates/* = SKIP
+494392011 MAC METAL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
+494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index ce2a53d..cdcc7e2 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2470,12 +2470,76 @@
     }
 }
 
+// Test glGenerateMipmap in the presence of mismatching level formats.  Regression test for a bug in
+// mesa.
+TEST_P(MipmapTestES3, MismatchingLevelFormats)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_texture_compression_rgtc"));
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    const std::vector<GLColor> kAllGreen(1000, GLColor::green);
+    const std::vector<GLColor> kAllBlue(1000, GLColor::blue);
+    const std::vector<GLColor> kAllCyan(1000, GLColor::cyan);
+
+    // Create mips at levels that should not be touched by glGenerateMipmap.
+    glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllCyan.data());
+    glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 20, 30, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+
+    // Create RGTC1 at level 1.  A 16x12 image has 4x3 blocks of 8 bytes each.
+    const std::vector<uint8_t> redRGTC1((16 / 4) * (12 / 4) * 8, 0xFF);
+    glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RED_RGTC1_EXT, 16, 12, 0,
+                           static_cast<GLsizei>(redRGTC1.size()), redRGTC1.data());
+
+    // Trigger mesa bug: After uploading a 4x4 image at level 0 and 1x2 at level 2, calling
+    // glGenerateMipmap silently gets nooped.  While ANGLE considers mip 1 now to be in RGBA format,
+    // mesa still thinks it's RGTC1 due to the noop.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllGreen.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    // Now dirty mesa's tracking again by reuploading to level 0, then call glGenerateMipmap on
+    // level 1, which ANGLE lets through because that mip is supposed to be RGBA now.  Mesa supports
+    // glGenerateMipmap on RGTC1 textures.  Where the bug is present, this glGenerateMipmap crashes
+    // by mistakenly processing levels 5 and 6 above (out of mipmap range).
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, kAllBlue.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    // Verify correctness
+    ANGLE_GL_PROGRAM(verify, essl3_shaders::vs::Texture2DLod(), essl3_shaders::fs::Texture2DLod());
+    glUseProgram(verify);
+    const GLint lodLoc = glGetUniformLocation(verify, essl3_shaders::LodUniform());
+
+    glUniform1i(glGetUniformLocation(verify, essl3_shaders::Texture2DUniform()), 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+    glUniform1f(lodLoc, 0);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+    glUniform1f(lodLoc, 1);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    glUniform1f(lodLoc, 2);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // Use this to select which configurations (e.g. which renderer, which GLES major version) these
 // tests should be run against.
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
     MipmapTest,
     ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass),
-    ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap));
+    ES2_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+    ES2_OPENGLES().enable(Feature::UseIntermediateTextureForGenerateMipmap),
+    ES2_OPENGLES()
+        .enable(Feature::UseIntermediateTextureForGenerateMipmap)
+        .enable(Feature::UseIntermediateTextureForGenerateMipmap));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Mipmap3DBoxFilterTest);
 ANGLE_INSTANTIATE_TEST(Mipmap3DBoxFilterTest,
@@ -2483,7 +2547,13 @@
                        ES2_METAL().disable(Feature::AllowGenMultipleMipsPerPass));
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES3);
-ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3, ES3_WEBGPU());
+ANGLE_INSTANTIATE_TEST_ES3_AND(MipmapTestES3,
+                               ES3_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+                               ES3_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+                               ES3_WEBGPU());
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapTestES31);
-ANGLE_INSTANTIATE_TEST_ES31(MipmapTestES31);
+ANGLE_INSTANTIATE_TEST_ES31_AND(
+    MipmapTestES31,
+    ES31_OPENGL().enable(Feature::RecreateMipmapLevelsBeforeGenerate),
+    ES31_OPENGLES().enable(Feature::RecreateMipmapLevelsBeforeGenerate));
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.