Critical chrome OOB 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in WebGL
DescriptionOut of bounds write in WebGL
ComponentWebGL
Bug ClassOOB
Tracker548127218
Fix commit107da744f62a (angle/angle) +439/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Background

ANGLE
Chrome’s translation layer that implements OpenGL ES / WebGL on top of native graphics APIs, here targeting a native desktop/mobile GL backend.
Mip level
one image in a texture’s mipmap chain, whose dimensions are normally constrained by level 0 per the OpenGL ES rules (ceilPow2(level0) >> level).
Oversized level
a non-zero mip level whose supplied width/height exceed the size implied by level 0, violating the ES mipmap sizing rules.
PBO (pixel unpack buffer)
a GPU-side buffer bound to GL_PIXEL_UNPACK_BUFFER from which the driver reads pixel data, routing an upload through a driver path distinct from immediate client-memory upload.

Root Cause Analysis

The vulnerable path is TextureGL::setImageHelper in TextureGL.cpp, which forwarded a client texImage2D/texSubImage2D for a non-zero mip level straight into the driver’s immediate software upload. When level 0 of a _2D texture is already defined and the application supplies an “oversized” non-zero level — one whose size.width/size.height exceed the slot dimensions max(1, ceilPow2(level0.size.width) >> level) / max(1, ceilPow2(level0.size.height) >> level) — certain drivers (PowerVR) size their destination allocation from level 0’s implied dimensions but copy using the larger client-supplied dimensions. The violated invariant is that a mip level’s dimensions must not exceed what level 0 dictates, so the driver’s write ran past the allocated backing store, producing an out-of-bounds write.

The fix detects this oversized-level condition in setImageHelper (gated on the new uploadOversizedMipLevelsViaUnpackBuffer feature, _2D type, level > 0, no bound unpackBuffer, and no depth/stencil format) and reroutes the upload to setImageViaScratchUnpackBuffer. That helper allocates a scratch GL_PIXEL_UNPACK_BUFFER of the exact expected uploadBytes, binds it, and issues the texImage2D with a nullptr data pointer so the driver reads from the PBO through a path that handles the sizing safely.

Key insight
The single mistake is trusting client-supplied, spec-violating dimensions for a non-zero mip level on the immediate upload path, where the driver allocates from level 0 but writes using the larger dimensions. The fix stages such oversized uploads through a scratch pixel unpack buffer, whose driver path bounds the write correctly instead of overrunning the allocation.

Attack Path

  1. Define level 0 A malicious WebGL page creates a TEXTURE_2D and defines mip level 0 with modest dimensions, establishing the texture’s expected storage footprint.
  2. Supply an oversized non-zero level The page calls texImage2D for level > 0 with width/height larger than the ceilPow2(level0) >> level slot, violating the ES mipmap rules.
  3. Reach the immediate upload With no unpack buffer bound and a color (non depth/stencil) format, setImageHelper forwarded the call directly to the driver’s software upload.
  4. Trigger the driver overrun On an affected (PowerVR) driver, the destination is sized from level 0 but written using the oversized dimensions, causing an out-of-bounds write in GPU-process memory.

Impact Assessment

An attacker who can run WebGL content gains an out-of-bounds write inside the GPU process (via ANGLE’s GL backend on affected drivers), corrupting memory beyond a texture’s allocation. The only precondition is the ability to issue WebGL texture-upload calls plus an affected driver stack; no special privileges are required. Such GPU-process memory corruption is a strong primitive toward sandbox escape or further exploitation.

Files Changed

  • include/platform/autogen/FeaturesGL_autogen.h
  • include/platform/gl_features.json
  • src/libANGLE/renderer/gl/TextureGL.cpp

