Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in ANGLE
DescriptionHeap buffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker486972661
Fix commit4de47461e452 (angle/angle) +31/-26
CISA KEVNot listed
CreditedM. Fauzan Wijaya (Gh05t666nero)
Disclosed2026-03-18

Files Changed

  • src/libANGLE/renderer/vulkan/TextureVk.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/vk_renderer.cpp
  • src/libANGLE/renderer/vulkan/vk_renderer.h
  • src/tests/gl_tests/TextureTest.cpp
From 4de47461e45248eeaf8fd0ef04ca3949f98029da Mon Sep 17 00:00:00 2001
From: Amirali Abdolrashidi <abdolrashidi@google.com>
Date: Thu, 26 Feb 2026 15:17:40 -0800
Subject: [PATCH] Vulkan: Cap memory allocation size to 1GB

  Currently the maximum memory allocation size for a single object
can be inquired from the Vulkan driver via KHR_maintenance3 (promoted
to core in Vulkan 1.1). However, the reported limit can still be quite
large for common usage. In addition, it can increase the risk of memory
size calculations overflowing if the operands are not 64 bits. As an
example, if the limit is 4GB and an image is defined with 32-bit dims
and format that theoretically require 4GB, the multiplication of these
values can result in overflow, even if it is to be assigned to a 64-bit
value.
  * (One way to avoid such overflow cases is to split multiplication
    into several steps, e.g., CL:7595734)

This change aims to reduce the maximum size allowed for allocation in
order to reduce the risk of such overflow issues.

* Added kMemoryAllocationSizeLimit to vk_renderer.
  * Currently set to 1GB.

* Added to vk::Renderer: mMaxMemoryAllocationSize
  * Set to the minimum of kMemoryAllocationSizeLimit and
    the maxMemoryAllocationSize reported from the driver.

* Cast the values used in some buffer size calculations to
  size_t.
  * (The leftmost value being size_t should be enough to propagate
    this type for the result of each multiplication.)

* Updated the following test to use a smaller width and height to
  avoid error due to the new cap:
  TextureCubeTestES32.MaxArrayTextureLayersVerify

Bug: chromium:486972661
Bug: chromium:487208468
Change-Id: I98bde71ede153324f524bb579b19043a474823d5
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7615650
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Amirali Abdolrashidi <abdolrashidi@google.com>
---

diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 2dd5f8a..84c51d6 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -3161,17 +3161,9 @@
         // invalidate must be called after wait for finish.
         ANGLE_TRY(srcBuffer->invalidate(renderer));
 
-        // Use size_t calculations to avoid 32-bit overflows.  Note that the dimensions are bound by
-        // the maximums specified in Constants.h, and that gl::Box members are signed 32-bit
-        // integers.
-        static_assert(gl::IMPLEMENTATION_MAX_2D_TEXTURE_SIZE *
-                          gl::IMPLEMENTATION_MAX_2D_TEXTURE_SIZE <
-                      std::numeric_limits<int32_t>::max());
-        size_t dstBufferSize = sourceBox.width * sourceBox.height;
-        static_assert(gl::IMPLEMENTATION_MAX_3D_TEXTURE_SIZE *
-                          gl::IMPLEMENTATION_MAX_2D_ARRAY_TEXTURE_LAYERS * 16 <
-                      std::numeric_limits<int32_t>::max());
-        dstBufferSize *= sourceBox.depth * dstFormat.pixelBytes * layerCount;
+        size_t dstBufferSize =
+            static_cast<size_t>(sourceBox.width) * static_cast<size_t>(sourceBox.height) *
+            static_cast<size_t>(sourceBox.depth) * dstFormat.pixelBytes * layerCount;
 
         // Allocate memory in the destination texture for the copy/conversion.
         uint8_t *dstData = nullptr;
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index 4f9c83d..f32774f 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -10783,9 +10783,10 @@
     // used in this function to be of some combined depth and stencil format.
     ASSERT(getAspectFlags() == VK_IMAGE_ASPECT_COLOR_BIT);
 
-    uint32_t pixelBytes = imageFormat.pixelBytes;
-    size_t bufferSize =
-        sourceArea.width * sourceArea.height * sourceArea.depth * pixelBytes * layerCount;
+    size_t pixelBytes = imageFormat.pixelBytes;
+    size_t bufferSize = static_cast<size_t>(sourceArea.width) *
+                        static_cast<size_t>(sourceArea.height) *
+                        static_cast<size_t>(sourceArea.depth) * pixelBytes * layerCount;
 
     const VkImageAspectFlags aspectFlags = getAspectFlags();
 
diff --git a/src/libANGLE/renderer/vulkan/vk_renderer.cpp b/src/libANGLE/renderer/vulkan/vk_renderer.cpp
index 62c43ae..612c044 100644
--- a/src/libANGLE/renderer/vulkan/vk_renderer.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_renderer.cpp
@@ -166,6 +166,9 @@
 // value will use a dedicated VkDeviceMemory.
 constexpr size_t kImageSizeThresholdForDedicatedMemoryAllocation = 8 * 1024 * 1024;
 
+// Maximum size for an allocated memory for a single object.
+constexpr VkDeviceSize kMemoryAllocationSizeLimit = 1 * 1024 * 1024 * 1024;
+
 // Pipeline cache header version. It should be incremented any time there is an update to the cache
 // header or data structure.
 constexpr uint32_t kPipelineCacheVersion = 3;
@@ -2091,6 +2094,7 @@
       mSupportedBufferWritePipelineStageMask(0),
       mSupportedVulkanShaderStageMask(0),
       mMemoryAllocationTracker(MemoryAllocationTracker(this)),
+      mMaxMemoryAllocationSize(0),
       mMaxBufferMemorySizeLimit(0),
       mNativeVectorWidthDouble(0),
       mNativeVectorWidthHalf(0),
@@ -5726,13 +5730,20 @@
                        mPhysicalDeviceProperties.limits.maxVertexInputBindingStride)
             : 0;
 
+    // Although the maximum memory allocation size for a single object can be derived from the
+    // Vulkan driver, it could still be too large for common use (e.g., ~4GB on some platforms) and
+    // increase the risk of overflow if the object dimensions used for size calculations are 32-bit.
+    // The limit can be restricted to a specific fixed value to reduce this risk.
+    mMaxMemoryAllocationSize =
+        std::min(mMaintenance3Properties.maxMemoryAllocationSize, kMemoryAllocationSizeLimit);
+
     // The limits related to buffer size should also take the max memory allocation size and padding
     // (if applicable) into account.
-    mMaxBufferMemorySizeLimit = getMaxMemoryAllocationSize() - mMaxVertexAttribStride;
+    mMaxBufferMemorySizeLimit = mMaxMemoryAllocationSize - mMaxVertexAttribStride;
 
     ANGLE_FEATURE_CONDITION(&mFeatures, forceD16TexFilter, IsAndroid() && isQualcommProprietary);
 
-    // Allocation sanitization disabled by default because of a heaveyweight implementation
+    // Allocation sanitization disabled by default because of a heavyweight implementation
     // that can cause OOM and timeouts.
     ANGLE_FEATURE_CONDITION(&mFeatures, allocateNonZeroMemory, false);
 
diff --git a/src/libANGLE/renderer/vulkan/vk_renderer.h b/src/libANGLE/renderer/vulkan/vk_renderer.h
index 41f4119..3e7d131 100644
--- a/src/libANGLE/renderer/vulkan/vk_renderer.h
+++ b/src/libANGLE/renderer/vulkan/vk_renderer.h
@@ -659,10 +659,7 @@
 
     void requestAsyncCommandsAndGarbageCleanup(vk::ErrorContext *context);
 
-    VkDeviceSize getMaxMemoryAllocationSize() const
-    {
-        return mMaintenance3Properties.maxMemoryAllocationSize;
-    }
+    VkDeviceSize getMaxMemoryAllocationSize() const { return mMaxMemoryAllocationSize; }
 
     // Cleanup garbage and finish command batches from the queue if necessary in the event of an OOM
     // error.
@@ -1113,6 +1110,9 @@
     // A placeholder descriptor set layout handle for layouts with no bindings.
     vk::DescriptorSetLayoutPtr mPlaceHolderDescriptorSetLayout;
 
