Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in ANGLE
DescriptionBuffer overflow in ANGLE
ComponentANGLE
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.
Tracker551708184
Fix commita5804ada06ac (angle/angle) +153/-90
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedMuhammad Alifa Ramdhan of STAR Labs SG Pte. Ltd.
Reported2026-08-24 (per vendor advisory)
Fix landed2026-09-16 (public git)
Disclosed2026-09-22
Patch gap6 days — fix public in source before the advisory

Background

ASTC compressed textures
a block-compressed GPU texture format uploaded through glCompressedTexImage2D (full image) and glCompressedTexSubImage2D (sub-region).
`TEXTURE_BASE_LEVEL`
the lowest mip level a texture samples from; ANGLE tracks the applied value as mAppliedBaseLevel and can change it with setBaseLevel.
PowerVR base-level driver bug
on PowerVR GPUs, uploading an ASTC image while base_level > 0 makes the driver miscompute the destination offset, writing outside the intended allocation.
ANGLE GL workaround feature
a per-vendor flag (here resetBaseLevelForASTCImage) enabled via ANGLE_FEATURE_CONDITION(..., IsPowerVR(vendor)) that gates a compensating code path.

Root Cause Analysis

ANGLE already carried a workaround, resetBaseLevelForASTCSubImage, that reset TEXTURE_BASE_LEVEL to 0 before glCompressedTexSubImage2D on PowerVR to dodge the driver’s offset miscomputation, but the full-image path glCompressedTexImage2D had no such guard. When an application uploaded an ASTC image with base_level > 0 through the full-image entry point, the PowerVR driver miscomputed the write offset and overflowed the texture allocation — a GPU-process buffer overflow reachable from WebGL.

The fix renames the feature to resetBaseLevelForASTCImage and applies the reset-to-0-then-restore dance in the glCompressedTexImage2D path as well: before the upload, if the feature is enabled and the format is ASTC, setBaseLevel(context, 0) is called, and the original level is restored afterward. Both upload paths now share the same protection, closing the overflow on the full-image route.

Key insight
The workaround for the PowerVR ASTC offset bug was applied only to glCompressedTexSubImage2D, leaving the equivalent glCompressedTexImage2D path unprotected; generalizing the feature to resetBaseLevelForASTCImage and wrapping both uploads with the base-level reset removes the overflow.

Attack Path

  1. PowerVR target the victim runs Chrome on a PowerVR GPU, where resetBaseLevelForASTCImage (and the underlying driver bug) apply.
  2. Enable ASTC in WebGL attacker JavaScript obtains a WebGL context and the ASTC compressed-texture extension.
  3. Raise the base level it sets TEXTURE_BASE_LEVEL > 0 on a texture, then uploads an ASTC image via the full-image path (compressedTexImage2D).
  4. Driver miscomputes offset pre-fix, ANGLE forwarded the call with a non-zero base level, and the PowerVR driver wrote the compressed data outside the allocation.
  5. Heap overflow in the GPU process the out-of-bounds write corrupts adjacent GPU-process memory, a primitive toward escaping the renderer/GPU boundary.

Impact Assessment

An attacker gains an out-of-bounds write in the GPU process from ordinary WebGL calls, corrupting heap memory adjacent to the texture allocation. The preconditions are a PowerVR GPU and use of the ASTC compressed-texture path with a non-zero base level; on other GPUs the feature (and the driver bug) do not trigger, bounding the exposure to PowerVR devices.

Changed Functions

FunctionChangeNotes
ASTCCompressedWithBaseLevelTest
src/tests/gl_tests/TextureTest.cpp
modified

Files Changed

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

Audit Directions

  • Asymmetric image vs sub-image guards
    audit every texture workaround applied to only one of the TexImage/TexSubImage (or 2D/3D) pairs; driver bugs usually affect both entry points.
  • Base-level-sensitive uploads
    flag compressed-texture paths that forward uploads without normalizing TEXTURE_BASE_LEVEL on GPUs known to miscompute offsets.
  • Vendor-gated workarounds
    review ANGLE_FEATURE_CONDITION flags that guard memory-safety fixes to confirm the condition matches every affected driver family and every affected call site.
