CVE-2026-95284
Overview
Background
- Compressed-texture block sizing
- block-compressed formats (S3TC, ASTC, ETC2/EAC, BPTC, RGTC, PVRTC) store a fixed-size block per NxM pixel tile, so a texture’s byte size must be computed in blocks, not pixels.
- `upload_oversized_mip_levels_via_unpack_buffer` workaround
- a GPU command-buffer path that uploads mip levels larger than the driver natively accepts by staging them through an unpack buffer, using a computed size to bound the copy.
- `GetCompressedTexSizeInBytes`
- the command-buffer helper that computes how many bytes a compressed upload occupies, and thus how much the workaround copies.
- `gfx::Vector2d` block dimensions
- the width/height (in pixels) of one compression block, needed to convert pixel dimensions into block counts.
Root Cause Analysis
The oversized-mip upload workaround computed compressed-texture sizes using pixel dimensions rather than the format’s block geometry, so for block-compressed formats the size used to bound the staged copy did not match the true byte layout. Because the workaround drives an upload through an unpack buffer using that size, a mismatched (under- or mis-computed) size let an oversized ASTC mipmap upload read or write past the intended bounds, a GPU-process buffer overflow.
The fix adds GetCompressedTexBlockDimensions(GLenum format), a table mapping every supported compressed format to its block width/height, and revises the size checks to compute sizes in blocks so the workaround’s bounds are correct for each format. A regression test for oversized ASTC mipmap uploads confirms the path no longer crashes.
GetCompressedTexBlockDimensions and computing in blocks makes the bound match the real allocation.Attack Path
- WebGL with compressed textures attacker JavaScript acquires a WebGL context and a block-compressed texture extension (e.g. ASTC).
- Trigger the oversized-mip path
it requests a mip level large enough to route through the
upload_oversized_mip_levels_via_unpack_bufferworkaround. - Exploit the pixel-vs-block gap because the size was computed in pixels, the staged copy’s bound is mismatched for the block format, so the upload reads/writes outside the buffer.
- Overflow the GPU process the mis-sized copy corrupts adjacent GPU-process memory (or over-reads it), yielding a memory-safety primitive.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchgpu/command_buffer/service/gl_utils.cc |
modified | |
ifgpu/command_buffer/service/gles2_cmd_decoder.cc |
modified | |
TEST_Pgpu/command_buffer/tests/gl_texture_unittest.cc |
modified | |
ifgpu/command_buffer/tests/gl_texture_unittest.cc |
modified | |
forgpu/command_buffer/tests/gl_texture_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/gl_utils.hgpu/command_buffer/service/gl_utils.ccgpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/tests/gl_texture_unittest.cc
Audit Directions
- Pixel-based size math on compressed formatsaudit any size/stride computation for compressed textures that multiplies raw width×height without dividing into blocks.
- Workaround copy boundsflag driver-workaround upload paths whose staged-copy size is computed independently of the real format layout.
- New-format coveragewhen adding a compressed format, verify it is present in
GetCompressedTexBlockDimensionsand every size helper, since a missing entry silently falls back to a 1×1 block.
Patch
From 069a204ae81338a90d88c622ac0bd5207a194041 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Thu, 17 Sep 2026 18:12:42 -0700 Subject: [PATCH] gpu: revise size checks for compressed texture workarounds. For the previous upload_oversized_mip_levels_via_unpack_buffer workaround, compute compressed texture sizes in blocks rather than pixels to be more correct. Add a regression test for oversized ASTC mipmap uploads which ensures they don't crash. Bug: 556435507 Change-Id: I9b0ad52ab332bbc18d9425f05d5a5df0a4304c6a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8417361 Commit-Queue: Kenneth Russell <[email protected]> Reviewed-by: Zhenyao Mo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700359} --- diff --git a/gpu/command_buffer/service/gl_utils.cc b/gpu/command_buffer/service/gl_utils.cc index 3b1b331..a28b8cc 100644 --- a/gpu/command_buffer/service/gl_utils.cc +++ b/gpu/command_buffer/service/gl_utils.cc @@ -490,6 +490,89 @@ NOTREACHED(); } +gfx::Vector2d GetCompressedTexBlockDimensions(GLenum format) { + switch (format) { + case GL_ATC_RGB_AMD: + case GL_ATC_RGBA_EXPLICIT_ALPHA_AMD: + case GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD: + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_ETC1_RGB8_OES: + return gfx::Vector2d(kS3TCBlockWidth, kS3TCBlockHeight); + case GL_COMPRESSED_RGBA_ASTC_4x4_KHR: + case GL_COMPRESSED_RGBA_ASTC_5x4_KHR: + case GL_COMPRESSED_RGBA_ASTC_5x5_KHR: + case GL_COMPRESSED_RGBA_ASTC_6x5_KHR: + case GL_COMPRESSED_RGBA_ASTC_6x6_KHR: + case GL_COMPRESSED_RGBA_ASTC_8x5_KHR: + case GL_COMPRESSED_RGBA_ASTC_8x6_KHR: + case GL_COMPRESSED_RGBA_ASTC_8x8_KHR: + case GL_COMPRESSED_RGBA_ASTC_10x5_KHR: + case GL_COMPRESSED_RGBA_ASTC_10x6_KHR: + case GL_COMPRESSED_RGBA_ASTC_10x8_KHR: + case GL_COMPRESSED_RGBA_ASTC_10x10_KHR: + case GL_COMPRESSED_RGBA_ASTC_12x10_KHR: + case GL_COMPRESSED_RGBA_ASTC_12x12_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR: + case GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR: { + const int index = + (format < GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR) + ? static_cast<int>(format - GL_COMPRESSED_RGBA_ASTC_4x4_KHR) + : static_cast<int>(format - + GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR); + return gfx::Vector2d(kASTCBlockArray[index].blockWidth, + kASTCBlockArray[index].blockHeight); + } + case GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG: + case GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG: + return gfx::Vector2d(4, 4); + case GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG: + case GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG: + return gfx::Vector2d(8, 4); + case GL_COMPRESSED_R11_EAC: + case GL_COMPRESSED_SIGNED_R11_EAC: + case GL_COMPRESSED_RGB8_ETC2: + case GL_COMPRESSED_SRGB8_ETC2: + case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + case GL_COMPRESSED_RG11_EAC: + case GL_COMPRESSED_SIGNED_RG11_EAC: + case GL_COMPRESSED_RGBA8_ETC2_EAC: + case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + return gfx::Vector2d(kEACAndETC2BlockSize, kEACAndETC2BlockSize); + case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT: + case GL_COMPRESSED_RGBA_BPTC_UNORM_EXT: + case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT: + case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT: + return gfx::Vector2d(kBPTCBlockWidth, kBPTCBlockHeight); + case GL_COMPRESSED_RED_RGTC1_EXT: + case GL_COMPRESSED_SIGNED_RED_RGTC1_EXT: + case GL_COMPRESSED_RED_GREEN_RGTC2_EXT: + case GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT: + return gfx::Vector2d(kRGTCBlockWidth, kRGTCBlockHeight); + default: + return gfx::Vector2d(1, 1); + } +} + bool GetCompressedTexSizeInBytes(const char* function_name, GLsizei width, GLsizei height, diff --git a/gpu/command_buffer/service/gl_utils.h b/gpu/command_buffer/service/gl_utils.h index a567a04..b68069c 100644 --- a/gpu/command_buffer/service/gl_utils.h +++ b/gpu/command_buffer/service/gl_utils.h @@ -21,6 +21,7 @@ #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/rrect_f.h" #include "ui/gfx/geometry/transform.h" +#include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/overlay_transform.h" #include "ui/gl/gl_bindings.h" @@ -117,6 +118,8 @@ error::ContextLostReason GetContextLostReasonFromResetStatus( GLenum reset_status); +GPU_GLES2_EXPORT gfx::Vector2d GetCompressedTexBlockDimensions(GLenum format); + bool GetCompressedTexSizeInBytes(const char* function_name, GLsizei width, GLsizei height, diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc index 9f98d38..2ddb9f5a 100644 --- a/gpu/command_buffer/service/gles2_cmd_decoder.cc +++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc @@ -13410,15 +13410,20 @@ if (texture->GetLevelSize(target, 0, &level0_width, &level0_height, &level0_depth) && level0_width > 0 && level0_height > 0) { - const int slot_w = std::max( - 1, static_cast<int>( - std::bit_ceil(static_cast<uint32_t>(level0_width))) >> - level); - const int slot_h = std::max( - 1, static_cast<int>( - std::bit_ceil(static_cast<uint32_t>(level0_height))) >> - level); - if (width > slot_w || height > slot_h) { + const gfx::Vector2d block_size = + GetCompressedTexBlockDimensions(internal_format); + const int expected_width = std::max(1, level0_width >> level); + const int expected_height = std::max(1, level0_height >> level); + const int expected_blocks_x = + (expected_width + block_size.x() - 1) / block_size.x(); + const int expected_blocks_y = + (expected_height + block_size.y() - 1) / block_size.y(); + const int incoming_blocks_x = + (width + block_size.x() - 1) / block_size.x(); + const int incoming_blocks_y = + (height + block_size.y() - 1) / block_size.y(); + if (incoming_blocks_x > expected_blocks_x || + incoming_blocks_y > expected_blocks_y) { GLuint scratch = 0; api()->glGenBuffersARBFn(1, &scratch); api()->glBindBufferFn(GL_PIXEL_UNPACK_BUFFER, scratch); diff --git a/gpu/command_buffer/tests/gl_texture_unittest.cc b/gpu/command_buffer/tests/gl_texture_unittest.cc index 4568e4f..c43cff7e 100644 --- a/gpu/command_buffer/tests/gl_texture_unittest.cc +++ b/gpu/command_buffer/tests/gl_texture_unittest.cc @@ -13,6 +13,7 @@ #include "base/containers/span.h" #include "base/logging.h" +#include "build/build_config.h" #include "gpu/command_buffer/tests/gl_manager.h" #include "gpu/command_buffer/tests/gl_test_utils.h" #include "gpu/config/gpu_driver_bug_workarounds.h" @@ -303,6 +304,128 @@ glDeleteProgram(program); } +// Test that defining an oversized nonzero mip level (level 0 257x257, level 1 +// 256x256) on an ASTC 12x12 compressed texture does not crash during the +// level 1 upload. +TEST_P(GLTextureTest, OversizedMipLevelsCompressedASTC12x12_256x256) { + if (!IsApplicable()) { + return; + } + + const bool has_astc = + GLTestHelper::HasExtension("GL_KHR_texture_compression_astc_ldr") || + GLTestHelper::HasExtension("GL_OES_texture_compression_astc"); + if (!has_astc) { + LOG(INFO) << "ASTC not supported, skipping test"; + return; + } + + GLuint tex = 0; + glGenTextures(1, &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 void-extent solid color block: 16 bytes per block. + constexpr auto kAstcBlock = + std::to_array<uint8_t>({0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF}); + + // Level 0: 257x257. In 12x12 blocks: ceil(257/12) = 22 blocks per axis. + constexpr int kLevel0Width = 257; + constexpr int kLevel0Height = 257; + constexpr size_t kLevel0Blocks = 22 * 22; + constexpr size_t kLevel0Bytes = kLevel0Blocks * 16; + std::vector<uint8_t> level0_data(kLevel0Bytes); + for (size_t b = 0; b < kLevel0Blocks; ++b) { + base::span(level0_data).subspan(b * 16u, 16u).copy_from(kAstcBlock); + } + glCompressedTexImage2D( + GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel0Width, + kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), level0_data.data()); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + // Level 1: 256x256. In 12x12 blocks: ceil(256/12) = 22 blocks per axis. + constexpr int kLevel1Width = 256; + constexpr int kLevel1Height = 256; + constexpr size_t kLevel1Blocks = 22 * 22; + constexpr size_t kLevel1Bytes = kLevel1Blocks * 16; + std::vector<uint8_t> level1_data(kLevel1Bytes); + for (size_t b = 0; b < kLevel1Blocks; ++b) { + base::span(level1_data).subspan(b * 16u, 16u).copy_from(kAstcBlock); + } + glCompressedTexImage2D( + GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel1Width, + kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes), level1_data.data()); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glFinish(); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glDeleteTextures(1, &tex); +} + +// Test that defining an oversized nonzero mip level (level 0 257x257, level 1 +// 192x192) on an ASTC 12x12 compressed texture does not crash during the +// level 1 upload. +TEST_P(GLTextureTest, OversizedMipLevelsCompressedASTC12x12_192x192) { + if (!IsApplicable()) { + return; + } + + const bool has_astc = + GLTestHelper::HasExtension("GL_KHR_texture_compression_astc_ldr") || + GLTestHelper::HasExtension("GL_OES_texture_compression_astc"); + if (!has_astc) { + LOG(INFO) << "ASTC not supported, skipping test"; + return; + } + + GLuint tex = 0; + glGenTextures(1, &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 void-extent solid color block: 16 bytes per block. + constexpr auto kAstcBlock = + std::to_array<uint8_t>({0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF}); + + // Level 0: 257x257. In 12x12 blocks: ceil(257/12) = 22 blocks per axis. + constexpr int kLevel0Width = 257; + constexpr int kLevel0Height = 257; + constexpr size_t kLevel0Blocks = 22 * 22; + constexpr size_t kLevel0Bytes = kLevel0Blocks * 16; + std::vector<uint8_t> level0_data(kLevel0Bytes); + for (size_t b = 0; b < kLevel0Blocks; ++b) { + base::span(level0_data).subspan(b * 16u, 16u).copy_from(kAstcBlock); + } + glCompressedTexImage2D( + GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel0Width, + kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), level0_data.data()); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + // Level 1: 192x192. In 12x12 blocks: ceil(192/12) = 16 blocks per axis. + constexpr int kLevel1Width = 192; + constexpr int kLevel1Height = 192; + constexpr size_t kLevel1Blocks = 16 * 16; + constexpr size_t kLevel1Bytes = kLevel1Blocks * 16; + std::vector<uint8_t> level1_data(kLevel1Bytes); + for (size_t b = 0; b < kLevel1Blocks; ++b) { + base::span(level1_data).subspan(b * 16u, 16u).copy_from(kAstcBlock); + } + glCompressedTexImage2D( + GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel1Width, + kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes), level1_data.data()); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glFinish(); + ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError()); + + glDeleteTextures(1, &tex); +} + // Test that defining an oversized nonzero mip level on a DXT compressed texture // succeeds and renders correctly. TEST_P(GLTextureTest, OversizedMipLevelsCompressedDXT) {
Regression Test / PoC
diff --git a/gpu/command_buffer/tests/gl_texture_unittest.cc b/gpu/command_buffer/tests/gl_texture_unittest.cc
index 4568e4f..c43cff7e 100644
--- a/gpu/command_buffer/tests/gl_texture_unittest.cc
+++ b/gpu/command_buffer/tests/gl_texture_unittest.cc
@@ -13,6 +13,7 @@
#include "base/containers/span.h"
#include "base/logging.h"
+#include "build/build_config.h"
#include "gpu/command_buffer/tests/gl_manager.h"
#include "gpu/command_buffer/tests/gl_test_utils.h"
#include "gpu/config/gpu_driver_bug_workarounds.h"
@@ -303,6 +304,128 @@
glDeleteProgram(program);
}
+// Test that defining an oversized nonzero mip level (level 0 257x257, level 1
+// 256x256) on an ASTC 12x12 compressed texture does not crash during the
+// level 1 upload.
+TEST_P(GLTextureTest, OversizedMipLevelsCompressedASTC12x12_256x256) {
+ if (!IsApplicable()) {
+ return;
+ }
+
+ const bool has_astc =
+ GLTestHelper::HasExtension("GL_KHR_texture_compression_astc_ldr") ||
+ GLTestHelper::HasExtension("GL_OES_texture_compression_astc");
+ if (!has_astc) {
+ LOG(INFO) << "ASTC not supported, skipping test";
+ return;
+ }
+
+ GLuint tex = 0;
+ glGenTextures(1, &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 void-extent solid color block: 16 bytes per block.
+ constexpr auto kAstcBlock =
+ std::to_array<uint8_t>({0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF});
+
+ // Level 0: 257x257. In 12x12 blocks: ceil(257/12) = 22 blocks per axis.
+ constexpr int kLevel0Width = 257;
+ constexpr int kLevel0Height = 257;
+ constexpr size_t kLevel0Blocks = 22 * 22;
+ constexpr size_t kLevel0Bytes = kLevel0Blocks * 16;
+ std::vector<uint8_t> level0_data(kLevel0Bytes);
+ for (size_t b = 0; b < kLevel0Blocks; ++b) {
+ base::span(level0_data).subspan(b * 16u, 16u).copy_from(kAstcBlock);
+ }
+ glCompressedTexImage2D(
+ GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel0Width,
+ kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), level0_data.data());
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ // Level 1: 256x256. In 12x12 blocks: ceil(256/12) = 22 blocks per axis.
+ constexpr int kLevel1Width = 256;
+ constexpr int kLevel1Height = 256;
+ constexpr size_t kLevel1Blocks = 22 * 22;
+ constexpr size_t kLevel1Bytes = kLevel1Blocks * 16;
+ std::vector<uint8_t> level1_data(kLevel1Bytes);
+ for (size_t b = 0; b < kLevel1Blocks; ++b) {
+ base::span(level1_data).subspan(b * 16u, 16u).copy_from(kAstcBlock);
+ }
+ glCompressedTexImage2D(
+ GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel1Width,
+ kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes), level1_data.data());
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glFinish();
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glDeleteTextures(1, &tex);
+}
+
+// Test that defining an oversized nonzero mip level (level 0 257x257, level 1
+// 192x192) on an ASTC 12x12 compressed texture does not crash during the
+// level 1 upload.
+TEST_P(GLTextureTest, OversizedMipLevelsCompressedASTC12x12_192x192) {
+ if (!IsApplicable()) {
+ return;
+ }
+
+ const bool has_astc =
+ GLTestHelper::HasExtension("GL_KHR_texture_compression_astc_ldr") ||
+ GLTestHelper::HasExtension("GL_OES_texture_compression_astc");
+ if (!has_astc) {
+ LOG(INFO) << "ASTC not supported, skipping test";
+ return;
+ }
+
+ GLuint tex = 0;
+ glGenTextures(1, &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 void-extent solid color block: 16 bytes per block.
+ constexpr auto kAstcBlock =
+ std::to_array<uint8_t>({0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF});
+
+ // Level 0: 257x257. In 12x12 blocks: ceil(257/12) = 22 blocks per axis.
+ constexpr int kLevel0Width = 257;
+ constexpr int kLevel0Height = 257;
+ constexpr size_t kLevel0Blocks = 22 * 22;
+ constexpr size_t kLevel0Bytes = kLevel0Blocks * 16;
+ std::vector<uint8_t> level0_data(kLevel0Bytes);
+ for (size_t b = 0; b < kLevel0Blocks; ++b) {
+ base::span(level0_data).subspan(b * 16u, 16u).copy_from(kAstcBlock);
+ }
+ glCompressedTexImage2D(
+ GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel0Width,
+ kLevel0Height, 0, static_cast<GLsizei>(kLevel0Bytes), level0_data.data());
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ // Level 1: 192x192. In 12x12 blocks: ceil(192/12) = 16 blocks per axis.
+ constexpr int kLevel1Width = 192;
+ constexpr int kLevel1Height = 192;
+ constexpr size_t kLevel1Blocks = 16 * 16;
+ constexpr size_t kLevel1Bytes = kLevel1Blocks * 16;
+ std::vector<uint8_t> level1_data(kLevel1Bytes);
+ for (size_t b = 0; b < kLevel1Blocks; ++b) {
+ base::span(level1_data).subspan(b * 16u, 16u).copy_from(kAstcBlock);
+ }
+ glCompressedTexImage2D(
+ GL_TEXTURE_2D, 1, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, kLevel1Width,
+ kLevel1Height, 0, static_cast<GLsizei>(kLevel1Bytes), level1_data.data());
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glFinish();
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+ glDeleteTextures(1, &tex);
+}
+
// Test that defining an oversized nonzero mip level on a DXT compressed texture
// succeeds and renders correctly.
TEST_P(GLTextureTest, OversizedMipLevelsCompressedDXT) {