Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in ANGLE
DescriptionBuffer overflow in ANGLE
ComponentANGLE
Sub-componentGPU
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.
Tracker556435507
Fix commit069a204ae813 (chromium/src) +223/-9
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 (STARLABS SG)
Reported2026-09-03 (per vendor advisory)
Fix landed2026-09-18 (public git)
Disclosed2026-09-22
Patch gap4 days — fix public in source before the advisory

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.

Key insight
The single mistake was measuring compressed textures in pixels instead of compression blocks, so the workaround’s size bound was wrong for block formats; introducing GetCompressedTexBlockDimensions and computing in blocks makes the bound match the real allocation.

Attack Path

  1. WebGL with compressed textures attacker JavaScript acquires a WebGL context and a block-compressed texture extension (e.g. ASTC).
  2. Trigger the oversized-mip path it requests a mip level large enough to route through the upload_oversized_mip_levels_via_unpack_buffer workaround.
  3. 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.
  4. Overflow the GPU process the mis-sized copy corrupts adjacent GPU-process memory (or over-reads it), yielding a memory-safety primitive.

Impact Assessment

An attacker gains an out-of-bounds access in the GPU process through the compressed-mip upload workaround, driven entirely from WebGL. The precondition is that a compressed-texture upload is large enough to enter the oversized-mip workaround path; the miscomputed size is what turns a benign upload into an overflow, so any format whose block size differs from one pixel is affected.

Changed Functions

FunctionChangeNotes
switch
gpu/command_buffer/service/gl_utils.cc
modified
if
gpu/command_buffer/service/gles2_cmd_decoder.cc
modified
TEST_P
gpu/command_buffer/tests/gl_texture_unittest.cc
modified
if
gpu/command_buffer/tests/gl_texture_unittest.cc
modified
for
gpu/command_buffer/tests/gl_texture_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/gl_utils.h
  • gpu/command_buffer/service/gl_utils.cc
  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/tests/gl_texture_unittest.cc

Audit Directions

  • Pixel-based size math on compressed formats
    audit any size/stride computation for compressed textures that multiplies raw width×height without dividing into blocks.
  • Workaround copy bounds
    flag driver-workaround upload paths whose staged-copy size is computed independently of the real format layout.
  • New-format coverage
    when adding a compressed format, verify it is present in GetCompressedTexBlockDimensions and every size helper, since a missing entry silently falls back to a 1×1 block.
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
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.