Audit Directions

  • Client-supplied texture dimensions
    Audit every backend upload path where per-level width/height/imageSize from the application flow into a native texImage/compressedTexImage call, and confirm they are validated against the storage implied by level 0 rather than trusted.
  • Immediate vs. PBO upload divergence
    Review other cases distinguishing immediate client-memory uploads from pixel-unpack-buffer uploads, since driver bugs are frequently isolated to only one path and workarounds may be missing for _3D, cube-map, or compressed variants.
  • Driver-specific workaround gating
    Check that feature flags like uploadOversizedMipLevelsViaUnpackBuffer cover all drivers exhibiting the sizing bug and that excluded conditions (depth/stencil formats, bound unpackBuffer, level == 0) do not leave an equivalent overrun reachable.
From 107da744f62a319b3c6851694740d9ebf247d048 Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Thu, 27 Aug 2026 16:34:18 -0700
Subject: [PATCH] GL: upload oversized mip levels through PBO.

Add a workaround for the following scenario: if uploading data to a
texture's mipmap level which is "oversized" compared to level 0 - in
other words, larger than the OpenGL ES specification's mipmap rules -
then upload the data via a pixel unpack buffer. Apply this workaround
to PowerVR drivers.

Due to another driver bug, the new workaround can not completely solve
the issue for compressed textures; modify the new tests to accommodate
this fact. It is believed this will not affect applications in the
field.

Bug: chromium:548127218
Change-Id: If4e2e8fceb6a77897b50fa2c7c932f0e2c5c5ce1
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8305515
Auto-Submit: Kenneth Russell <[email protected]>
Reviewed-by: Zhenyao Mo <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Geoff Lang <[email protected]>
---

diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index 839b476..a42c2950 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -776,6 +776,12 @@
         &members,
     };
 
