CVE-2026-3536
Overview
Background
- ANGLE
- Chrome’s translation layer that implements OpenGL ES on top of native backends such as Vulkan; the affected code lives in its Vulkan renderer.
- `rx::TextureVk`
- the Vulkan-backend texture object that manages GPU image storage and the CPU-side staging buffers used for copies and readbacks.
- `gl::Box`
- a rectangular region descriptor whose
width,height, anddepthmembers are signed 32-bit ints (GLint), used here as the source extent of a copy. - Staging buffer
- an intermediate host-visible allocation sized to hold the pixel bytes transferred between a texture and CPU/GPU memory during a copy or readback.
Root Cause Analysis
In the copy/readback path, the destination staging-buffer size was computed as sourceBox.width * sourceBox.height * sourceBox.depth * dstFormat.pixelBytes * layerCount, where the leading operands are signed 32-bit gl::Box ints and dstFormat.pixelBytes/layerCount are also narrow types. Because C++ integer promotion keeps this arithmetic in 32-bit (int) until the final assignment, the product of all factors is truncated modulo 2^32 before it is widened to size_t. For a large 2D-array texture — for example ~1GB per layer across 4 layers, ~4GB total — the true byte count exceeds INT32_MAX, so the 32-bit multiplication overflows and yields a dstBufferSize far smaller than the real transfer size. The subsequent allocation reserves that undersized buffer, while the actual copy engine writes the full (untruncated) amount of pixel data, producing a heap buffer overflow in the GPU process.
The fix builds the product stepwise in size_t (first width * height, then multiplying in the remaining factors) and adds static_asserts bounding the intermediate products by ANGLE’s IMPLEMENTATION_MAX_* texture-size constants so no intermediate can overflow before promotion.
size_t, so simply assigning to a size_t was too late to be safe.Attack Path
- Allocate an oversized texture
Content creates a 2D-array (or otherwise large) texture whose total byte size exceeds
INT32_MAXvia WebGL/WebGPU-exposed GL ES calls. - Trigger the copy/readback
A
copySubImage/CPU-readback operation drivesrx::TextureVk::copySubImageImplWithDraw, computingdstBufferSizeover the full sourcegl::Boxand layer count. - Integer overflow truncates the size
The 32-bit multiplication wraps, so
dstBufferSizeis far smaller than the true transfer size. - Undersized allocation A staging buffer of the truncated size is allocated for the destination.
- Heap buffer overflow The copy writes the full pixel payload into the undersized buffer, corrupting adjacent GPU-process heap memory.
Impact Assessment
Files Changed
src/libANGLE/renderer/vulkan/TextureVk.cppsrc/tests/gl_tests/FramebufferTest.cppsrc/tests/gl_tests/VulkanImageTest.cpputil/shader_utils.cpp
Audit Directions
- Pre-promotion integer overflowFlag size/byte computations that multiply narrow signed operands (e.g.
gl::Boxmembers,pixelBytes,layerCount) and only widen at assignment; force the first operand or a cast intosize_tso the whole chain is 64-bit. - Buffer size vs. transfer size mismatchAudit allocation paths where a computed buffer size is used for
allocwhile the actual copy length is derived separately, ensuring both use the same overflow-safe arithmetic. - Bounded-input assumptionsWhere
IMPLEMENTATION_MAX_*constants are relied on to keep products in range, require explicitstatic_asserts (or runtime checks) so future limit increases cannot silently reintroduce overflow.
Patch
From a08731cf6d70c60fd74b1d75f2e8b94c52e18140 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Thu, 19 Feb 2026 14:42:08 -0500
Subject: [PATCH] Vulkan: Avoid overflow in texture size calculation
Bug: chromium:485622239
Change-Id: Idf9847afa0aa2e72b6433ac8348ae2820c1ad8c5
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7595734
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 9e208f9..e2185a4 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -3164,8 +3164,17 @@
// invalidate must be called after wait for finish.
ANGLE_TRY(srcBuffer->invalidate(renderer));
- size_t dstBufferSize = sourceBox.width * sourceBox.height * sourceBox.depth *
- dstFormat.pixelBytes * layerCount;
+ // 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;
// Allocate memory in the destination texture for the copy/conversion.
uint8_t *dstData = nullptr;
diff --git a/src/tests/gl_tests/FramebufferTest.cpp b/src/tests/gl_tests/FramebufferTest.cpp
index 020a041..f72f1a7 100644
--- a/src/tests/gl_tests/FramebufferTest.cpp
+++ b/src/tests/gl_tests/FramebufferTest.cpp
@@ -8894,6 +8894,62 @@
ASSERT_GL_NO_ERROR();
}
+// Test that 2D array texture size calculation doesn't overflow internally when rendering to it. An
+// RGB format is used which is often emualted with RGBA.
+//
+// Practically we cannot run this test. On most configurations, allocating a 4GB texture fails due
+// to internal driver limitations. On the few configs that the test actually runs, allocating such
+// large memory leads to instability.
+TEST_P(FramebufferTest_ES3, DISABLED_MaxSize2DArrayNoOverflow)
+{
+ GLint maxTexture2DSize;
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexture2DSize);
+
+ maxTexture2DSize = std::min(maxTexture2DSize, 16384);
+
+ // Create a 2D array texture with RGB format. Every layer is going to take 1GB of memory (if
+ // emulated with RGBA), so only create 4 layers of it (for a total of 4GB of memory). If 32-bit
+ // math is involved when calculating sizes related to this texture, they will overflow.
+ constexpr uint32_t kLayers = 4;
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D_ARRAY, tex);
+ glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGB8, maxTexture2DSize, maxTexture2DSize, kLayers);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Initialize the texture so its content is considered valid and worth preserving.
+ constexpr int kValidSubsectionWidth = 16;
+ constexpr int kValidSubsectionHeight = 20;
+ std::vector<GLColorRGB> data(kValidSubsectionWidth * kValidSubsectionHeight,
+ GLColorRGB(0, 255, 0));
+ for (uint32_t layer = 0; layer < kLayers; ++layer)
+ {
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kValidSubsectionWidth,
+ kValidSubsectionHeight, 1, GL_RGB, GL_UNSIGNED_BYTE, data.data());
+ }
+
+ // Draw with the texture, making sure it's initialized and data is flushed.
+ ANGLE_GL_PROGRAM(drawTex2DArray, essl3_shaders::vs::Texture2DArray(),
+ essl3_shaders::fs::Texture2DArray());
+ drawQuad(drawTex2DArray, essl3_shaders::PositionAttrib(), 0.5f);
+
+ // Bind a framebuffer to the texture and render into it. In some backends, the texture is
+ // recreated to RGBA to be renderable.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, 1);
+
+ ANGLE_GL_PROGRAM(drawRed, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glViewport(0, 0, kValidSubsectionWidth / 2, kValidSubsectionHeight);
+ drawQuad(drawRed, essl1_shaders::PositionAttrib(), 0.5f);
+
+ EXPECT_PIXEL_RECT_EQ(0, 0, kValidSubsectionWidth / 2, kValidSubsectionHeight, GLColor::red);
+ EXPECT_PIXEL_RECT_EQ(kValidSubsectionWidth / 2, 0,
+ kValidSubsectionWidth - kValidSubsectionWidth / 2, kValidSubsectionHeight,
+ GLColor::green);
+ ASSERT_GL_NO_ERROR();
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND(AddMockTextureNoRenderTargetTest,
ES2_D3D9().enable(Feature::AddMockTextureNoRenderTarget),
ES2_D3D11().enable(Feature::AddMockTextureNoRenderTarget));
diff --git a/src/tests/gl_tests/VulkanImageTest.cpp b/src/tests/gl_tests/VulkanImageTest.cpp
index 2f06e2d..87e7482 100644
--- a/src/tests/gl_tests/VulkanImageTest.cpp
+++ b/src/tests/gl_tests/VulkanImageTest.cpp
@@ -677,8 +677,8 @@
kTextureHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE, textureColor.data());
}
- ANGLE_GL_PROGRAM(drawTex2DArray, essl1_shaders::vs::Texture2DArray(),
- essl1_shaders::fs::Texture2DArray());
+ ANGLE_GL_PROGRAM(drawTex2DArray, essl3_shaders::vs::Texture2DArray(),
+ essl3_shaders::fs::Texture2DArray());
drawQuad(drawTex2DArray, essl1_shaders::PositionAttrib(), 0.5f);
// Fill up the device memory until we start allocating on the system memory.
diff --git a/util/shader_utils.cpp b/util/shader_utils.cpp
index 275e261..8994612 100644
--- a/util/shader_utils.cpp
+++ b/util/shader_utils.cpp
@@ -580,18 +580,6 @@
})";
}
-const char *Texture2DArray()
-{
- return R"(#version 300 es
-out vec2 v_texCoord;
-in vec4 a_position;
-void main()
-{
- gl_Position = vec4(a_position.xy, 0.0, 1.0);
- v_texCoord = (a_position.xy * 0.5) + 0.5;
-})";
-}
-
} // namespace vs
namespace fs
@@ -689,20 +677,6 @@
})";
}
-const char *Texture2DArray()
-{
- return R"(#version 300 es
-precision highp float;
-uniform highp sampler2DArray tex2DArray;
-uniform int slice;
-in vec2 v_texCoord;
-out vec4 fragColor;
-void main()
-{
- fragColor = texture(tex2DArray, vec3(v_texCoord, float(slice)));
-})";
-}
-
} // namespace fs
} // namespace essl1_shaders
@@ -787,6 +761,18 @@
})";
}
+const char *Texture2DArray()
+{
+ return R"(#version 300 es
+out vec2 v_texCoord;
+in vec4 a_position;
+void main()
+{
+ gl_Position = vec4(a_position.xy, 0.0, 1.0);
+ v_texCoord = (a_position.xy * 0.5) + 0.5;
+})";
+}
+
} // namespace vs
namespace fs
@@ -844,6 +830,20 @@
})";
}
+const char *Texture2DArray()
+{
+ return R"(#version 300 es
+precision highp float;
+uniform highp sampler2DArray tex2DArray;
+uniform int slice;
+in vec2 v_texCoord;
+out vec4 fragColor;
+void main()
+{
+ fragColor = texture(tex2DArray, vec3(v_texCoord, float(slice)));
+})";
+}
+
} // namespace fs
Regression Test / PoC
diff --git a/src/tests/gl_tests/FramebufferTest.cpp b/src/tests/gl_tests/FramebufferTest.cpp
index 020a041..f72f1a7 100644
--- a/src/tests/gl_tests/FramebufferTest.cpp
+++ b/src/tests/gl_tests/FramebufferTest.cpp
@@ -8894,6 +8894,62 @@
ASSERT_GL_NO_ERROR();
}
+// Test that 2D array texture size calculation doesn't overflow internally when rendering to it. An
+// RGB format is used which is often emualted with RGBA.
+//
+// Practically we cannot run this test. On most configurations, allocating a 4GB texture fails due
+// to internal driver limitations. On the few configs that the test actually runs, allocating such
+// large memory leads to instability.
+TEST_P(FramebufferTest_ES3, DISABLED_MaxSize2DArrayNoOverflow)
+{
+ GLint maxTexture2DSize;
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexture2DSize);
+
+ maxTexture2DSize = std::min(maxTexture2DSize, 16384);
+
+ // Create a 2D array texture with RGB format. Every layer is going to take 1GB of memory (if
+ // emulated with RGBA), so only create 4 layers of it (for a total of 4GB of memory). If 32-bit
+ // math is involved when calculating sizes related to this texture, they will overflow.
+ constexpr uint32_t kLayers = 4;
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D_ARRAY, tex);
+ glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGB8, maxTexture2DSize, maxTexture2DSize, kLayers);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Initialize the texture so its content is considered valid and worth preserving.
+ constexpr int kValidSubsectionWidth = 16;
+ constexpr int kValidSubsectionHeight = 20;
+ std::vector<GLColorRGB> data(kValidSubsectionWidth * kValidSubsectionHeight,
+ GLColorRGB(0, 255, 0));
+ for (uint32_t layer = 0; layer < kLayers; ++layer)
+ {
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kValidSubsectionWidth,
+ kValidSubsectionHeight, 1, GL_RGB, GL_UNSIGNED_BYTE, data.data());
+ }
+
+ // Draw with the texture, making sure it's initialized and data is flushed.
+ ANGLE_GL_PROGRAM(drawTex2DArray, essl3_shaders::vs::Texture2DArray(),
+ essl3_shaders::fs::Texture2DArray());
+ drawQuad(drawTex2DArray, essl3_shaders::PositionAttrib(), 0.5f);
+
+ // Bind a framebuffer to the texture and render into it. In some backends, the texture is
+ // recreated to RGBA to be renderable.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, 1);
+
+ ANGLE_GL_PROGRAM(drawRed, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glViewport(0, 0, kValidSubsectionWidth / 2, kValidSubsectionHeight);
+ drawQuad(drawRed, essl1_shaders::PositionAttrib(), 0.5f);
+
+ EXPECT_PIXEL_RECT_EQ(0, 0, kValidSubsectionWidth / 2, kValidSubsectionHeight, GLColor::red);
+ EXPECT_PIXEL_RECT_EQ(kValidSubsectionWidth / 2, 0,
+ kValidSubsectionWidth - kValidSubsectionWidth / 2, kValidSubsectionHeight,
+ GLColor::green);
+ ASSERT_GL_NO_ERROR();
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND(AddMockTextureNoRenderTargetTest,
ES2_D3D9().enable(Feature::AddMockTextureNoRenderTarget),
ES2_D3D11().enable(Feature::AddMockTextureNoRenderTarget));
diff --git a/src/tests/gl_tests/VulkanImageTest.cpp b/src/tests/gl_tests/VulkanImageTest.cpp
index 2f06e2d..87e7482 100644
--- a/src/tests/gl_tests/VulkanImageTest.cpp
+++ b/src/tests/gl_tests/VulkanImageTest.cpp
@@ -677,8 +677,8 @@
kTextureHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE, textureColor.data());
}
- ANGLE_GL_PROGRAM(drawTex2DArray, essl1_shaders::vs::Texture2DArray(),
- essl1_shaders::fs::Texture2DArray());
+ ANGLE_GL_PROGRAM(drawTex2DArray, essl3_shaders::vs::Texture2DArray(),
+ essl3_shaders::fs::Texture2DArray());
drawQuad(drawTex2DArray, essl1_shaders::PositionAttrib(), 0.5f);
// Fill up the device memory until we start allocating on the system memory.
Original Bug Report
ANGLE Vulkan reinitImageAsRenderable uint32 Overflow causes GPU OOB Write
Report description
ANGLE Vulkan reinitImageAsRenderable uint32 Overflow causes GPU OOB Write
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?
The problem
Please describe the technical details of the vulnerability
TextureVk::reinitImageAsRenderable in the ANGLE Vulkan backend computes a staging buffer size using five operands that multiply in 32-bit arithmetic, overflowing before widening to size_t. When a WebGL2 page creates a non-renderable RGB8 TEXTURE_2D_ARRAY and calls copyTexSubImage3D, ANGLE detects that RGB8 is not natively renderable in Vulkan and converts the texture to RGBA8. The size calculation at TextureVk.cpp:3167 multiplies width, height, depth, pixelBytes, and layerCount in 32-bit arithmetic. For a 16384x16384x4-layer texture, the product is exactly 2^32 and wraps to 0. The staging buffer allocation receives the overflowed size (0). CopyImageCHROMIUM then writes 4GB of converted pixel data into it.
The attacker controls the source RGB8 pixel data. After conversion to RGBA8, the R, G, B channels are derived from the attacker’s data and the alpha channel is always 0xFF. The attacker controls 3 of every 4 bytes written during the overflow.
No compromised renderer is required. The overflow is triggered entirely from JavaScript via the WebGL2 API.
Affects Stable, Beta, and Dev. The vulnerable code has been present since September 2021. Tested on:
- 144.0.7559.133 (Stable, Windows)
- 146.0.7678.0 (ASan, Windows)
Affected Code
TextureVk.cpp, lines 3167-3168 in reinitImageAsRenderable:
size_t dstBufferSize = sourceBox.width * sourceBox.height * sourceBox.depth *
dstFormat.pixelBytes * layerCount;
All five operands (width, height, depth as int; pixelBytes, layerCount as uint32_t) multiply in 32-bit arithmetic. The result overflows before being stored into the 64-bit size_t. This value is passed to stageSubresourceUpdateAndGetData (line 3172), which calls initBufferForImageCopy to allocate a VMA staging buffer of the overflowed size. The CopyImageCHROMIUM loop at lines 3190-3197 writes the correct (non-overflowed) amount of data per layer, using pitch values from lines 3177-3184.
The pitch calculations at lines 3177-3184 use GLuint (uint32_t) and have the same overflow class, but they do not overflow at the dimensions required to trigger the size overflow.
Steps to Reproduce
- Launch Chrome with: –use-angle=vulkan
- Navigate to the attached poc.html
- The PoC auto-fires on page load
For ASan builds, also pass –in-process-gpu –disable-features=SkiaGraphite –disable-gpu-compositing. The latter two flags prevent an unrelated Dawn/D3D11 initialization failure in ASan builds when ANGLE is set to Vulkan; they are not related to the vulnerability, but were required in my testing.
ASan output (Chrome 146.0.7678.0, Windows, Intel HD 620):
==17556==ERROR: AddressSanitizer: access-violation on unknown address 0x120973c50000
==17556==The signal is caused by a WRITE memory access.
#0 angle::R8G8B8A8SRGB::writeColor imageformats.cpp:392
#1 rx::CopyImageCHROMIUM renderer_utils.cpp:795
#2 rx::TextureVk::reinitImageAsRenderable TextureVk.cpp:3192
#3 rx::TextureVk::respecifyImageStorage TextureVk.cpp:3260
#4 rx::TextureVk::ensureRenderableWithFormat TextureVk.cpp:4907
#5 rx::TextureVk::ensureRenderableIfCopyTexImage.. TextureVk.cpp:4968
#6 rx::TextureVk::copySubImage TextureVk.cpp:1479
#7 gl::Texture::copySubImage Texture.cpp:1613
#8 gl::Context::copyTexSubImage3D Context.cpp:5139
rax = 41 rcx = 120973c50000
RAX holds the attacker’s marker byte (0x41) at the point of the crash. RCX is the destination pointer. The faulting instruction is writeColor writing the attacker’s R channel byte to the overflow destination.
Chrome 144 stable (no –in-process-gpu) crashes the GPU process with STATUS_ACCESS_VIOLATION (0xC0000005), WRITE. The crashpad minidump confirms:
- RAX = 0x41 (attacker’s marker byte)
- The 128 bytes immediately before the crash address contain 32 consecutive 41 41 41 FF RGBA pixels
- The overflow wrote through a 4MB PAGE_READWRITE heap region and crashed at the adjacent PAGE_NOACCESS boundary
The PoC works as follows:
- Creates a 16384x16384x4-layer RGB8 TEXTURE_2D_ARRAY (non-renderable in Vulkan)
- Fills the first 512 rows with a controlled marker byte (0x41)
- Creates a 1x1 RGBA8 renderbuffer FBO as the copy source
- Calls copyTexSubImage3D from the RGBA8 FBO into the RGB8 texture
- ANGLE calls reinitImageAsRenderable, which computes dstBufferSize = 0 (uint32 overflow) and writes 4GB of RGBA8 converted data into the undersized staging buffer
The trigger path is:
gl.copyTexSubImage3D(TEXTURE_2D_ARRAY, ...)
-> TextureVk::copySubImage
-> ensureRenderableIfCopyTexImageCannotTransfer
-> ensureRenderableWithFormat (RGB8 not renderable, convert to RGBA8)
-> respecifyImageStorage
-> reinitImageAsRenderable (layerCount > 1, takes slow path)
-> dstBufferSize = width * height * depth * pixelBytes * layerCount overflows to 0
-> stageSubresourceUpdateAndGetData allocates staging buffer with overflowed size
-> CopyImageCHROMIUM writes 4GB into undersized buffer
Fix
Use CheckedNumeric for the size calculation, consistent with how block-compressed formats are already handled via ANGLE_VK_CHECK_MATH in vk_helpers.cpp stageSubresourceUpdateImpl (lines 8375-8390):
angle::CheckedNumeric<size_t> checkedSize = sourceBox.width;
checkedSize *= sourceBox.height;
checkedSize *= sourceBox.depth;
checkedSize *= dstFormat.pixelBytes;
checkedSize *= layerCount;
ANGLE_VK_CHECK_MATH(contextVk, checkedSize.IsValid());
size_t dstBufferSize = checkedSize.ValueOrDie();
The GLuint pitch calculations at lines 3177-3184 should also use CheckedNumeric.
Bisect
The vulnerable code was introduced in ANGLE commit 8ea87a6767 (“Vulkan: Avoid texture format fallback when possible”), committed 2021-09-08, CL https://chromium-review.googlesource.com/c/angle/angle/+/3104514. The entire reinitImageAsRenderable function, including the unchecked multiplication, was added as new code in this commit.
Impact analysis
Heap buffer overflow in the GPU process, reachable from JavaScript via WebGL2. The attacker controls 3 of every 4 bytes written during the 4GB overflow (the 4th byte is always 0xFF). The staging buffer is allocated in the VMA (Vulkan Memory Allocator) HOST_VISIBLE staging pool. The overflow corrupts adjacent heap allocations. In release Chrome on Windows, approximately 4MB of heap data is overwritten with attacker-controlled content before the write reaches an unmapped page.
On Android, ANGLE Vulkan is the default backend and no command-line flags are required. The Android GPU process has no sandbox (kAndroidGpuSandbox is FEATURE_DISABLED_BY_DEFAULT) and shares the same UID as the browser process, so this overflow corrupts memory in an unsandboxed process reachable from JavaScript with no user interaction. On Linux and ChromeOS, ANGLE Vulkan is the default on many configurations.
The cause
What version of Chrome have you found the security issue in?
144.0.7559.133 Stable
Is the security issue related to a crash?
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?
cinzinga