+    // Allocation size limit for a single object.
+    VkDeviceSize mMaxMemoryAllocationSize;
+
     // Cached value for the buffer memory size limit.
     VkDeviceSize mMaxBufferMemorySizeLimit;
 
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 2beb301..d87b33c 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14088,27 +14088,28 @@
 // Test that the maximum texture layer can allocate enough memory.
 TEST_P(TextureCubeTestES32, MaxArrayTextureLayersVerify)
 {
-    GLint maxTextureLayers = 0;
-    GLTexture texture;
+    constexpr uint32_t kSize = 128;
 
+    GLTexture texture;
     glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
     ASSERT_GL_NO_ERROR();
 
+    GLint maxTextureLayers = -1;
     glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxTextureLayers);
     ASSERT_GL_NO_ERROR();
 
-    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 256, 256, maxTextureLayers, 0, GL_RGBA,
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, maxTextureLayers, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, nullptr);
     ASSERT_GL_NO_ERROR();
 
-    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 256, 256, maxTextureLayers + 1, 0, GL_RGBA,
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, maxTextureLayers + 1, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, nullptr);
     EXPECT_GL_ERROR(GL_INVALID_VALUE);
 
-    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 256, 256, maxTextureLayers);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, maxTextureLayers);
     ASSERT_GL_NO_ERROR();
 
-    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 256, 256, maxTextureLayers + 1);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, maxTextureLayers + 1);
     EXPECT_GL_ERROR(GL_INVALID_VALUE);
 }
 
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 2beb301..d87b33c 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -14088,27 +14088,28 @@
 // Test that the maximum texture layer can allocate enough memory.
 TEST_P(TextureCubeTestES32, MaxArrayTextureLayersVerify)
 {
-    GLint maxTextureLayers = 0;
-    GLTexture texture;
+    constexpr uint32_t kSize = 128;
 
+    GLTexture texture;
     glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
     ASSERT_GL_NO_ERROR();
 
+    GLint maxTextureLayers = -1;
     glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &maxTextureLayers);
     ASSERT_GL_NO_ERROR();
 
-    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 256, 256, maxTextureLayers, 0, GL_RGBA,
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, maxTextureLayers, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, nullptr);
     ASSERT_GL_NO_ERROR();
 
-    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 256, 256, maxTextureLayers + 1, 0, GL_RGBA,
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, maxTextureLayers + 1, 0, GL_RGBA,
                  GL_UNSIGNED_BYTE, nullptr);
     EXPECT_GL_ERROR(GL_INVALID_VALUE);
 
-    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 256, 256, maxTextureLayers);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, maxTextureLayers);
     ASSERT_GL_NO_ERROR();
 
-    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 256, 256, maxTextureLayers + 1);
+    glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, maxTextureLayers + 1);
     EXPECT_GL_ERROR(GL_INVALID_VALUE);
 }
Loading diff…

Original Bug Report

reported by oj...@gmail.com

Heap buffer overflow in ANGLE copyImageDataToBuffer


Report description

Heap buffer overflow in ANGLE copyImageDataToBuffer


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium


The problem

Please describe the technical details of the vulnerability

Description

Commit a08731cf (Bug: chromium:485622239) fixed a 32-bit integer overflow in the destination buffer size calculation in TextureVk::reinitImageAsRenderable() at TextureVk.cpp:3170 by splitting the multiplication into two stages with static_assert guards.

That same function also calls ImageHelper::copyImageDataToBuffer() at TextureVk.cpp:3155, which does its own staging buffer allocation with the same kind of unchecked 32-bit multiply at vk_helpers.cpp:10788:

uint32_t pixelBytes = imageFormat.pixelBytes;
size_t bufferSize =
    sourceArea.width * sourceArea.height * sourceArea.depth * pixelBytes * layerCount;

Every operand here is 32-bit (int or uint32_t), so the product wraps before it gets assigned to the 64-bit size_t. When the true result hits 2^32 the value becomes zero, a zero-byte staging buffer gets allocated, and the following vkCmdCopyImageToBuffer writes the full image data into it, overflowing the heap.

In short: the fix patched the destination buffer (caller) but missed the source buffer (callee).

Steps to Reproduce