From a5804ada06ac3756d2d1917912856d4ab110a434 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <[email protected]>
Date: Thu, 27 Aug 2026 13:17:43 -0700
Subject: [PATCH] GL: Reset base level for ASTC compressed image uploads

Rename ResetBaseLevelForASTCSubImage workaround to
ResetBaseLevelForASTCImage, and apply it to glCompressedTexImage2D
in addition to glCompressedTexSubImage2D. For ASTC textures when
base_level > 0, base_level is reset to 0 before calling
glCompressedTexImage2D or glCompressedTexSubImage2D and restored to
its original value afterwards to work around a PowerVR driver bug.

Bug: chromium:551708184
Change-Id: Ia0218a2148ae007a15773fe7bb58b723f5159e9d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8315907
Reviewed-by: Kenneth Russell <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Zhenyao Mo <[email protected]>
---
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index ee4a40b..82506cf 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -309,13 +309,13 @@
             "issue": "https://crbug.com/705865"
         },
         {
-            "name": "reset_base_level_for_ASTC_sub_image",
+            "name": "reset_base_level_for_ASTC_image",
             "category": "Workarounds",
             "description": [
-                "Reset texture base level before calling glCompressedTexSubImage2D for ASTC textures to ",
+                "Reset texture base level before calling glCompressedTexImage2D or glCompressedTexSubImage2D for ASTC textures to ",
                 "work around PowerVR driver bug."
             ],
-            "issue": "https://crbug.com/528131939"
+            "issue": "https://crbug.com/551708184"
         },
         {
             "name": "recreate_immutable_texture_on_base_level_increase",
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index ee23e09..91d435d 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -881,6 +881,13 @@
 
     stateManager->bindTexture(getType(), mTextureID);
     ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
+
+    const bool isASTC = gl::IsASTC2DFormat(internalFormat) || gl::IsASTC3DFormat(internalFormat);
+    if (features.resetBaseLevelForASTCImage.enabled && isASTC)
+    {
+        ANGLE_TRY(setBaseLevel(context, 0));
+    }
+
     if (nativegl::UseTexImage2D(getType()))
     {
         ASSERT(size.depth == 1);
@@ -935,7 +942,7 @@
     ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
 
     const bool isASTC = gl::IsASTC2DFormat(format) || gl::IsASTC3DFormat(format);
-    if (features.resetBaseLevelForASTCSubImage.enabled && isASTC)
+    if (features.resetBaseLevelForASTCImage.enabled && isASTC)
     {
         ANGLE_TRY(setBaseLevel(context, 0));
     }
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 13ff8c9..558a872 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2403,7 +2403,7 @@
     ANGLE_FEATURE_CONDITION(features, resetTexImage2DBaseLevel,
                             IsApple() && isIntel && GetMacOSVersion() >= OSVersion(10, 12, 4));
 
-    ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCSubImage, IsPowerVR(vendor));
+    ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCImage, IsPowerVR(vendor));
     ANGLE_FEATURE_CONDITION(features, recreateImmutableTextureOnBaseLevelIncrease,
                             IsPowerVR(vendor));
     ANGLE_FEATURE_CONDITION(features, resetTexStorage2DBaseLevel, IsPowerVR(vendor));
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 80a00f4..5e7b3cd 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12472,15 +12472,57 @@
     }
 }
 
-// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
-// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
-TEST_P(Texture2DTestES3, ASTCCompressedSubImageWithBaseLevel)
+class ASTCCompressedWithBaseLevelTest : public Texture2DTestES3
 {
-    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+  protected:
+    void uploadASTCLevel(GLenum target,
+                         GLenum format,
+                         GLint level,
+                         GLsizei width,
+                         GLsizei height,
+                         GLsizei blockWidth,
+                         GLsizei blockHeight,
+                         const GLColor &color,
+                         bool useSubImage)
+    {
+        // Void-extent block for ASTC.
+        // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+        // R_lo, R_hi, G_lo, G_hi, B_lo, B_hi, A_lo, A_hi
+        const uint8_t block[16] = {0xFC,    0xFD,    0xFF,    0xFF,    0xFF,    0xFF,
+                                   0xFF,    0xFF,    color.R, color.R, color.G, color.G,
+                                   color.B, color.B, color.A, color.A};
 
-    // Use shaders that match the proof-of-concept.
-    // They don't use vertex attributes, but gl_VertexID to generate a quad.
-    const char *kVS = R"(#version 300 es
+        GLsizei numBlocksX = (width + blockWidth - 1) / blockWidth;
+        GLsizei numBlocksY = (height + blockHeight - 1) / blockHeight;
+        GLsizei numBlocks  = numBlocksX * numBlocksY;
+
+        std::vector<uint8_t> data;
+        data.reserve(numBlocks * 16);
+        for (GLsizei i = 0; i < numBlocks; ++i)
+        {
+            data.insert(data.end(), std::begin(block), std::end(block));
+        }
+
+        if (useSubImage)
+        {
+            glCompressedTexSubImage2D(target, level, 0, 0, width, height, format,
+                                      static_cast<GLsizei>(data.size()), data.data());
+        }
+        else
+        {
+            glCompressedTexImage2D(target, level, format, width, height, 0,
+                                   static_cast<GLsizei>(data.size()), data.data());
+        }
+        ASSERT_GL_NO_ERROR();
+    }
+
+    void runASTCCompressedWithBaseLevelTest(bool useSubImage)
+    {
+        ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+
+        // Use shaders that match the proof-of-concept.
+        // They don't use vertex attributes, but gl_VertexID to generate a quad.
+        const char *kVS = R"(#version 300 es
 out vec2 uv;
 void main()
 {
@@ -12489,7 +12531,7 @@
     gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
 })";
 
-    const char *kFS = R"(#version 300 es
+        const char *kFS = R"(#version 300 es
 precision highp float;
 uniform sampler2D t;
 in vec2 uv;
@@ -12499,85 +12541,96 @@
     c = texture(t, uv);
 })";
 
-    ANGLE_GL_PROGRAM(program, kVS, kFS);
-    glUseProgram(program);
-    GLint texLocation = glGetUniformLocation(program, "t");
-    ASSERT_NE(-1, texLocation);
-    glUniform1i(texLocation, 0);
-    ASSERT_GL_NO_ERROR();
+        ANGLE_GL_PROGRAM(program, kVS, kFS);
+        glUseProgram(program);
+        GLint texLocation = glGetUniformLocation(program, "t");
+        ASSERT_NE(-1, texLocation);
+        glUniform1i(texLocation, 0);
+        ASSERT_GL_NO_ERROR();
 
