Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker494823889
Fix commitc83175b85bf9 (angle/angle) +24/-1
CISA KEVNot listed
Credited86ac1f1587b71893ed2ad792cd7dde32
Disclosed2026-06-02

Files Changed

  • src/libANGLE/validationES.cpp
  • src/tests/gl_tests/TextureTest.cpp
From c83175b85bf991ff2ef05073496c50c9ebfc569d Mon Sep 17 00:00:00 2001
From: Amirali Abdolrashidi <abdolrashidi@google.com>
Date: Fri, 10 Apr 2026 17:05:02 -0700
Subject: [PATCH] Check depth for cube map arrays in size validation

* Updated ValidImageDataSize() so cube map arrays are also regarded as
  3D targets, so their depth is also taken into account when checking
  their size.
  * (Otherwise, it can lead to memory access errors or VVLs such as
    VUID-vkCmdCopyBufferToImage-pRegions-00171.)

* Added the test: ValidateCubeMapArrayCopyExceedsPBOSize.
  * It attempts to copy to a cube map array from a PBO with a smaller
    size. It is expected that the glTexSubImage3D() call fails with
    GL_INVALID_OPERATION.

Bug: chromium:494823889
Change-Id: I0927d1bfa4cc75c6a26cde442f2ffc95212aaa13
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7749968
Commit-Queue: Amirali Abdolrashidi <abdolrashidi@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
---

diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 360921e..bd51e02 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -1413,7 +1413,8 @@
     const Extents size(width, height, depth);
     const auto &unpack = context->getState().getUnpackState();
 
-    bool targetIs3D = texType == TextureType::_3D || texType == TextureType::_2DArray;
+    bool targetIs3D = texType == TextureType::_3D || texType == TextureType::_2DArray ||
+                      texType == TextureType::CubeMapArray;
     GLuint endByte  = 0;
     if (!formatInfo.computePackUnpackEndByte(type, size, unpack, targetIs3D, &endByte))
     {
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 2b6399c..e756a93 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14644,6 +14644,28 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Tests copying from a PBO into a cube map array exceeding its size.
+TEST_P(TextureCubeTestES32, ValidateCubeMapArrayCopyExceedsPBOSize)
+{
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_UNPACK_BUFFER, 4 * 1024 * 1024 + 16, nullptr, GL_STATIC_DRAW);
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, tex);
+    glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, 1024, 1024, 6);
+
+    glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, 1024, 1024, 6, GL_RGBA, GL_UNSIGNED_BYTE,
+                    reinterpret_cast<const void *>(1));
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+    glFlush();
+
+    glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, 1024, 1024, 6, GL_RGBA, GL_UNSIGNED_BYTE,
+                    nullptr);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+    glFlush();
+}
+
 // Verify that using negative texture base level and max level generates GL_INVALID_VALUE.
 TEST_P(Texture2DTestES3, NegativeTextureBaseLevelAndMaxLevel)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 2b6399c..e756a93 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14644,6 +14644,28 @@
     EXPECT_GL_ERROR(GL_INVALID_OPERATION);
 }
 
+// Tests copying from a PBO into a cube map array exceeding its size.
+TEST_P(TextureCubeTestES32, ValidateCubeMapArrayCopyExceedsPBOSize)
+{
+    GLBuffer pbo;
+    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_UNPACK_BUFFER, 4 * 1024 * 1024 + 16, nullptr, GL_STATIC_DRAW);
+
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, tex);
+    glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, 1024, 1024, 6);
+
+    glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, 1024, 1024, 6, GL_RGBA, GL_UNSIGNED_BYTE,
+                    reinterpret_cast<const void *>(1));
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+    glFlush();
+
+    glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, 1024, 1024, 6, GL_RGBA, GL_UNSIGNED_BYTE,
+                    nullptr);
+    ASSERT_GL_ERROR(GL_INVALID_OPERATION);
+    glFlush();
+}
+
 // Verify that using negative texture base level and max level generates GL_INVALID_VALUE.
 TEST_P(Texture2DTestES3, NegativeTextureBaseLevelAndMaxLevel)
 {
Loading diff…

Original Bug Report

reported by se...@gmail.com

Heap OOB read in ANGLE via `CubeMapArray` texture upload due to `endByte` underestimation in `ValidImageDataSize`

Summary

ANGLE’s ValidImageDataSize function fails to account for CubeMapArray when computing endByte, treating it as a 2D upload instead of a 3D one. This causes the PBO bounds check and the robust-variant bufSize check to use an underestimated byte count, allowing undersized source buffers to pass validation. The backend then reads the full 3D extent from the undersized buffer, producing a heap-buffer-overflow. The bug is reachable from a compromised renderer on platforms where the ANGLE Vulkan or OpenGL backend exposes the GL_EXT_texture_cube_map_array extension (Linux, Android, Windows with Vulkan). It affects the GPU process.

Bisect

Introducing Commit: 7fde3673a473621063f88faa35294dde43dd07c0

This commit added CubeMapArray to ImageIndex::usesTex3D() and to various frontend dispatch paths, but did not update the targetIs3D variable in ValidImageDataSize. The original targetIs3D check was introduced earlier in commit ff5b2d5128 when the GL_ANGLE_robust_client_memory extension was added.

Root Cause

ValidImageDataSize in validationES.cpp determines whether a texture target requires 3D data layout calculations through a local boolean:

// third_party/angle/src/libANGLE/validationES.cpp
bool targetIs3D = texType == TextureType::_3D || texType == TextureType::_2DArray;

This variable is missing TextureType::CubeMapArray (and TextureType::_2DMultisampleArray). It is passed to computePackUnpackEndByte, which uses it to decide whether to include the depth pitch in the total byte count:

// third_party/angle/src/libANGLE/formatutils.cpp
if (is3D)
{
    CheckedNumeric<GLuint> depthMinusOne = size.depth - 1;
    checkedCopyBytes += depthMinusOne * depthPitch;
}

When is3D is false, the entire depth contribution is omitted. For a 4x4 RGBA8 CubeMapArray texture with depth=6, the correct endByte is 64 + 5×64 = 384, but the function computes only 64.

The implementation side does not share this oversight. ImageIndex::usesTex3D() correctly includes CubeMapArray:

// third_party/angle/src/libANGLE/ImageIndex.cpp
bool ImageIndex::usesTex3D() const
{
    return mType == TextureType::_3D || mType == TextureType::_2DArray ||
           mType == TextureType::_2DMultisampleArray || mType == TextureType::CubeMapArray;
}

The Vulkan backend calls calculateBufferInfo with index.usesTex3D(), which returns true for CubeMapArray, and proceeds to read the full depth extent from the source buffer.

The underestimated endByte is used in two places that constitute real security checks. For PBO uploads, ValidImageDataSize verifies that the pixel unpack buffer is large enough:

// third_party/angle/src/libANGLE/validationES.cpp
if (pixelUnpackBuffer)
{
    CheckedNumeric<size_t> checkedEndByte(endByte);
    CheckedNumeric<size_t> checkedOffset(reinterpret_cast<size_t>(pixels));
    checkedEndByte += checkedOffset;
    if (checkedEndByte.ValueOrDie() > static_cast<size_t>(pixelUnpackBuffer->getSize()))
    {
        return false;
    }
}

For the GL_ANGLE_robust_client_memory path (used by the Chromium command buffer via glTexSubImage3DRobustANGLE), it checks that the caller-supplied bufSize covers the pixel data:

if (pixels != nullptr && endByte > static_cast<GLuint>(imageSize))
{
    return false;
}

Both checks pass with the underestimated endByte, but the backend reads the correct, larger amount.

In Chromium’s GPU process architecture, the passthrough command buffer decoder forwards texture targets to ANGLE without independent validation. A compromised renderer can therefore send GL_TEXTURE_CUBE_MAP_ARRAY (0x9009) as the target for TexSubImage3D, and if the ANGLE backend supports the extension, the command reaches the vulnerable validation path in the GPU process. The CubeMapArray extension is not requested by default for WebGL contexts, but a compromised renderer can enable it by calling RequestExtensionCHROMIUM("GL_EXT_texture_cube_map_array") through the command buffer; this maps to glRequestExtensionANGLE, which is processed in the GPU process and enables the extension if the Vulkan device supports imageCubeArray.

Reproduce

This issue was tested on Chromium commit e6831951cd5fd2d7db105507e6f5e06ba600e073, Ubuntu 22.04. The ANGLE Vulkan backend is required because GL_EXT_texture_cube_map_array must be available and the vulnerable CPU copy path is Vulkan-specific. Use --use-angle=vulkan on all platforms.

Check out the commit and apply the patch, which modifies WebGL2RenderingContextBase::texSubImage3D in the renderer process to inject CubeMapArray commands when it detects a sentinel PBO offset value. This simulates a compromised renderer that sends crafted GL commands through the passthrough command buffer decoder. No GPU process code is modified.

Configure an ASAN build with out/asan/args.gn as follows, then build:

is_asan = true
is_debug = false
dcheck_always_on = false
git apply patch.diff
autoninja -C out/asan chrome

Launch Chrome and open the PoC page:

out/asan/chrome --use-angle=vulkan --enable-logging=stderr --user-data-dir=./userdata poc.html

The PoC page creates a WebGL2 context and calls texSubImage3D with PBO offset 0x900900, which the patched renderer intercepts. The renderer requests the GL_EXT_texture_cube_map_array extension via RequestExtensionCHROMIUM, creates a 1024×1024 CubeMapArray texture with 6 layers (RGBA8), and creates a ~4MB PBO (exactly layerSize+3 bytes). It then calls TexSubImage3D with the CubeMapArray target, depth=6, and PBO offset=3. The non-aligned offset forces ANGLE’s Vulkan backend into the CPU copy path. ANGLE’s ValidImageDataSize computes endByte for a single layer (~4MB) instead of all 6 layers (~24MB) because it treats CubeMapArray as a 2D target, so the PBO size check passes. The backend then reads ~24MB from the ~4MB PBO allocation, producing a ~20MB heap-buffer-overflow.

ASAN output (Linux):

=================================================================
==345063==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x778695bbb000 at pc 0x5ce65edb700b bp 0x7ffcce9e7d10 sp 0x7ffcce9e74d0
READ of size 25165824 at 0x778695bbb000 thread T0 (chrome)
    #0 0x5ce65edb700a in __asan_memcpy (/home/test/Desktop/chromium/src/out/asan/chrome+0x10d2400a) (BuildId: e191f33f940a09e4)
    #1 0x77875f943833 in void angle::LoadToNative<unsigned char, 4ul>(angle::ImageLoadContext const&, unsigned long, unsigned long, unsigned long, unsigned char const*, unsigned long, unsigned long, unsigned char*, unsigned long, unsigned long) third_party/angle/src/image_util/loadimage.inc
    #2 0x77875f43484e in rx::vk::ImageHelper::stageSubresourceUpdateImpl(rx::ContextVk*, gl::ImageIndex const&, angle::Extents<int> const&, angle::Offset<int> const&, gl::InternalFormat const&, gl::PixelUnpackState const&, unsigned int, unsigned char const*, rx::vk::Format const&, rx::vk::ImageFormatSupport, unsigned int, unsigned int, unsigned int, rx::vk::ApplyImageUpdate, bool*) third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp:8538:5
    #3 0x77875f2a8c34 in rx::TextureVk::setSubImageImpl(gl::Context const*, gl::ImageIndex const&, gl::Box const&, gl::InternalFormat const&, unsigned int, gl::PixelUnpackState const&, gl::Buffer*, unsigned char const*, rx::vk::Format const&) third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp:1310:31
    #4 0x77875f2a7b2f in rx::TextureVk::setSubImage(gl::Context const*, gl::ImageIndex const&, gl::Box const&, unsigned int, unsigned int, gl::PixelUnpackState const&, gl::Buffer*, unsigned char const*) third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp:616:12
    #5 0x77875f84b246 in gl::Texture::setSubImage(gl::Context*, gl::PixelUnpackState const&, gl::Buffer*, gl::TextureTarget, int, gl::Box const&, unsigned int, unsigned int, unsigned char const*) third_party/angle/src/libANGLE/Texture.cpp:1441:25
......

References

Credit

Please use 86ac1f1587b71893ed2ad792cd7dde32 as the credit for this vulnerability. Thank you.

View on issue tracker