+    FeatureInfo uploadOversizedMipLevelsViaUnpackBuffer = {
+        "uploadOversizedMipLevelsViaUnpackBuffer",
+        FeatureCategory::OpenGLWorkarounds,
+        &members,
+    };
+
     FeatureInfo validateState = {
         "validateState",
         FeatureCategory::OpenGLFeatures,
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 9bac71d..276028d 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -1021,6 +1021,15 @@
             "issue": "http://crbug.com/499602793"
         },
         {
+            "name": "upload_oversized_mip_levels_via_unpack_buffer",
+            "category": "Workarounds",
+            "description": [
+                "Stage oversized nonzero-level texture definitions through a scratch pixel unpack buffer ",
+                "to work around driver bugs."
+            ],
+            "issue": "http://crbug.com/548127218"
+        },
+        {
             "name": "validate_state",
             "category": "Features",
             "description": [
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index 25f2644..3b3a0f9 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -226,6 +226,30 @@
     gl::TextureTarget target = index.getTarget();
     size_t level             = static_cast<size_t>(index.getLevelIndex());
 
+    // Oversized nonzero-level definitions may cause driver issues during immediate software
+    // texture upload when level 0 is already defined. Stage them through a scratch unpack buffer
+    // so the driver handles the upload safely.
+    if (features.uploadOversizedMipLevelsViaUnpackBuffer.enabled &&
+        getType() == gl::TextureType::_2D && level > 0 && unpackBuffer == nullptr &&
+        gl::GetInternalFormatInfo(internalFormat, type).depthBits == 0 &&
+        gl::GetInternalFormatInfo(internalFormat, type).stencilBits == 0)
+    {
+        const gl::ImageDesc &level0 = mState.getImageDesc(gl::TextureTarget::_2D, 0);
+        if (level0.size.width != 0 && level0.size.height != 0)
+        {
+            const int slotW =
+                std::max(1, static_cast<int>(gl::ceilPow2(level0.size.width)) >> level);
+            const int slotH =
+                std::max(1, static_cast<int>(gl::ceilPow2(level0.size.height)) >> level);
+            if (size.width > slotW || size.height > slotH)
+            {
+                return setImageViaScratchUnpackBuffer(context, target, level, internalFormat, size,
+                                                      format, type, unpack, /*isCompressed=*/false,
+                                                      /*imageSize=*/0, pixels);
+            }
+        }
+    }
+
     if (features.unpackOverlappingRowsSeparatelyUnpackBuffer.enabled && unpackBuffer &&
         unpack.rowLength != 0 && unpack.rowLength < size.width)
     {
@@ -371,6 +395,102 @@
     return angle::Result::Continue;
 }
 
+angle::Result TextureGL::setImageViaScratchUnpackBuffer(const gl::Context *context,
+                                                        gl::TextureTarget target,
+                                                        size_t level,
+                                                        GLenum internalFormat,
+                                                        const gl::Extents &size,
+                                                        GLenum format,
+                                                        GLenum type,
+                                                        const gl::PixelUnpackState &unpack,
+                                                        bool isCompressed,
+                                                        size_t imageSize,
+                                                        const uint8_t *pixels)
+{
+    ContextGL *contextGL              = GetImplAs<ContextGL>(context);
+    const FunctionsGL *functions      = GetFunctionsGL(context);
+    StateManagerGL *stateManager      = GetStateManagerGL(context);
+    const angle::FeaturesGL &features = GetFeaturesGL(context);
+
+    if (features.reattachFboDepthStencilOnReallocation.enabled)
+    {
+        onStateChange(angle::SubjectMessage::ObjectReallocated);
+    }
+
+    GLuint uploadBytes = 0;
+    if (isCompressed)
+    {
+        uploadBytes = static_cast<GLuint>(imageSize);
+    }
+    else
+    {
+        ANGLE_CHECK_GL_MATH(contextGL, gl::GetInternalFormatInfo(format, type)
+                                           .computePackUnpackEndByte(type, size, unpack,
+                                                                     /*is3D=*/false, &uploadBytes));
+    }
+
+    GLuint scratch = 0;
+    functions->genBuffers(1, &scratch);
+    stateManager->bindBuffer(gl::BufferBinding::PixelUnpack, scratch);
+    // Regardless of whether the user supplied data (pixels != nullptr), the pixel unpack buffer
+    // must be allocated with the expected amount of data.
+    if (uploadBytes > 0)
+    {
+        ANGLE_GL_TRY(context, functions->bufferData(GL_PIXEL_UNPACK_BUFFER, uploadBytes, pixels,
+                                                    GL_STREAM_DRAW));
+    }
+
+    ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
+
+    stateManager->bindTexture(getType(), mTextureID);
+
+    if (features.resetTexImage2DBaseLevel.enabled)
+    {
+        (void)setBaseLevel(context, 0);
+    }
+
+    if (isCompressed)
+    {
+        const gl::InternalFormat &originalInternalFormatInfo =
+            gl::GetSizedInternalFormatInfo(internalFormat);
+        nativegl::CompressedTexImageFormat compressedTexImageFormat =
+            nativegl::GetCompressedTexImageFormat(functions, features, internalFormat);
+
+        ANGLE_GL_TRY_ALWAYS_CHECK(
+            context, functions->compressedTexImage2D(
+                         nativegl::GetTextureBindingTarget(target), static_cast<GLint>(level),
+                         compressedTexImageFormat.internalFormat, size.width, size.height, 0,
+                         static_cast<GLsizei>(uploadBytes), nullptr));
+
+        LevelInfoGL levelInfo = GetLevelInfo(features, originalInternalFormatInfo,
+                                             compressedTexImageFormat.internalFormat);
+        ASSERT(!levelInfo.lumaWorkaround.enabled);
+        setLevelInfo(context, target, level, 1, levelInfo);
+    }
+    else
+    {
+        const gl::InternalFormat &originalInternalFormatInfo =
+            gl::GetInternalFormatInfo(internalFormat, type);
+        nativegl::TexImageFormat texImageFormat =
+            nativegl::GetTexImageFormat(functions, features, internalFormat, format, type);
+
+        ANGLE_GL_TRY_ALWAYS_CHECK(
+            context, functions->texImage2D(nativegl::GetTextureBindingTarget(target),
+                                           static_cast<GLint>(level), texImageFormat.internalFormat,
+                                           size.width, size.height, 0, texImageFormat.format,
+                                           texImageFormat.type, nullptr));
+
+        LevelInfoGL levelInfo =
+            GetLevelInfo(features, originalInternalFormatInfo, texImageFormat.internalFormat);
+        setLevelInfo(context, target, level, 1, levelInfo);
+    }
+
+    stateManager->deleteBuffer(scratch);
+
+    contextGL->markWorkSubmitted();
+    return angle::Result::Continue;
+}
+
 angle::Result TextureGL::reserveTexImageToBeFilled(const gl::Context *context,
                                                    gl::TextureTarget target,
                                                    size_t level,
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 cc11e10..e5c12f8 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -2916,6 +2916,12 @@
 549916890 WIN INTEL OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeStagedVerifyByDraw/* = SKIP
 549916890 WIN INTEL OPENGL : Texture3DIncreaseDepthTestES3.TexImage3DDepthIncreaseOutsideRangeStagedVerifyByReadPixels/* = SKIP
 
+// New tests failing on Windows Intel OpenGL
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.Uncompressed/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedASTC/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedDXT/* = SKIP
+548127218 WIN INTEL OPENGL : Texture2DTestES3_OversizedMipLevels.CompressedETC/* = SKIP
+
 // Various crashes when using Vulkan on Win/ARM64.
 545676921 WIN QUALCOMM VULKAN : GLSLTest_ES31.MixedRowAndColumnMajorMatrices/* = SKIP
 545676921 WIN QUALCOMM VULKAN : MSRTTES3Test.CubeMap/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index a7c77fd..51b6704 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -23810,6 +23810,256 @@
     }
 }
 
+class Texture2DTestES3_OversizedMipLevels : public Texture2DTestES3
+{
+  protected:
+    Texture2DTestES3_OversizedMipLevels() : Texture2DTestES3() {}
+
+    void testSetUp() override
+    {
+        Texture2DTestES3::testSetUp();
+        setUpProgram();
+        glUseProgram(mProgram);
+        glUniform1i(mTexture2DUniformLocation, 0);
+        glActiveTexture(GL_TEXTURE0);
+    }
+};
+
+// Test that defining an oversized nonzero mip level on an uncompressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, Uncompressed)
+{
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // Upload Level 0: 64x64 solid red.
+    constexpr int kLevel0Width  = 64;
+    constexpr int kLevel0Height = 64;
+    std::vector<GLColor> redData(kLevel0Width * kLevel0Height, GLColor::red);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Width, kLevel0Height, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, redData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Upload Level 1: 128x128 solid green (oversized relative to Level 0).
+    constexpr int kLevel1Width  = 128;
+    constexpr int kLevel1Height = 128;
+    std::vector<GLColor> greenData(kLevel1Width * kLevel1Height, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Width, kLevel1Height, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, greenData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Verify Level 0 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    drawQuad(mProgram, "position", 0.5f);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+    // Verify Level 1 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    drawQuad(mProgram, "position", 0.5f);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::green);
+}
+
+// Test that defining an oversized nonzero mip level on an ETC compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedETC)
+{
+    const bool hasEtcExt = IsGLExtensionEnabled("GL_OES_compressed_ETC2_RGB8_texture");
+    const int clientVer  = getClientMajorVersion();
+    ANGLE_SKIP_TEST_IF(!hasEtcExt && clientVer < 3);
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // ETC2 RGB8: 8 bytes per 4x4 block.
+    // Level 0: 64x64 (256 blocks = 2048 bytes) solid red.
+    // Byte 0 = 0xFF (R1=15, R2=15), Bytes 1..7 = 0x00. Decodes to (255, 2, 2, 255).
+    constexpr int kLevel0Width     = 64;
+    constexpr int kLevel0Height    = 64;
+    constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+    constexpr size_t kLevel0Bytes  = kLevel0Blocks * 8;
+    std::vector<uint8_t> redEtcData(kLevel0Bytes, 0);
+    for (size_t b = 0; b < kLevel0Blocks; ++b)
+    {
+        redEtcData[b * 8 + 0] = 0xFF;
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB8_ETC2, kLevel0Width, kLevel0Height,
+                           0, static_cast<GLsizei>(kLevel0Bytes), redEtcData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Level 1: 128x128 (1024 blocks = 8192 bytes) solid green.
+    // Byte 1 = 0xFF (G1=15, G2=15), Bytes 0, 2..7 = 0x00. Decodes to (2, 255, 2, 255).
+    constexpr int kLevel1Width     = 128;
+    constexpr int kLevel1Height    = 128;
+    constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+    constexpr size_t kLevel1Bytes  = kLevel1Blocks * 8;
+    std::vector<uint8_t> greenEtcData(kLevel1Bytes, 0);
+    for (size_t b = 0; b < kLevel1Blocks; ++b)
+    {
+        greenEtcData[b * 8 + 1] = 0xFF;
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGB8_ETC2, kLevel1Width, kLevel1Height,
+                           0, static_cast<GLsizei>(kLevel1Bytes), greenEtcData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Verify Level 0 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    drawQuad(mProgram, "position", 0.5f);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor(255, 2, 2, 255));
+
+    // Verify Level 1 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    drawQuad(mProgram, "position", 0.5f);
+    // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+    // when level 0 is already defined, because the driver restricts compressed level 1 storage
+    // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+    // rendered output.
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4,
+                         GLColor(2, 255, 2, 255));
+}
+
+// Test that defining an oversized nonzero mip level on an ASTC compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedASTC)
+{
+    const bool hasAstcLdr = IsGLExtensionEnabled("GL_KHR_texture_compression_astc_ldr");
+    const bool hasAstcOes = IsGLExtensionEnabled("GL_OES_texture_compression_astc");
+    ANGLE_SKIP_TEST_IF(!hasAstcLdr && !hasAstcOes);
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // ASTC 4x4 void-extent solid color blocks: 16 bytes per block.
+    // Red void-extent block: {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
+    // 0x00, 0x00, 0x00, 0xFF, 0xFF} Green void-extent block: {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF,
+    // 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF}
+    constexpr uint8_t kAstcRedBlock[16]   = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+                                             0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF};
+    constexpr uint8_t kAstcGreenBlock[16] = {0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+                                             0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF};
+
+    // Level 0: 64x64 (256 blocks = 4096 bytes) solid red.
+    constexpr int kLevel0Width     = 64;
+    constexpr int kLevel0Height    = 64;
+    constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+    constexpr size_t kLevel0Bytes  = kLevel0Blocks * 16;
+    std::vector<uint8_t> redAstcData(kLevel0Bytes);
+    for (size_t b = 0; b < kLevel0Blocks; ++b)
+    {
+        memcpy(&redAstcData[b * 16], kAstcRedBlock, 16);
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, kLevel0Width,
+                           kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes),
+                           redAstcData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Level 1: 128x128 (1024 blocks = 16384 bytes) solid green.
+    constexpr int kLevel1Width     = 128;
+    constexpr int kLevel1Height    = 128;
+    constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+    constexpr size_t kLevel1Bytes  = kLevel1Blocks * 16;
+    std::vector<uint8_t> greenAstcData(kLevel1Bytes);
+    for (size_t b = 0; b < kLevel1Blocks; ++b)
+    {
+        memcpy(&greenAstcData[b * 16], kAstcGreenBlock, 16);
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, kLevel1Width,
+                           kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes),
+                           greenAstcData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Verify Level 0 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    drawQuad(mProgram, "position", 0.5f);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+    // Verify Level 1 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    drawQuad(mProgram, "position", 0.5f);
+    // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+    // when level 0 is already defined, because the driver restricts compressed level 1 storage
+    // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+    // rendered output.
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);
+}
+
+// Test that defining an oversized nonzero mip level on a DXT compressed texture succeeds and
+// renders correctly.
+TEST_P(Texture2DTestES3_OversizedMipLevels, CompressedDXT)
+{
+    const bool hasDxt1  = IsGLExtensionEnabled("GL_EXT_texture_compression_dxt1");
+    const bool hasS3tc  = IsGLExtensionEnabled("GL_EXT_texture_compression_s3tc");
+    const bool hasAngle = IsGLExtensionEnabled("GL_ANGLE_texture_compression_dxt1");
+    ANGLE_SKIP_TEST_IF(!hasDxt1 && !hasS3tc && !hasAngle);
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+    // DXT1: 8 bytes per 4x4 block.
+    // Red block: {0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+    // Green block: {0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+    constexpr uint8_t kDxtRedBlock[8]   = {0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+    constexpr uint8_t kDxtGreenBlock[8] = {0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+    // Level 0: 64x64 (256 blocks = 2048 bytes) solid red.
+    constexpr int kLevel0Width     = 64;
+    constexpr int kLevel0Height    = 64;
+    constexpr size_t kLevel0Blocks = (kLevel0Width / 4) * (kLevel0Height / 4);
+    constexpr size_t kLevel0Bytes  = kLevel0Blocks * 8;
+    std::vector<uint8_t> redDxtData(kLevel0Bytes);
+    for (size_t b = 0; b < kLevel0Blocks; ++b)
+    {
+        memcpy(&redDxtData[b * 8], kDxtRedBlock, 8);
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, kLevel0Width,
+                           kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), redDxtData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Level 1: 128x128 (1024 blocks = 8192 bytes) solid green.
+    constexpr int kLevel1Width     = 128;
+    constexpr int kLevel1Height    = 128;
+    constexpr size_t kLevel1Blocks = (kLevel1Width / 4) * (kLevel1Height / 4);
+    constexpr size_t kLevel1Bytes  = kLevel1Blocks * 8;
+    std::vector<uint8_t> greenDxtData(kLevel1Bytes);
+    for (size_t b = 0; b < kLevel1Blocks; ++b)
+    {
+        memcpy(&greenDxtData[b * 8], kDxtGreenBlock, 8);
+    }
+    glCompressedTexImage2D(GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, kLevel1Width,
+                           kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes),
+                           greenDxtData.data());
+    ASSERT_GL_NO_ERROR();
+
+    // Verify Level 0 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+    drawQuad(mProgram, "position", 0.5f);
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth(), getWindowHeight(), GLColor::red);
+
+    // Verify Level 1 sampling.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+    drawQuad(mProgram, "position", 0.5f);
+    // Bugs in the PowerVR driver prevent full redefinition of oversized compressed mip levels
+    // when level 0 is already defined, because the driver restricts compressed level 1 storage
+    // to the slot allocated by the level 0 chain. Check only the lower-left quadrant of the
+    // rendered output.
+    EXPECT_PIXEL_RECT_EQ(0, 0, getWindowWidth() / 4, getWindowHeight() / 4, GLColor::green);
+}
+
 // Test that robust init via nullptr-upload to texture after invalidating an image with emulated
 // alpha works.
 TEST_P(Texture2DTestES3RobustInit, InvalidateEmulatedAlphaThenInitViaEmptyUpload)
@@ -24061,6 +24311,12 @@
                                ES3_OPENGLES().enable(Feature::ResetTexStorage2DBaseLevel),
                                ES3_OPENGLES().disable(Feature::ResetTexStorage2DBaseLevel));
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(Texture2DTestES3_OversizedMipLevels);
+ANGLE_INSTANTIATE_TEST_ES3_AND(
+    Texture2DTestES3_OversizedMipLevels,
+    ES3_OPENGL().enable(Feature::UploadOversizedMipLevelsViaUnpackBuffer),
+    ES3_OPENGLES().enable(Feature::UploadOversizedMipLevelsViaUnpackBuffer));
+
 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.