-    // 8x5 ASTC format.
-    GLenum format             = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
-    constexpr GLsizei kWidth  = 8;
-    constexpr GLsizei kHeight = 160;
-    constexpr GLsizei kLevels = 5;
-    // Void-extent blocks for ASTC.
-    // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, R_lo, R_hi, G_lo, G_hi, B_lo, B_hi,
-    // A_lo, A_hi Red: (255, 0, 0, 255) -> R=0xFFFF, G=0x0000, B=0x0000, A=0xFFFF
-    constexpr uint8_t kBlockRed[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-                                       0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
-    // Green: (0, 255, 0, 255) -> R=0x0000, G=0xFFFF, B=0x0000, A=0xFFFF
-    constexpr uint8_t kBlockGreen[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-                                         0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+        // 8x5 ASTC format.
+        GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
+        // This needs to be big enough, so at smallest level (=4), width >= kBlockWidth.
+        // This is to work around a driver bug: https://crbug.com/562185979.
+        constexpr GLsizei kWidth       = 128;
+        constexpr GLsizei kHeight      = 160;
+        constexpr GLsizei kLevels      = 5;
+        constexpr GLsizei kBlockWidth  = 8;
+        constexpr GLsizei kBlockHeight = 5;
 
-    // Level 4: 1x10 pixels. 8x5 blocks. Padded: 8x10. Blocks: 1x2 = 2.
-    std::vector<uint8_t> dataRed;
-    dataRed.reserve(32);
-    dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
-    dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
+        // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
+        // Keep textures alive to groom the heap similarly to the WebGL PoC.
+        constexpr int kIterations = 16;
+        std::vector<GLTexture> textures(kIterations);
+        for (int i = 0; i < kIterations; ++i)
+        {
+            glBindTexture(GL_TEXTURE_2D, textures[i]);
 
-    // Level 3: 1x20 pixels. 8x5 blocks. Padded: 8x20. Blocks: 1x4 = 4.
-    std::vector<uint8_t> dataGreen;
-    dataGreen.reserve(64);
-    for (int i = 0; i < 4; ++i)
-    {
-        dataGreen.insert(dataGreen.end(), std::begin(kBlockGreen), std::end(kBlockGreen));
+            if (useSubImage)
+            {
+                glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
+                ASSERT_GL_NO_ERROR();
+            }
+            else
+            {
+                // We need to initialize all levels to work around a driver bug.
+                // https://crbug.com/556435507.
+                for (GLint level = 0; level < kLevels; ++level)
+                {
+                    GLsizei levelWidth  = std::max(1, kWidth >> level);
+                    GLsizei levelHeight = std::max(1, kHeight >> level);
+                    uploadASTCLevel(GL_TEXTURE_2D, format, level, levelWidth, levelHeight,
+                                    kBlockWidth, kBlockHeight, GLColor::black,
+                                    /*useSubImage=*/false);
+                }
+            }
+
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
+            ASSERT_GL_NO_ERROR();
+
+            // Upload Red to level 4 (8x10).
+            uploadASTCLevel(GL_TEXTURE_2D, format, 4, 8, 10, kBlockWidth, kBlockHeight,
+                            GLColor::red, useSubImage);
+
+            // Upload Green to level 3 (16x20).
+            uploadASTCLevel(GL_TEXTURE_2D, format, 3, 16, 20, kBlockWidth, kBlockHeight,
+                            GLColor::green, useSubImage);
+
+            // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
+            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+            glFinish();
+            ASSERT_GL_NO_ERROR();
+
+            EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
+
+            // Change BASE_LEVEL to 3.
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
+            ASSERT_GL_NO_ERROR();
+
+            // Draw again. Now it should sample from level 3 (Green).
+            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+            glFinish();
+            ASSERT_GL_NO_ERROR();
+
+            EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
+        }
     }
+};
 
-    // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
-    // Keep textures alive to groom the heap similarly to the WebGL PoC.
-    constexpr int kIterations = 16;
-    std::vector<GLTexture> textures(kIterations);
-    for (int i = 0; i < kIterations; ++i)
-    {
-        glBindTexture(GL_TEXTURE_2D, textures[i]);
+// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, SubImage)
+{
+    runASTCCompressedWithBaseLevelTest(/*useSubImage=*/true);
+}
 
-        glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
-        ASSERT_GL_NO_ERROR();
-
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
-        ASSERT_GL_NO_ERROR();
-
-        // Upload Red to level 4.
-        glCompressedTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 1, 10, format,
-                                  static_cast<GLsizei>(dataRed.size()), dataRed.data());
-        ASSERT_GL_NO_ERROR();
-
-        // Upload Green to level 3.
-        glCompressedTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 1, 20, format,
-                                  static_cast<GLsizei>(dataGreen.size()), dataGreen.data());
-        ASSERT_GL_NO_ERROR();
-
-        // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
-        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
-        glFinish();
-        ASSERT_GL_NO_ERROR();
-
-        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
-
-        // Change BASE_LEVEL to 3.
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
-        ASSERT_GL_NO_ERROR();
-
-        // Draw again. Now it should sample from level 3 (Green).
-        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
-        glFinish();
-        ASSERT_GL_NO_ERROR();
-
-        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
-    }
+// Test that compressed image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, Image)
+{
+    runASTCCompressedWithBaseLevelTest(/*useSubImage=*/false);
 }
 
 // Test that the selected decode precision is actually used for texture decoding.
