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 <kbr@chromium.org>
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 <kbr@chromium.org>
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
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) {