Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in ANGLE
DescriptionInteger overflow in ANGLE
ComponentANGLE
Bug ClassInteger Overflow
Tracker487208468
Fix commit56c952c65e74 (angle/angle) +31/-24
CISA KEVNot listed
Creditedheesun
Disclosed2026-03-18

Files Changed

  • src/libANGLE/renderer/vulkan/TextureVk.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
From 56c952c65e74493b4a6ce3c0d98f072e46651db3 Mon Sep 17 00:00:00 2001
From: Amirali Abdolrashidi <[email protected]>
Date: Wed, 04 Mar 2026 12:14:08 -0800
Subject: [PATCH] Vulkan: Cast size calculations to reduce overflow

* Updated or cast some extents used in size calculations to
  size_t to reduce the possibility of 32-bit overflow if the
  sizes are too large.

Bug: chromium:487208468
Change-Id: I48a8e14b2d9fd4ceb967f9fd66e9ebc43a78a391
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7633508
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Amirali Abdolrashidi <[email protected]>
---

diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index cb5c521..e0aacab 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -1777,8 +1777,9 @@
     const angle::Format &srcTextureFormat = source->getImage().getActualFormat();
     const angle::Format &dstTextureFormat =
         dstVkFormat.getActualImageFormat(getRequiredFormatSupport());
-    size_t destinationAllocationSize =
-        sourceBox.width * sourceBox.height * sourceBox.depth * dstTextureFormat.pixelBytes;
+    const size_t destinationAllocationSize =
+        static_cast<size_t>(sourceBox.width) * static_cast<size_t>(sourceBox.height) *
+        static_cast<size_t>(sourceBox.depth) * dstTextureFormat.pixelBytes;
 
     // Allocate memory in the destination texture for the copy/conversion
     uint32_t stagingBaseLayer =
@@ -2885,9 +2886,9 @@
                                               &bufferHelper.get(), &imageData));
 
     const angle::Format &angleFormat = mImage->getActualFormat();
-    GLuint sourceRowPitch            = baseLevelExtents.width * angleFormat.pixelBytes;
-    GLuint sourceDepthPitch          = sourceRowPitch * baseLevelExtents.height;
-    size_t baseLevelAllocationSize   = sourceDepthPitch * baseLevelExtents.depth;
+    const size_t sourceRowPitch          = baseLevelExtents.width * angleFormat.pixelBytes;
+    const size_t sourceDepthPitch        = sourceRowPitch * baseLevelExtents.height;
+    const size_t baseLevelAllocationSize = sourceDepthPitch * baseLevelExtents.depth;
 
     // We now have the base level available to be manipulated in the imageData pointer. Generate all
     // the missing mipmaps with the slow path. For each layer, use the copied data to generate all
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index f32774f..94a7641 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -8817,11 +8817,11 @@
                 const VkBufferImageCopy &copy = update.data.buffer.copyRegion;
 
                 // Source and dst data are tightly packed
-                GLuint srcDataRowPitch = copy.imageExtent.width * srcFormat.pixelBytes;
-                GLuint dstDataRowPitch = copy.imageExtent.width * dstFormat.pixelBytes;
+                const size_t srcDataRowPitch = copy.imageExtent.width * srcFormat.pixelBytes;
+                const size_t dstDataRowPitch = copy.imageExtent.width * dstFormat.pixelBytes;
 
-                GLuint srcDataDepthPitch = srcDataRowPitch * copy.imageExtent.height;
-                GLuint dstDataDepthPitch = dstDataRowPitch * copy.imageExtent.height;
+                const size_t srcDataDepthPitch = srcDataRowPitch * copy.imageExtent.height;
+                const size_t dstDataDepthPitch = dstDataRowPitch * copy.imageExtent.height;
 
                 // Retrieve source buffer
                 vk::BufferHelper *srcBuffer = update.data.buffer.bufferHelper;
@@ -9411,8 +9411,9 @@
     {
         // When a conversion is required, we need to use the loadFunction to read from a temporary
         // buffer instead so its an even slower path.
-        size_t bufferSize =
-            storageFormat.pixelBytes * clippedRectangle.width * clippedRectangle.height;
+        const size_t bufferSize = static_cast<size_t>(clippedRectangle.width) *
+                                  static_cast<size_t>(clippedRectangle.height) *
+                                  storageFormat.pixelBytes;
         angle::MemoryBuffer *memoryBuffer = nullptr;
         ANGLE_VK_CHECK_ALLOC(contextVk, context->getScratchBuffer(bufferSize, &memoryBuffer));
 
@@ -11229,33 +11230,38 @@
         ASSERT(depthOffset > 0 || stencilOffset > 0);
         ASSERT(depthOffset + depthFormat.depthBits / 8 <= readFormat.pixelBytes);
         ASSERT(stencilOffset + stencilFormat.stencilBits / 8 <= readFormat.pixelBytes);
+        const size_t areaWidth  = static_cast<size_t>(area.width);
+        const size_t areaHeight = static_cast<size_t>(area.height);
 
         // Read the depth values, tightly-packed
         angle::MemoryBuffer depthBuffer;
-        ANGLE_VK_CHECK_ALLOC(contextVk,
-                             depthBuffer.resize(depthFormat.pixelBytes * area.width * area.height));
-        ANGLE_TRY(
-            readPixelsImpl(contextVk, area,
-                           PackPixelsParams(area, depthFormat, depthFormat.pixelBytes * area.width,
-                                            false, nullptr, 0),
-                           VK_IMAGE_ASPECT_DEPTH_BIT, levelGL, layer, depthBuffer.data()));
+        const size_t outputDepthPitch = areaWidth * depthFormat.pixelBytes;
+        const size_t depthBufferSize  = outputDepthPitch * areaHeight;
+        ANGLE_VK_CHECK_ALLOC(contextVk, depthBuffer.resize(depthBufferSize));
+        ANGLE_TRY(readPixelsImpl(
+            contextVk, area,
+            PackPixelsParams(area, depthFormat, static_cast<GLuint>(outputDepthPitch), false,
+                             nullptr, 0),
+            VK_IMAGE_ASPECT_DEPTH_BIT, levelGL, layer, depthBuffer.data()));
 
         // Read the stencil values, tightly-packed
         angle::MemoryBuffer stencilBuffer;
-        ANGLE_VK_CHECK_ALLOC(
-            contextVk, stencilBuffer.resize(stencilFormat.pixelBytes * area.width * area.height));
+        const size_t outputStencilPitch = areaWidth * stencilFormat.pixelBytes;
+        const size_t stencilBufferSize  = outputStencilPitch * areaHeight;
+        ANGLE_VK_CHECK_ALLOC(contextVk, stencilBuffer.resize(stencilBufferSize));
         ANGLE_TRY(readPixelsImpl(
             contextVk, area,
-            PackPixelsParams(area, stencilFormat, stencilFormat.pixelBytes * area.width, false,
+            PackPixelsParams(area, stencilFormat, static_cast<GLuint>(outputStencilPitch), false,
                              nullptr, 0),
             VK_IMAGE_ASPECT_STENCIL_BIT, levelGL, layer, stencilBuffer.data()));
 
         // Interleave them together
         angle::MemoryBuffer readPixelBuffer;
-        ANGLE_VK_CHECK_ALLOC(
-            contextVk, readPixelBuffer.resize(readFormat.pixelBytes * area.width * area.height));
+        const size_t readPixelArea       = areaWidth * areaHeight;
+        const size_t readPixelBufferSize = readPixelArea * readFormat.pixelBytes;
+        ANGLE_VK_CHECK_ALLOC(contextVk, readPixelBuffer.resize(readPixelBufferSize));
         readPixelBuffer.fill(0);
-        for (int i = 0; i < area.width * area.height; i++)
+        for (size_t i = 0; i < readPixelArea; i++)
         {
             uint8_t *readPixel = readPixelBuffer.data() + i * readFormat.pixelBytes;
             memcpy(readPixel + depthOffset, depthBuffer.data() + i * depthFormat.pixelBytes,
Loading diff…

Original Bug Report

reported by [email protected]

Integer overflow in ANGLE TextureVk::reinitImageAsRenderable leads to heap buffer overflow in GPU process

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.


VULNERABILITY DETAILS

Integer overflow in ANGLE TextureVk::reinitImageAsRenderable() leads to heap buffer overflow in the GPU process via WebGL2.

When a WebGL2 TEXTURE_2D_ARRAY with RGB16F internal format is bound as a framebuffer attachment, ANGLE’s Vulkan backend converts the texture from sample-only format (R16G16B16_FLOAT, 6 bytes/pixel) to renderable format (R16G16B16A16_FLOAT, 8 bytes/pixel). The destination staging buffer size calculation at TextureVk.cpp:3167-3168 uses mixed-type integer arithmetic that silently overflows uint32, producing a zero-byte allocation. The subsequent CopyImageCHROMIUM call writes up to 4 GB of attacker-supplied texture data into this ~64-byte buffer, causing a heap buffer overflow in the GPU process.

This is triggerable from any web page using WebGL2 JavaScript, with no user interaction beyond visiting the page.

ROOT CAUSE

File: third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp Function: TextureVk::reinitImageAsRenderable() Lines: 3167-3168

Vulnerable code:

size_t dstBufferSize = sourceBox.width * sourceBox.height * sourceBox.depth *
                       dstFormat.pixelBytes * layerCount;

C++ type promotion analysis (left-to-right, usual arithmetic conversions):

Step 1: sourceBox.width (int) * sourceBox.height (int) -> int 16384 * 16384 = 268,435,456 (fits in int32)

Step 2: result (int) * sourceBox.depth (int) -> int 268,435,456 * 1 = 268,435,456 (fits in int32)

Step 3: result (int) * dstFormat.pixelBytes (GLuint/unsigned int) -> unsigned int int promoted to unsigned int per C++ [conv.rank]: 268,435,456u * 8u = 2,147,483,648 (fits in uint32)

Step 4: result (unsigned int) * layerCount (uint32_t) -> unsigned int 2,147,483,648u * 2u = 4,294,967,296 = 2^32 WRAPS TO 0 (unsigned overflow is defined behavior per C++ [basic.fundamental])

Step 5: 0u zero-extended to size_t -> dstBufferSize = 0

Chromium builds with -fno-strict-overflow (build/config/compiler/BUILD.gn), making the signed multiplication at step 1 deterministic (two’s complement). The uint32 wrap at step 4 is defined behavior per the C++ standard.

The overflowed dstBufferSize (0) is passed to stageSubresourceUpdateAndGetData() at line 3172, which allocates a buffer of ~64 bytes (imageCopyAlignment padding, see ContextVk.cpp:7093). CopyImageCHROMIUM at lines 3192-3197 then writes width * height * dstPixelBytes * layerCount = 4,294,967,296 bytes into this 64-byte buffer.

Additionally, both the source buffer allocation (vk_helpers.cpp:10747-10748) and all pitch calculations (TextureVk.cpp:3177-3184, GLuint type) contain the same overflow pattern, causing both GPU-side and CPU-side out-of-bounds access.

A standalone C program (verify_overflow.c) is attached that reproduces the exact type promotion chain and confirms the overflow to zero.

TRIGGER PATH (WebGL2 JavaScript -> GPU process heap overflow)

  1. Create a TEXTURE_2D_ARRAY with gl.RGB16F internal format

    • ANGLE maps to VK_FORMAT_R16G16B16_SFLOAT on Vulkan
    • On NVIDIA/AMD, this format supports sampling but NOT rendering
    • SampleOnly format: R16G16B16_FLOAT (pixelBytes = 6)
    • Renderable fallback: R16G16B16A16_FLOAT (pixelBytes = 8)
  2. Choose dimensions where width * height * 8 * layers = 2^32:

    • 16384 x 16384 x 2 layers (requires ~3.2 GB VRAM)
    • 8192 x 8192 x 8 layers (requires ~3.2 GB VRAM)
    • 4096 x 4096 x 32 layers (requires ~3.2 GB VRAM)
  3. Bind texture to framebuffer via gl.framebufferTextureLayer() -> sets mState.hasBeenBoundAsAttachment() = true

  4. Issue a draw call (gl.drawArrays) to trigger syncState(): -> TextureVk.cpp:3840: respecifyImageStorageIfNecessary() -> Line 3641: checks hasBeenBoundAsAttachment() -> Line 3644: ensureRenderable() -> ensureRenderableWithFormat() -> Line 4835: sets mRequiredFormatSupport = Renderable -> Line 4907: respecifyImageStorage() -> Line 3256-3258: detects format mismatch (R16G16B16_FLOAT != R16G16B16A16_FLOAT) -> Line 3260: reinitImageAsRenderable()

  5. In reinitImageAsRenderable() multi-layer slow path (line 3138): -> Line 3111: layerCount > 1, takes CPU copy path (not draw path) -> Line 3158: copyImageDataToBuffer() reads GPU texture to srcBuffer -> Line 3167: dstBufferSize = 1638416384182 = 2^32 -> WRAPS TO 0 -> Line 3172: stageSubresourceUpdateAndGetData() allocates ~64 bytes -> Lines 3192-3197: CopyImageCHROMIUM writes 4 GB to 64-byte buffer -> HEAP BUFFER OVERFLOW

ADDITIONAL OVERFLOW INSTANCES (same unchecked pattern)

The ANGLE Vulkan renderer contains at least 15 instances of the same vulnerability pattern (unchecked integer multiplication for buffer sizing). None use CheckedNumeric or any overflow validation. Key locations:

a) vk_helpers.cpp:10747-10748 - copyImageDataToBuffer() size_t bufferSize = sourceArea.width * sourceArea.height * sourceArea.depth * pixelBytes * layerCount; Overflows the SOURCE staging buffer; called from the same trigger path.

b) TextureVk.cpp:1781-1782 - copySubTextureImpl() size_t destinationAllocationSize = sourceBox.width * sourceBox.height * sourceBox.depth * dstTextureFormat.pixelBytes; Reachable via gl.copyTexSubImage3D() with cross-format textures.

c) TextureVk.cpp:2891-2893 - generateMipmapsWithCPU() GLuint sourceRowPitch = baseLevelExtents.width * angleFormat.pixelBytes; GLuint sourceDepthPitch = sourceRowPitch * baseLevelExtents.height; size_t baseLevelAllocationSize = sourceDepthPitch * baseLevelExtents.depth; GLuint pitch overflows, truncated value used for buffer offset calculation.

d) TextureVk.cpp:1812-1816 - copySubTextureImpl() pitches GLuint srcDataRowPitch/dstDataRowPitch/srcDataDepthPitch/dstDataDepthPitch Same GLuint overflow pattern in pitch calculations.

e) vk_helpers.cpp:8792-8796 - reformatStagedBufferUpdates() Same GLuint row/depth pitch overflow pattern.

f) vk_helpers.cpp:9387 - readPixelsImpl() conversion path g) vk_helpers.cpp:11195-11215 - readPixelsImpl() depth/stencil path h) vk_helpers.cpp:11455 - readPixelsImpl() allocation i) SurfaceVk.cpp:284-286 - readPixels() GLuint rowStride overflow

IMPACT

  • Heap buffer overflow in Chrome’s GPU process, triggered from WebGL2 JS
  • Attacker-supplied texture data (uploaded via texSubImage3D) is written past the staging buffer bounds via CopyImageCHROMIUM
  • Up to 4 GB written to a ~64-byte buffer
  • No user interaction required beyond visiting a malicious web page
  • The GPU process has a more permissive sandbox than the renderer process (weaker seccomp-bpf filter on Linux, broader syscall allowlist)

AFFECTED PLATFORMS

Requires a GPU where VK_FORMAT_R16G16B16_SFLOAT supports sampling (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) but NOT rendering (no VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT):

  • NVIDIA GPUs on Linux and Windows (nvidia-driver-535+): AFFECTED
  • AMD GPUs on Linux (RADV/AMDVLK) and Windows: AFFECTED
  • macOS (Metal backend): NOT affected (Metal uses RGBA16F for both)
  • Mobile/Android: NOT affected (most mobile GPUs differ)

Additional requirements:

  • MAX_TEXTURE_SIZE >= 4096 (universal on desktop GPUs)
  • VRAM >= ~3.2 GB (common on modern desktop GPUs, GTX 1070+)
  • Chrome using ANGLE Vulkan backend (default on Linux)

SUGGESTED FIX

  1. Use checked arithmetic for dstBufferSize (TextureVk.cpp:3167-3168):

    base::CheckedNumeric<size_t> dstBufferSize = base::CheckedNumeric<size_t>(sourceBox.width) * sourceBox.height * sourceBox.depth * dstFormat.pixelBytes * layerCount; if (!dstBufferSize.IsValid()) { return angle::Result::Stop; }

  2. Fix copySubTextureImpl allocation (TextureVk.cpp:1781-1782):

    base::CheckedNumeric<size_t> destinationAllocationSize = base::CheckedNumeric<size_t>(sourceBox.width) * sourceBox.height * sourceBox.depth * dstTextureFormat.pixelBytes;

  3. Change pitch types from GLuint to size_t (TextureVk.cpp:3177-3184):

    size_t srcDataRowPitch = static_cast<size_t>(sourceBox.width) * srcFormat.pixelBytes; size_t dstDataRowPitch = static_cast<size_t>(sourceBox.width) * dstFormat.pixelBytes; size_t srcDataDepthPitch = srcDataRowPitch * sourceBox.height; size_t dstDataDepthPitch = dstDataRowPitch * sourceBox.height;

  4. Apply the same pattern to all 15 affected locations listed above.

VERSION

Chrome Version: 147.0.7682.0 (Developer Build, 64-bit) ANGLE: Vulkan backend Operating System: Linux or Windows with NVIDIA/AMD Vulkan GPU

The vulnerable code has been present since at least Chrome 100 (reinitImageAsRenderable was introduced for format fallback support).

REPRODUCTION CASE

Prerequisites:

  • Linux system with NVIDIA or AMD GPU (Vulkan driver installed)
  • GPU VRAM >= 3.2 GB
  • Chrome with ANGLE Vulkan backend (default on Linux)

To verify the format prerequisite, run: vulkaninfo | grep -A5 “VK_FORMAT_R16G16B16_SFLOAT” -> VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT should be present -> VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT should be ABSENT

Steps:

  1. Serve poc.html locally: python3 -m http.server 8080

  2. Open Chrome: chrome –use-angle=vulkan http://localhost:8080/poc.html

  3. Click “Check Environment” to verify:

    • WebGL2 is available
    • RGB16F is NOT renderable (FBO status is INCOMPLETE)
    • “This system is vulnerable to the overflow” message appears
  4. Click “Run PoC” to trigger the overflow:

    • Expected: GPU process crashes, WebGL context is lost

For ASAN builds (recommended for clear crash report): gn gen out/asan –args=' is_asan = true is_debug = false is_component_build = false dcheck_always_on = true target_cpu = “x64” angle_enable_vulkan = true ' autoninja -C out/asan chrome out/asan/chrome –use-angle=vulkan –no-sandbox –disable-gpu-sandbox
http://localhost:8080/poc.html

Expected ASAN output: ==GPU_PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x… WRITE of size … at 0x… thread T… #0 in angle::CopyImageCHROMIUM(…) #1 in rx::TextureVk::reinitImageAsRenderable(…) #2 in rx::TextureVk::respecifyImageStorage(…)

Standalone arithmetic verification (no GPU required): cc -o verify_overflow verify_overflow.c && ./verify_overflow This confirms dstBufferSize overflows to 0 using the exact C++ type promotion rules, independent of any hardware.

If the system has < 3.2 GB free VRAM, texStorage3D will fail with OUT_OF_MEMORY. Use a GPU with more VRAM, or adjust dimensions (any combination where width * height * 8 * layers >= 2^32 triggers the overflow).

FOR CRASHES

Type of crash: GPU process crash (not tab, not browser) Crash state (from source analysis, ASAN build on affected hardware required for runtime stack trace):

Expected crash stack: angle::CopyImageCHROMIUM(…) <- writes 4 GB to 64-byte buffer rx::TextureVk::reinitImageAsRenderable(…) <- TextureVk.cpp:3192 rx::TextureVk::respecifyImageStorage(…) <- TextureVk.cpp:3260 rx::TextureVk::respecifyImageStorageIfNecessary(…) <- TextureVk.cpp:3641 rx::TextureVk::syncState(…) <- TextureVk.cpp:3840

Root cause frame: TextureVk.cpp:3167-3168 size_t dstBufferSize = sourceBox.width * sourceBox.height * sourceBox.depth * dstFormat.pixelBytes * layerCount; // Evaluates to 0 due to uint32 overflow (1638416384182 = 2^32 -> 0)

Allocation frame: TextureVk.cpp:3172 stageSubresourceUpdateAndGetData(contextVk, dstBufferSize=0, …) // Allocates ~64 bytes (imageCopyAlignment padding)

Overflow frame: TextureVk.cpp:3192-3197 CopyImageCHROMIUM(srcData, …, dstData, …, width=16384, height=16384, …) // Writes 16384 * 16384 * 8 = 2,147,483,648 bytes PER LAYER to 64-byte dstData

The GPU process will crash with SIGSEGV (release/debug build) or report heap-buffer-overflow (ASAN build). In a release build, the GPU process restarts and the WebGL context is reported as lost.

ATTACHED FILES

  1. poc.html - Self-contained WebGL2 PoC with environment check
  2. verify_overflow.c - Standalone C program proving the integer overflow (reproduces exact C++ type promotion chain)

CREDIT INFORMATION

Reporter credit: heesun

View on issue tracker