High CVSS 4.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebGL
DescriptionOut of bounds read in WebGL
ComponentWebGL
Bug ClassOOB
Tracker500114058
Fix commit35873c178b68 (chromium/src) +2/-5
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-05-06 (public git)
Disclosed2026-05-27
Patch gap21 days — fix public in source before the advisory

Background

`depth` parameter
For GL_TEXTURE_2D_ARRAY (and 3D) textures, depth is the number of slices; the total size is per-slice size times depth.
Lazy clearing
Chromium’s validating command decoder leaves texture storage uncleared until first use, then zero-fills it (allocating a base::HeapArray and calling glCompressedTexSubImage3D) to avoid leaking stale memory into web content.
Validating command decoder
The CPU-side GL command validator (default on Android); it both bounds-checks client data and manages the cleared/uncleared bookkeeping.

Root Cause Analysis

GetCompressedTexSizeInBytes computed a correct per-slice block size for every format, but only multiplied the result by depth inside the switch cases for the ES3-era formats (ETC2/EAC and BPTC/RGTC). For the other compressed families — ASTC (all variants), S3TC/DXT, ATC, ETC1, PVRTC — the depth multiply was simply absent, so the function returned the size of a single 2D slice regardless of how many array slices were requested. This undersized value then drove two security-relevant paths: the lazy-clear path (ClearCompressedTextureLevel3D) allocated a zero buffer sized for one slice but handed it to the driver’s glCompressedTexSubImage3D with the full depth, and the validation path (ValidateCompressedTexFuncData) accepted an attacker’s one-slice imageSize for a multi-slice upload.

The patch deletes the per-format bytes_required *= depth; lines from the ETC2/EAC, BPTC, and RGTC cases and instead applies a single bytes_required *= depth; once, after the switch, so the depth multiply now runs for every format uniformly (just before the IsValid() overflow check). This matches the advisory’s description exactly; the bug class is an OOB condition (CWE-125 out-of-bounds read / undersized allocation) rooted in an arithmetic/logic omission. The diff is small (2 added, 5 removed) and is a clean, direct fix for the described undersizing.

Key insight
The per-slice compressed-size computation multiplied by array depth only for ES3 formats; every other compressed format returned a one-slice size, so array-texture allocations and validations were too small by a factor of depth.

Attack Path

  1. Malicious page on Android Host a WebGL2 context on a device using the validating decoder (inferred default on Android).
  2. Enable a missing-multiply format Enable e.g. WEBGL_compressed_texture_astc.
  3. Allocate an array texture gl.texStorage3D(GL_TEXTURE_2D_ARRAY, 1, COMPRESSED_RGBA_ASTC_4x4_KHR, w, h, 256); VRAM is reserved and the texture marked uncleared.
  4. Force lazy clear Bind and issue a draw (gl.drawArrays), triggering ClearCompressedTextureLevel3D, which allocates a one-slice buffer but uploads with depth=256.
  5. Leak The driver either reads past the undersized CPU heap buffer (OOB read of adjacent process memory) or rejects it while the decoder still marks the texture cleared (uninitialized VRAM left in place).
  6. Exfiltrate Sample the texture and gl.readPixels() to read back cross-origin heap/VRAM contents (primitive: cross-origin information disclosure).

Impact Assessment

Executes in the GPU process on the validating decoder (Android default). No elevated privilege is required beyond a page able to use WebGL2 with a compressed-texture extension; UI:R reflects needed interaction. The primitive is an information leak: either an out-of-bounds read of adjacent CPU-heap memory uploaded into a sampleable texture, or disclosure of uninitialized VRAM (potentially cross-origin compositor tiles). The advisory’s low CVSS (4.3, C:L) is consistent — this is a read/leak, not memory-corruption or RCE. A secondary validation-bypass vector (accepting an undersized imageSize) can also drive the driver to read past the shared-memory transfer buffer.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/gl_utils.cc
modified

Files Changed

  • gpu/command_buffer/service/gl_utils.cc

Audit Directions

  • Size vs. validate symmetry
    Audit all callers of GetCompressedTexSizeInBytes (lazy clear in texture_manager.cc/gles2_cmd_decoder.cc, ValidateCompressedTexFuncData) to confirm the corrected size flows to both allocation and validation and that no caller re-derives size independently.
  • Per-format arithmetic drift
    Review other block-size helpers for format families where a dimension multiply (depth, layers, faces, mip) is applied in some branches but not others.
  • Error-ignoring clear paths
    ClearCompressedTextureLevel3D reportedly returns true even when the driver errors; audit lazy-clear routines that unconditionally mark levels cleared without checking the driver result.
From 35873c178b689d732069c41f6c4d9d79d8214af8 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <[email protected]>
Date: Wed, 06 May 2026 07:22:57 -0700
Subject: [PATCH] Fix GetCompressedTexSizeInBytes depth handling

It was handled only for ES2 formats.

Bug: 500114058
Change-Id: I987973c53e474a7ef9699cdd6eca71b9c04a3574
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7816322
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Vasiliy Telezhnikov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1626161}
---

diff --git a/gpu/command_buffer/service/gl_utils.cc b/gpu/command_buffer/service/gl_utils.cc
index c2772ae7..0067150 100644
--- a/gpu/command_buffer/service/gl_utils.cc
+++ b/gpu/command_buffer/service/gl_utils.cc
@@ -599,7 +599,6 @@
           base::CheckDiv(base::CheckAdd(height, kEACAndETC2BlockSize - 1),
                          kEACAndETC2BlockSize);
       bytes_required *= 8;
-      bytes_required *= depth;
       break;
     case GL_COMPRESSED_RG11_EAC:
     case GL_COMPRESSED_SIGNED_RG11_EAC:
@@ -612,7 +611,6 @@
           base::CheckDiv(base::CheckAdd(height, kEACAndETC2BlockSize - 1),
                          kEACAndETC2BlockSize);
       bytes_required *= 16;
-      bytes_required *= depth;
       break;
     case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:
     case GL_COMPRESSED_RGBA_BPTC_UNORM_EXT:
@@ -623,7 +621,6 @@
       bytes_required *= base::CheckDiv(
           base::CheckAdd(height, kBPTCBlockHeight - 1), kBPTCBlockHeight);
       bytes_required *= 16;
-      bytes_required *= depth;
       break;
     case GL_COMPRESSED_RED_RGTC1_EXT:
     case GL_COMPRESSED_SIGNED_RED_RGTC1_EXT:
@@ -632,7 +629,6 @@
       bytes_required *= base::CheckDiv(
           base::CheckAdd(height, kRGTCBlockHeight - 1), kRGTCBlockHeight);
       bytes_required *= 8;
-      bytes_required *= depth;
       break;
     case GL_COMPRESSED_RED_GREEN_RGTC2_EXT:
     case GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
@@ -641,7 +637,6 @@
       bytes_required *= base::CheckDiv(
           base::CheckAdd(height, kRGTCBlockHeight - 1), kRGTCBlockHeight);
       bytes_required *= 16;
-      bytes_required *= depth;
       break;
     default:
       if (function_name && error_state) {
@@ -651,6 +646,8 @@
       return false;
   }
 
+  bytes_required *= depth;
+
   if (!bytes_required.IsValid()) {
     if (function_name && error_state) {
       ERRORSTATE_SET_GL_ERROR(error_state, GL_INVALID_VALUE, function_name,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Heap OOB Read / Uninitialized VRAM Leak in WebGL Compressed Texture Arrays

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: The GetCompressedTexSizeInBytes function fails to multiply by the depth parameter for several compressed formats (ASTC, S3TC, PVRTC). This causes undersized buffer allocations during the lazy-clearing of GL_TEXTURE_2D_ARRAY, leading to a potential heap out-of-bounds read or an uninitialized VRAM leak in the GPU process.

Affected files:

  • gpu/command_buffer/service/gl_utils.cc
  • gpu/command_buffer/service/gles2_cmd_decoder.cc
  • gpu/command_buffer/service/texture_manager.cc

Estimated timestamp from git blame: 2026-03-24

Description

There is a logic error in gpu/command_buffer/service/gl_utils.cc where GetCompressedTexSizeInBytes calculates the memory required for compressed texture data. For formats like ETC2 and BPTC, it correctly multiplies the size by the depth parameter (representing slices in a 3D or 2D Array texture). However, for several other formats, it ignores depth and returns a size sufficient for only a single 2D slice. Affected formats include:

  • ASTC (all variants)
  • S3TC/DXT (DXT1, DXT3, DXT5)
  • ATC
  • ETC1
  • PVRTC

This flaw introduces two distinct security vulnerabilities in the Validating Command Decoder (which is enabled by default on Android):

Vector 1: Lazy Clear Undersized Allocation When gl.texStorage3D is used with GL_TEXTURE_2D_ARRAY, the texture is marked as uncleared. When the texture is subsequently used in a draw call, GLES2DecoderImpl::ClearCompressedTextureLevel3D is triggered to perform a lazy clear. It uses GetCompressedTexSizeInBytes to allocate a zero-initialized CPU buffer (base::HeapArray). Because the size is undersized (missing the depth multiplier), the buffer is too small. It then calls the driver’s glCompressedTexSubImage3D with the full depth but the undersized buffer.

Depending on the GL driver’s implementation, this results in:

  • Heap Out-of-Bounds Read: The driver reads past the end of the undersized CPU heap allocation, uploading sensitive cross-origin GPU process memory into the WebGL texture.
  • Uninitialized VRAM Leak: The driver rejects the undersized buffer (GL_INVALID_VALUE). Because ClearCompressedTextureLevel3D ignores driver errors and unconditionally returns true, Chrome incorrectly marks the texture as cleared, allowing the attacker to read uninitialized cross-process VRAM.

Vector 2: Validation Bypass GLES2DecoderImpl::ValidateCompressedTexFuncData also uses GetCompressedTexSizeInBytes to validate the imageSize provided by user data during compressedTexImage3D. An attacker can provide an undersized imageSize (corresponding to 1 slice) for a multi-slice texture. The validation will pass, and the driver may read out-of-bounds from the shared memory transfer buffer.

Suggested Attacker Steps

Note: These steps are based on static analysis; our tooling agent cannot execute code to verify them.

  1. An attacker creates a malicious webpage hosting a WebGL2 context, targeting an Android device.
  2. The attacker enables a compressed texture extension (e.g., WEBGL_compressed_texture_astc).
  3. The attacker calls gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.COMPRESSED_RGBA_ASTC_4x4_KHR, width, height, 256). Chrome allocates the VRAM and marks the texture as uncleared.
  4. The attacker binds the texture and triggers a draw call (e.g., gl.drawArrays).
  5. Chrome intercepts the draw call and calls ClearCompressedTextureLevel3D. It allocates an undersized buffer (only large enough for 1 slice, instead of 256) and passes it to the driver.
  6. The texture array is populated with either out-of-bounds CPU heap memory or uninitialized VRAM.
  7. The attacker’s WebGL shader samples the texture and uses gl.readPixels() to extract the leaked cross-origin data.

Suggested Fix

Update GetCompressedTexSizeInBytes in gpu/command_buffer/service/gl_utils.cc to correctly multiply the calculated bytes_required by the depth parameter for the affected formats (S3TC, ASTC, ATC, ETC1, and PVRTC), bringing them in line with the logic used for ETC2 and BPTC.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker