High chrome OOB 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in WebGL
DescriptionOut of bounds write in WebGL
ComponentWebGL
Bug ClassOOB
Tracker529932631
Fix commit88229d73b7cf (angle/angle) +134/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • include/platform/autogen/FeaturesGL_autogen.h
  • include/platform/gl_features.json
  • src/libANGLE/renderer/gl/TextureGL.cpp
  • src/libANGLE/renderer/gl/renderergl_utils.cpp
  • src/tests/gl_tests/TextureTest.cpp
From 88229d73b7cf2b464ffd94621928690c08b16a1b Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Tue, 14 Jul 2026 17:44:47 -0700
Subject: [PATCH] GL: reset base level to 0 for ASTC uploads on PowerVR.

Add workaround which resets the base level of the destination texture
while performing TexSubImage2D uploads of ASTC data. Apply this
workaround on Imagination's GLES drivers.

Add a new test which both catches the crash bug on Pixel 10 as well as
verifies rendering from the ASTC-uploaded texture.

Co-authored with Gemini. [agy]

CONV: b6f783ec-ad2a-4e4b-95c8-86d2fe49e5ee
Bug: chromium:529932631
Change-Id: I15753dd2d1e5de85bf923289ae589ccd74c05fb1
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8094449
Auto-Submit: Kenneth Russell <[email protected]>
Commit-Queue: Kenneth Russell <[email protected]>
Reviewed-by: Amirali Abdolrashidi <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
---

diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index f39bce2..9f60d6b 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -254,6 +254,12 @@
         &members,
     };
 
+    FeatureInfo resetBaseLevelForASTCSubImage = {
+        "resetBaseLevelForASTCSubImage",
+        FeatureCategory::OpenGLWorkarounds,
+        &members,
+    };
+
     FeatureInfo limitMax3dArrayTextureSizeTo1024 = {
         "limitMax3dArrayTextureSizeTo1024",
         FeatureCategory::OpenGLWorkarounds,
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index f035663..127cccf 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -309,6 +309,15 @@
             "issue": "https://crbug.com/705865"
         },
         {
+            "name": "reset_base_level_for_ASTC_sub_image",
+            "category": "Workarounds",
+            "description": [
+                "Reset texture base level before calling glCompressedTexSubImage2D for ASTC textures to ",
+                "work around PowerVR driver bug."
+            ],
+            "issue": "https://crbug.com/528131939"
+        },
+        {
             "name": "limit_max_3d_array_texture_size_to_1024",
             "category": "Workarounds",
             "description": [
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index b4fd17d..d0f10ee 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -744,6 +744,13 @@
 
     stateManager->bindTexture(getType(), mTextureID);
     ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
+
+    const bool isASTC = gl::IsASTC2DFormat(format) || gl::IsASTC3DFormat(format);
+    if (features.resetBaseLevelForASTCSubImage.enabled && isASTC)
+    {
+        ANGLE_TRY(setBaseLevel(context, 0));
+    }
+
     if (nativegl::UseTexImage2D(getType()))
     {
         ASSERT(area.z == 0 && area.depth == 1);
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 714dd1a..5cbbd9f 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2491,6 +2491,8 @@
     ANGLE_FEATURE_CONDITION(features, resetTexImage2DBaseLevel,
                             IsApple() && isIntel && GetMacOSVersion() >= OSVersion(10, 12, 4));
 
+    ANGLE_FEATURE_CONDITION(features, resetBaseLevelForASTCSubImage, IsPowerVR(vendor));
+
     ANGLE_FEATURE_CONDITION(features, adjustSrcDstRegionForBlitFramebuffer,
                             IsLinux() || (IsAndroid() && isNvidia) || (IsWindows() && isNvidia) ||
                                 (IsApple() && functions->standard == STANDARD_GL_ES));
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index dcfc647..1614822 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -11453,6 +11453,114 @@
     }
 }
 
+// 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)
+{
+    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()
+{
+    vec2 p = vec2(gl_VertexID & 1, gl_VertexID >> 1);
+    uv = p;
+    gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
+})";
+
+    const char *kFS = R"(#version 300 es
+precision highp float;
+uniform sampler2D t;
+in vec2 uv;
+out vec4 c;
+void main()
+{
+    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();
+
+    // 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};
+
+    // 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));
+
+    // 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));
+    }
+
+    // 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]);
+
+        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).
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 dcfc647..1614822 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -11453,6 +11453,114 @@
     }
 }
 
+// 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)
+{
+    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()
+{
+    vec2 p = vec2(gl_VertexID & 1, gl_VertexID >> 1);
+    uv = p;
+    gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
+})";
+
+    const char *kFS = R"(#version 300 es
+precision highp float;
+uniform sampler2D t;
+in vec2 uv;
+out vec4 c;
+void main()
+{
+    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();
+
+    // 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};
+
+    // 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));
+
+    // 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));
+    }
+
+    // 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]);
+
+        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 the selected decode precision is actually used for texture decoding.
 TEST_P(Texture2DTestES3, ASTCDecodeModeSwitch)
 {
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.