@@ -25212,6 +25265,9 @@
     ES3_OPENGL().enable(Feature::UseTexSubImageForClientDataNpotUploads),
     ES3_OPENGLES().enable(Feature::UseTexSubImageForClientDataNpotUploads));
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ASTCCompressedWithBaseLevelTest);
+ANGLE_INSTANTIATE_TEST_ES3(ASTCCompressedWithBaseLevelTest);
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
 ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
                        ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index 6a87983..59e2223 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -254,8 +254,8 @@
         &members,
     };
 
-    FeatureInfo resetBaseLevelForASTCSubImage = {
-        "resetBaseLevelForASTCSubImage",
+    FeatureInfo resetBaseLevelForASTCImage = {
+        "resetBaseLevelForASTCImage",
         FeatureCategory::OpenGLWorkarounds,
         &members,
     };
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index 15187b8..d8d24f3 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -315,7 +315,7 @@
     {Feature::RemoveInvariantAndCentroidForESSL3, "removeInvariantAndCentroidForESSL3"},
     {Feature::RequireGpuFamily2, "requireGpuFamily2"},
     {Feature::RescopeGlobalVariables, "rescopeGlobalVariables"},
-    {Feature::ResetBaseLevelForASTCSubImage, "resetBaseLevelForASTCSubImage"},
+    {Feature::ResetBaseLevelForASTCImage, "resetBaseLevelForASTCImage"},
     {Feature::ResetSampleCoverageOnFBOChange, "resetSampleCoverageOnFBOChange"},
     {Feature::ResetTexImage2DBaseLevel, "resetTexImage2DBaseLevel"},
     {Feature::ResetTexStorage2DBaseLevel, "resetTexStorage2DBaseLevel"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 7cd5166..a06aa66 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -315,7 +315,7 @@
     RemoveInvariantAndCentroidForESSL3,
     RequireGpuFamily2,
     RescopeGlobalVariables,
-    ResetBaseLevelForASTCSubImage,
+    ResetBaseLevelForASTCImage,
     ResetSampleCoverageOnFBOChange,
     ResetTexImage2DBaseLevel,
     ResetTexStorage2DBaseLevel,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 80a00f4..5e7b3cd 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -12472,15 +12472,57 @@
     }
 }
 
-// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
-// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
-TEST_P(Texture2DTestES3, ASTCCompressedSubImageWithBaseLevel)
+class ASTCCompressedWithBaseLevelTest : public Texture2DTestES3
 {
-    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+  protected:
+    void uploadASTCLevel(GLenum target,
+                         GLenum format,
+                         GLint level,
+                         GLsizei width,
+                         GLsizei height,
+                         GLsizei blockWidth,
+                         GLsizei blockHeight,
+                         const GLColor &color,
+                         bool useSubImage)
+    {
+        // Void-extent block for ASTC.
+        // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+        // R_lo, R_hi, G_lo, G_hi, B_lo, B_hi, A_lo, A_hi
+        const uint8_t block[16] = {0xFC,    0xFD,    0xFF,    0xFF,    0xFF,    0xFF,
+                                   0xFF,    0xFF,    color.R, color.R, color.G, color.G,
+                                   color.B, color.B, color.A, color.A};
 
-    // Use shaders that match the proof-of-concept.
-    // They don't use vertex attributes, but gl_VertexID to generate a quad.
-    const char *kVS = R"(#version 300 es
+        GLsizei numBlocksX = (width + blockWidth - 1) / blockWidth;
+        GLsizei numBlocksY = (height + blockHeight - 1) / blockHeight;
+        GLsizei numBlocks  = numBlocksX * numBlocksY;
+
+        std::vector<uint8_t> data;
+        data.reserve(numBlocks * 16);
+        for (GLsizei i = 0; i < numBlocks; ++i)
+        {
+            data.insert(data.end(), std::begin(block), std::end(block));
+        }
+
+        if (useSubImage)
+        {
+            glCompressedTexSubImage2D(target, level, 0, 0, width, height, format,
+                                      static_cast<GLsizei>(data.size()), data.data());
+        }
+        else
+        {
+            glCompressedTexImage2D(target, level, format, width, height, 0,
+                                   static_cast<GLsizei>(data.size()), data.data());
+        }
+        ASSERT_GL_NO_ERROR();
+    }
+
+    void runASTCCompressedWithBaseLevelTest(bool useSubImage)
+    {
+        ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr"));
+
+        // Use shaders that match the proof-of-concept.
+        // They don't use vertex attributes, but gl_VertexID to generate a quad.
+        const char *kVS = R"(#version 300 es
 out vec2 uv;
 void main()
 {
@@ -12489,7 +12531,7 @@
     gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
 })";
 
-    const char *kFS = R"(#version 300 es
+        const char *kFS = R"(#version 300 es
 precision highp float;
 uniform sampler2D t;
 in vec2 uv;
@@ -12499,85 +12541,96 @@
     c = texture(t, uv);
 })";
 
-    ANGLE_GL_PROGRAM(program, kVS, kFS);
-    glUseProgram(program);
-    GLint texLocation = glGetUniformLocation(program, "t");
-    ASSERT_NE(-1, texLocation);
-    glUniform1i(texLocation, 0);
-    ASSERT_GL_NO_ERROR();
+        ANGLE_GL_PROGRAM(program, kVS, kFS);
+        glUseProgram(program);
+        GLint texLocation = glGetUniformLocation(program, "t");
+        ASSERT_NE(-1, texLocation);
+        glUniform1i(texLocation, 0);
+        ASSERT_GL_NO_ERROR();
 
-    // 8x5 ASTC format.
-    GLenum format             = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
-    constexpr GLsizei kWidth  = 8;
-    constexpr GLsizei kHeight = 160;
-    constexpr GLsizei kLevels = 5;
-    // Void-extent blocks for ASTC.
-    // Format: 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, R_lo, R_hi, G_lo, G_hi, B_lo, B_hi,
-    // A_lo, A_hi Red: (255, 0, 0, 255) -> R=0xFFFF, G=0x0000, B=0x0000, A=0xFFFF
-    constexpr uint8_t kBlockRed[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-                                       0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
-    // Green: (0, 255, 0, 255) -> R=0x0000, G=0xFFFF, B=0x0000, A=0xFFFF
-    constexpr uint8_t kBlockGreen[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-                                         0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+        // 8x5 ASTC format.
+        GLenum format = GL_COMPRESSED_RGBA_ASTC_8x5_KHR;
+        // This needs to be big enough, so at smallest level (=4), width >= kBlockWidth.
+        // This is to work around a driver bug: https://crbug.com/562185979.
+        constexpr GLsizei kWidth       = 128;
+        constexpr GLsizei kHeight      = 160;
+        constexpr GLsizei kLevels      = 5;
+        constexpr GLsizei kBlockWidth  = 8;
+        constexpr GLsizei kBlockHeight = 5;
 
-    // Level 4: 1x10 pixels. 8x5 blocks. Padded: 8x10. Blocks: 1x2 = 2.
-    std::vector<uint8_t> dataRed;
-    dataRed.reserve(32);
-    dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
-    dataRed.insert(dataRed.end(), std::begin(kBlockRed), std::end(kBlockRed));
+        // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
+        // Keep textures alive to groom the heap similarly to the WebGL PoC.
+        constexpr int kIterations = 16;
+        std::vector<GLTexture> textures(kIterations);
+        for (int i = 0; i < kIterations; ++i)
+        {
+            glBindTexture(GL_TEXTURE_2D, textures[i]);
 
-    // Level 3: 1x20 pixels. 8x5 blocks. Padded: 8x20. Blocks: 1x4 = 4.
-    std::vector<uint8_t> dataGreen;
-    dataGreen.reserve(64);
-    for (int i = 0; i < 4; ++i)
-    {
-        dataGreen.insert(dataGreen.end(), std::begin(kBlockGreen), std::end(kBlockGreen));
+            if (useSubImage)
+            {
+                glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
+                ASSERT_GL_NO_ERROR();
+            }
+            else
+            {
+                // We need to initialize all levels to work around a driver bug.
+                // https://crbug.com/556435507.
+                for (GLint level = 0; level < kLevels; ++level)
+                {
+                    GLsizei levelWidth  = std::max(1, kWidth >> level);
+                    GLsizei levelHeight = std::max(1, kHeight >> level);
+                    uploadASTCLevel(GL_TEXTURE_2D, format, level, levelWidth, levelHeight,
+                                    kBlockWidth, kBlockHeight, GLColor::black,
+                                    /*useSubImage=*/false);
+                }
+            }
+
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
+            ASSERT_GL_NO_ERROR();
+
+            // Upload Red to level 4 (8x10).
+            uploadASTCLevel(GL_TEXTURE_2D, format, 4, 8, 10, kBlockWidth, kBlockHeight,
+                            GLColor::red, useSubImage);
+
+            // Upload Green to level 3 (16x20).
+            uploadASTCLevel(GL_TEXTURE_2D, format, 3, 16, 20, kBlockWidth, kBlockHeight,
+                            GLColor::green, useSubImage);
+
+            // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
+            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+            glFinish();
+            ASSERT_GL_NO_ERROR();
+
+            EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
+
+            // Change BASE_LEVEL to 3.
+            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
+            ASSERT_GL_NO_ERROR();
+
+            // Draw again. Now it should sample from level 3 (Green).
+            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+            glFinish();
+            ASSERT_GL_NO_ERROR();
+
+            EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
+        }
     }
+};
 
-    // Loop multiple times to increase chances of hitting OOB write/crash if workaround fails.
-    // Keep textures alive to groom the heap similarly to the WebGL PoC.
-    constexpr int kIterations = 16;
-    std::vector<GLTexture> textures(kIterations);
-    for (int i = 0; i < kIterations; ++i)
-    {
-        glBindTexture(GL_TEXTURE_2D, textures[i]);
+// Test that compressed sub-image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, SubImage)
+{
+    runASTCCompressedWithBaseLevelTest(/*useSubImage=*/true);
+}
 
-        glTexStorage2D(GL_TEXTURE_2D, kLevels, format, kWidth, kHeight);
-        ASSERT_GL_NO_ERROR();
-
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 4);
-        ASSERT_GL_NO_ERROR();
-
-        // Upload Red to level 4.
-        glCompressedTexSubImage2D(GL_TEXTURE_2D, 4, 0, 0, 1, 10, format,
-                                  static_cast<GLsizei>(dataRed.size()), dataRed.data());
-        ASSERT_GL_NO_ERROR();
-
-        // Upload Green to level 3.
-        glCompressedTexSubImage2D(GL_TEXTURE_2D, 3, 0, 0, 1, 20, format,
-                                  static_cast<GLsizei>(dataGreen.size()), dataGreen.data());
-        ASSERT_GL_NO_ERROR();
-
-        // Draw. Since BASE_LEVEL is 4, it should sample from level 4 (Red).
-        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
-        glFinish();
-        ASSERT_GL_NO_ERROR();
-
-        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(255, 0, 0, 255), 1);
-
-        // Change BASE_LEVEL to 3.
-        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 3);
-        ASSERT_GL_NO_ERROR();
-
-        // Draw again. Now it should sample from level 3 (Green).
-        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
-        glFinish();
-        ASSERT_GL_NO_ERROR();
-
-        EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0, 255, 0, 255), 1);
-    }
+// Test that compressed image updates work when TEXTURE_BASE_LEVEL > 0.
+// This is a workaround for a PowerVR driver bug where it miscomputes the offset.
+TEST_P(ASTCCompressedWithBaseLevelTest, Image)
+{
+    runASTCCompressedWithBaseLevelTest(/*useSubImage=*/false);
 }
 
 // Test that the selected decode precision is actually used for texture decoding.
@@ -25212,6 +25265,9 @@
     ES3_OPENGL().enable(Feature::UseTexSubImageForClientDataNpotUploads),
     ES3_OPENGLES().enable(Feature::UseTexSubImageForClientDataNpotUploads));
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ASTCCompressedWithBaseLevelTest);
+ANGLE_INSTANTIATE_TEST_ES3(ASTCCompressedWithBaseLevelTest);
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TextureSizeLimitTest);
 ANGLE_INSTANTIATE_TEST(TextureSizeLimitTest,
                        ES2_D3D11().enable(Feature::LimitMaxTextureBytesTo1MB),
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.