Tested on Windows 11 with Chrome 145 Stable. The PoC allocates a 4 GiB texture, so the Vulkan driver must support a single allocation of that size. Most discrete GPUs (e.g. NVIDIA RTX 4050, 6 GB VRAM) cap maxMemoryAllocationSize just under 4 GiB, causing texStorage3D to return GL_OUT_OF_MEMORY before the vulnerable path is reached. On laptops with an Intel Iris Xe iGPU, the Vulkan driver allocates from system RAM and supports allocations up to ~16 GiB, which is enough.

  1. Place poc.html in a directory and start a local web server (PowerShell):
cd path\to\poc
python -m http.server 8765
  1. Open a second PowerShell window. Force Chrome to use the Intel Vulkan driver and launch with logging to file:
$env:VK_ICD_FILENAMES = "C:\Windows\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_9741ef1f4093481f\igvk64.json"

New-Item -ItemType Directory -Force "$env:TEMP\chrome-vulkan-test" | Out-Null

& "C:\Program Files\Google\Chrome\Application\chrome.exe" --use-angle=vulkan --disable-gpu-sandbox --no-sandbox --user-data-dir="$env:TEMP\chrome-vulkan-test" --no-first-run --enable-logging --log-file="$env:TEMP\chrome-vulkan-test\chrome.log" http://localhost:8765/poc.html

The VK_ICD_FILENAMES path is for the Intel driver on the test system. On other machines, find it under C:\Windows\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_*\igvk64.json.

  1. The page will go blank within a few seconds (GPU process crash). Wait ~10 seconds, then check the log:
Select-String "GPU process exited unexpectedly" "$env:TEMP\chrome-vulkan-test\chrome.log"

Expected output:

[ERROR:gpu_process_host.cc:996] GPU process exited unexpectedly: exit_code=-1073741819

-1073741819 is 0xC0000005 (STATUS_ACCESS_VIOLATION).

  1. Crashpad also writes a minidump to the user-data-dir:
ls "$env:TEMP\chrome-vulkan-test\Crashpad\reports\*.dmp"

Minidump from test system:

ExceptionCode:  EXCEPTION_ACCESS_VIOLATION (0xC0000005)
Access type:    WRITE
Faulting addr:  libglesv2.dll + 0x0044A120
Write target:   0x1B1D28C0000 (unmapped, past end of staging buffer)

What the PoC does: it creates a TEXTURE_2D_ARRAY (RGB8, 2048x2048, 256 layers). ANGLE stores RGB8 as RGBA8 (4 bytes/pixel) on Vulkan since RGB8 is not natively renderable. Attaching layer 0 to an FBO and calling gl.clear() forces reinitImageAsRenderable, which calls copyImageDataToBuffer. The buffer size wraps: 2048 * 2048 * 1 * 4 * 256 = 2^32 = 0.

Control tests

Test Result
poc.html (2048x2048x256, overflows) GPU crash, exit_code=-1073741819
about:blank (no WebGL) No crash
poc_safe.html (256x256x4, no overflow) No crash

Root Cause

Looking at the commit diff, the fix only touches TextureVk.cpp (the destination buffer allocation). ImageHelper::copyImageDataToBuffer in vk_helpers.cpp (the source buffer allocation) was left as-is and still does the whole multiplication in one 32-bit expression.

GPU memory note

The PoC needs a single Vulkan allocation of exactly 4 GiB (2^32 bytes). Many discrete GPUs cap maxMemoryAllocationSize just below that (the RTX 4050 caps at 0xffe00000, ~2 MiB short). The Intel Iris Xe iGPU allocates from system RAM with a limit of ~16 GiB, so it works on any machine with enough free RAM. On Linux or systems without an Intel iGPU, lowering IMPLEMENTATION_MAX_2D_TEXTURE_SIZE in src/libANGLE/Constants.h to 256 and rebuilding ANGLE makes the overflow trigger with only a few hundred KiB.

Impact analysis

A web page can cause a heap buffer overflow in Chrome’s GPU process through WebGL 2. The data written past the buffer comes from texture contents, which the page controls through texSubImage3D. The GPU process runs at higher privilege than the renderer.


The cause

What version of Chrome have you found the security issue in?

Chrome 145.0.7632.110 Stable, Windows 11 x86_64

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

M. Fauzan Wijaya (Gh05t666nero)

View on issue tracker