CVE-2026-9919
Overview
Background
- `depth` parameter
- For
GL_TEXTURE_2D_ARRAY(and 3D) textures,depthis the number of slices; the total size is per-slice size timesdepth. - Lazy clearing
- Chromium’s validating command decoder leaves texture storage uncleared until first use, then zero-fills it (allocating a
base::HeapArrayand callingglCompressedTexSubImage3D) 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.
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
- Malicious page on Android Host a WebGL2 context on a device using the validating decoder (inferred default on Android).
- Enable a missing-multiply format
Enable e.g.
WEBGL_compressed_texture_astc. - 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. - Force lazy clear
Bind and issue a draw (
gl.drawArrays), triggeringClearCompressedTextureLevel3D, which allocates a one-slice buffer but uploads withdepth=256. - 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).
- Exfiltrate
Sample the texture and
gl.readPixels()to read back cross-origin heap/VRAM contents (primitive: cross-origin information disclosure).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/gl_utils.cc |
modified |
Files Changed
gpu/command_buffer/service/gl_utils.cc
Audit Directions
- Size vs. validate symmetryAudit all callers of
GetCompressedTexSizeInBytes(lazy clear intexture_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 driftReview 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
ClearCompressedTextureLevel3Dreportedly returnstrueeven when the driver errors; audit lazy-clear routines that unconditionally mark levels cleared without checking the driver result.
Patch
From 35873c178b689d732069c41f6c4d9d79d8214af8 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <vasilyt@chromium.org>
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 <geofflang@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
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,
Original Bug Report
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.ccgpu/command_buffer/service/gles2_cmd_decoder.ccgpu/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). BecauseClearCompressedTextureLevel3Dignores driver errors and unconditionally returnstrue, 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.
- An attacker creates a malicious webpage hosting a WebGL2 context, targeting an Android device.
- The attacker enables a compressed texture extension (e.g.,
WEBGL_compressed_texture_astc). - 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. - The attacker binds the texture and triggers a draw call (e.g.,
gl.drawArrays). - 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. - The texture array is populated with either out-of-bounds CPU heap memory or uninitialized VRAM.
- 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.