CVE-2026-17678
Overview
Background
- `GLintptr` vs `uintptr_t`
GLintptris a signed pointer-sized integer, whereasuintptr_tis unsigned; a pointer value with the high bit set (e.g.0x80000000on 32-bit) becomes negative when stored inGLintptr.- `glVertexAttribPointer` offset
- for a client-array attribute bound to a buffer, the
pointerargument is a byte offset into the buffer, obtained by casting thevoid*pointer to an integer. - `glBindVertexBuffer` offset
- a
GLintptrargument that is validated to be non-negative, unlike the pointer-derived offset above. - `ComputeVertexAttributeOffset`
- helper returning
attrib.relativeOffset + binding.getOffset(), the effective byte offset used by backends to locate source vertex data.
Root Cause Analysis
The vertex binding offset was stored and propagated as the signed type GLintptr throughout VertexArray, VertexBinding, and the D3D vertex data path. For glVertexAttribPointer, the offset is a pointer cast to an integer (reinterpret_cast<GLintptr>(pointer)), so a large pointer value such as 0x80000000 was reinterpreted as a negative offset. Downstream arithmetic — ComputeVertexAttributeOffset, the D3D offset % attribStride computations, and sourceData += offset in VertexDataManager — then operated on a negative value, which after casts to size_t/pointer arithmetic could compute an out-of-range source address, producing the out-of-bounds read the advisory reports (see the StoreStaticAttribWithLargeOffset and LargeAttribPointerOffsetNoCrash regression tests using offset 0x80000000).
The fix changes the offset type to uintptr_t end-to-end (bindVertexBufferImpl, VertexBinding::mOffset/getOffset/setOffset, ComputeVertexAttributeOffset, and the D3D consumers), so the large pointer value is treated as the large positive offset it actually is; glBindVertexBuffer keeps its signed GLintptr API but now ASSERTs offset >= 0 and casts to uintptr_t before calling the impl, and FrameCapture casts back to GLintptr only for the capture API.
GLintptr, so high pointer values became negative and corrupted downstream address arithmetic; switching the internal offset type to uintptr_t (while asserting non-negativity at the one genuinely signed API entry) removes the sign misinterpretation.Attack Path
- Bind a buffer and set a large offset
Call
glBindBuffer(GL_ARRAY_BUFFER, buf)thenglVertexAttribPointer(loc, ..., reinterpret_cast<void*>(0x80000000)), so the pointer-derived offset has its high bit set. - Store as negative offset
Pre-patch,
reinterpret_cast<GLintptr>(pointer)yields a negative value stored inVertexBinding::mOffset. - Trigger offset arithmetic on draw
Issue
glDrawArrays/glDrawElements;ComputeVertexAttributeOffsetand backend consumers (VertexDataManager::StoreStaticAttrib, D3DgetSpaceRequired) use the negative offset in address/modulo computations. - Out-of-bounds read (per advisory) The miscomputed source address causes reads outside the buffer’s backing store; the regression tests assert no crash, and the diff shows the type fix but not a further read/write primitive.
Impact Assessment
Files Changed
src/libANGLE/VertexArray.cppsrc/libANGLE/VertexArray.hsrc/libANGLE/VertexAttribute.cppsrc/libANGLE/VertexAttribute.hsrc/libANGLE/capture/FrameCapture.cppsrc/libANGLE/renderer/d3d/VertexBuffer.cppsrc/libANGLE/renderer/d3d/VertexDataManager.cpp
Audit Directions
- Signed storage of pointer-derived integersAudit any
reinterpret_cast<GLintptr>/intptr_tof avoid*used as an offset or length; pointer values with the high bit set become negative and corrupt later arithmetic. - Type mismatch across API boundariesWhere one API (
glBindVertexBuffer) supplies a validated signed offset and another (glVertexAttribPointer) supplies an unsigned pointer, ensure the shared internal path uses the widest correct unsigned type and asserts the signed entry’s non-negativity. - Offset used in modulo / pointer additionReview
offset % stride,sourceData += offset, andCheckedNumericoffsets for signedness before the cast tosize_t, since a negative value silently wraps to a huge index.
Patch
From 9464aca6502c2f4ba787f8a8ec9d1f82788454ac Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 10 Jun 2026 22:42:53 -0400
Subject: [PATCH] Use uintptr_t instead of GLintptr for vertex binding offsets
In glBindVertexBuffer, the offset is GLintptr, but is required to be
non-negative. In glVertexAttribPointer, the offset is a pointer cast to
integer. In the latter case, if the pointer value is large, the offset
shouldn't be interpreted as negative.
Bug: chromium:515452019
Change-Id: I9fc934b8723a8294593058a791e937f651a63546
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7921635
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
---
diff --git a/src/libANGLE/VertexArray.cpp b/src/libANGLE/VertexArray.cpp
index fcc2d72..d56d38a 100644
--- a/src/libANGLE/VertexArray.cpp
+++ b/src/libANGLE/VertexArray.cpp
@@ -475,7 +475,7 @@
ANGLE_INLINE VertexArray::DirtyBindingBits VertexArray::bindVertexBufferImpl(const Context *context,
size_t bindingIndex,
Buffer *boundBuffer,
- GLintptr offset,
+ uintptr_t offset,
GLsizei stride)
{
ASSERT(bindingIndex < getMaxBindings());
@@ -556,8 +556,10 @@
GLintptr offset,
GLsizei stride)
{
- const VertexArray::DirtyBindingBits dirtyBindingBits =
- bindVertexBufferImpl(context, bindingIndex, boundBuffer, offset, stride);
+ // |offset| must be non-negative per validation rules of glBindVertexBuffer.
+ ASSERT(offset >= 0);
+ const VertexArray::DirtyBindingBits dirtyBindingBits = bindVertexBufferImpl(
+ context, bindingIndex, boundBuffer, static_cast<uintptr_t>(offset), stride);
if (!dirtyBindingBits.test(DIRTY_BINDING_BUFFER) && context->isSharedContext() &&
boundBuffer != nullptr)
@@ -627,7 +629,7 @@
// Change of attrib.pointer is not part of attribDirty. Pointer is actually the buffer offset
// which is handled within bindVertexBufferImpl and reflected in bufferDirty.
attrib.pointer = pointer;
- GLintptr offset = boundBuffer ? reinterpret_cast<GLintptr>(pointer) : 0;
+ uintptr_t offset = boundBuffer ? reinterpret_cast<uintptr_t>(pointer) : 0;
const VertexArray::DirtyBindingBits dirtyBindingBits =
bindVertexBufferImpl(context, attribIndex, boundBuffer, offset, effectiveStride);
diff --git a/src/libANGLE/VertexArray.h b/src/libANGLE/VertexArray.h
index 413cbdd..78c6705 100644
--- a/src/libANGLE/VertexArray.h
+++ b/src/libANGLE/VertexArray.h
@@ -413,7 +413,7 @@
DirtyBindingBits bindVertexBufferImpl(const Context *context,
size_t bindingIndex,
Buffer *boundBuffer,
- GLintptr offset,
+ uintptr_t offset,
GLsizei stride);
void onBind(const Context *context);
diff --git a/src/libANGLE/VertexAttribute.cpp b/src/libANGLE/VertexAttribute.cpp
index faef8fe..49f6f54 100644
--- a/src/libANGLE/VertexAttribute.cpp
+++ b/src/libANGLE/VertexAttribute.cpp
@@ -135,7 +135,7 @@
}
// Warning: you should ensure binding really matches attrib.bindingIndex before using this function.
-GLintptr ComputeVertexAttributeOffset(const VertexAttribute &attrib, const VertexBinding &binding)
+uintptr_t ComputeVertexAttributeOffset(const VertexAttribute &attrib, const VertexBinding &binding)
{
return attrib.relativeOffset + binding.getOffset();
}
diff --git a/src/libANGLE/VertexAttribute.h b/src/libANGLE/VertexAttribute.h
index cbea773..9593169 100644
--- a/src/libANGLE/VertexAttribute.h
+++ b/src/libANGLE/VertexAttribute.h
@@ -37,8 +37,8 @@
GLuint getDivisor() const { return mDivisor; }
void setDivisor(GLuint divisorIn) { mDivisor = divisorIn; }
- GLintptr getOffset() const { return mOffset; }
- void setOffset(GLintptr offsetIn) { mOffset = offsetIn; }
+ uintptr_t getOffset() const { return mOffset; }
+ void setOffset(uintptr_t offsetIn) { mOffset = offsetIn; }
const AttributesMask &getBoundAttributesMask() const { return mBoundAttributesMask; }
@@ -49,7 +49,7 @@
private:
GLuint mStride;
GLuint mDivisor;
- GLintptr mOffset;
+ uintptr_t mOffset;
// Mapping from this binding to all of the attributes that are using this binding.
AttributesMask mBoundAttributesMask;
@@ -98,7 +98,7 @@
size_t ComputeVertexAttributeStride(const VertexAttribute &attrib, const VertexBinding &binding);
// Warning: you should ensure binding really matches attrib.bindingIndex before using this function.
-GLintptr ComputeVertexAttributeOffset(const VertexAttribute &attrib, const VertexBinding &binding);
+uintptr_t ComputeVertexAttributeOffset(const VertexAttribute &attrib, const VertexBinding &binding);
size_t ComputeVertexBindingElementCount(GLuint divisor,
uint64_t drawCount,
diff --git a/src/libANGLE/capture/FrameCapture.cpp b/src/libANGLE/capture/FrameCapture.cpp
index dbc4f1e..206beaf 100644
--- a/src/libANGLE/capture/FrameCapture.cpp
+++ b/src/libANGLE/capture/FrameCapture.cpp
@@ -2962,7 +2962,8 @@
}
else if (attrib.bindingIndex == attribIndex &&
VertexBindingMatchesAttribStride(attrib, binding) &&
- (!buffer || binding.getOffset() == reinterpret_cast<GLintptr>(attrib.pointer)))
+ (!buffer ||
+ binding.getOffset() == reinterpret_cast<uintptr_t>(attrib.pointer)))
{
// Check if we can use strictly ES2 semantics, and track indexes that do.
vertexPointerBindings.set(attribIndex);
@@ -3058,9 +3059,10 @@
if (buffer)
{
- Capture(setupCalls, CaptureBindVertexBuffer(
- *replayState, true, static_cast<GLuint>(bindingIndex),
- buffer->id(), binding.getOffset(), binding.getStride()));
+ Capture(setupCalls,
+ CaptureBindVertexBuffer(
+ *replayState, true, static_cast<GLuint>(bindingIndex), buffer->id(),
+ static_cast<GLintptr>(binding.getOffset()), binding.getStride()));
}
if (binding.getDivisor() != 0)
diff --git a/src/libANGLE/renderer/d3d/VertexBuffer.cpp b/src/libANGLE/renderer/d3d/VertexBuffer.cpp
index c8891f6..1262e14 100644
--- a/src/libANGLE/renderer/d3d/VertexBuffer.cpp
+++ b/src/libANGLE/renderer/d3d/VertexBuffer.cpp
@@ -249,7 +249,7 @@
}
size_t attribOffset =
- (static_cast<size_t>(ComputeVertexAttributeOffset(attrib, binding)) % attribStride);
+ static_cast<size_t>(ComputeVertexAttributeOffset(attrib, binding) % attribStride);
return (offset == attribOffset);
}
@@ -257,9 +257,9 @@
const gl::VertexBinding &binding)
{
formatID = attrib.format->id;
- offset = stride = static_cast<GLuint>(ComputeVertexAttributeStride(attrib, binding));
- offset = static_cast<size_t>(ComputeVertexAttributeOffset(attrib, binding)) %
- ComputeVertexAttributeStride(attrib, binding);
+ stride = static_cast<GLuint>(ComputeVertexAttributeStride(attrib, binding));
+ offset = static_cast<size_t>(ComputeVertexAttributeOffset(attrib, binding) %
+ ComputeVertexAttributeStride(attrib, binding));
}
StaticVertexBufferInterface::StaticVertexBufferInterface(BufferFactoryD3D *factory)
diff --git a/src/libANGLE/renderer/d3d/VertexDataManager.cpp b/src/libANGLE/renderer/d3d/VertexDataManager.cpp
index 729e1eb..a2181a7 100644
--- a/src/libANGLE/renderer/d3d/VertexDataManager.cpp
+++ b/src/libANGLE/renderer/d3d/VertexDataManager.cpp
@@ -122,10 +122,10 @@
alignment = std::min<size_t>(elementSize, 4);
}
- GLintptr offset = ComputeVertexAttributeOffset(attrib, binding);
+ uintptr_t offset = ComputeVertexAttributeOffset(attrib, binding);
// Final alignment check - unaligned data must be converted.
- return (static_cast<size_t>(ComputeVertexAttributeStride(attrib, binding)) % alignment == 0) &&
- (static_cast<size_t>(offset) % alignment == 0);
+ return (ComputeVertexAttributeStride(attrib, binding) % alignment == 0) &&
+ (offset % alignment == 0);
}
} // anonymous namespace
@@ -370,13 +370,13 @@
// Compute source data pointer
const uint8_t *sourceData = nullptr;
- angle::CheckedNumeric<GLintptr> offset = ComputeVertexAttributeOffset(attrib, binding);
+ angle::CheckedNumeric<uintptr_t> offset = ComputeVertexAttributeOffset(attrib, binding);
ANGLE_TRY(bufferD3D->getData(context, &sourceData));
if (sourceData)
{
- sourceData += GLintptr{offset.ValueOrDie()};
+ sourceData += uintptr_t{offset.ValueOrDie()};
}
translated->storage = nullptr;
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 84f0c4c..edb1d10 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -101,7 +101,7 @@
42266900 VULKAN : EGLSurfacelessContextTest.*/*ForceDelayedDeviceCreationForTesting* = SKIP
510376884 VULKAN : ComputeShaderTest.DeleteTextureBoundToImageUnit/* = SKIP
-500721361 VULKAN SWIFTSHADER : VertexAttributeTestES3.storeStaticAttribWithLargeOffset/* = SKIP
+500721361 VULKAN SWIFTSHADER : VertexAttributeTestES3.StoreStaticAttribWithLargeOffset/* = SKIP
// Causes a write-after-write hazard on Linux/Vk
524405499 VULKAN : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyStencil8/* = SKIP
@@ -394,7 +394,7 @@
42265138 MAC AMD OPENGL : TransformFeedbackTest.TransformFeedbackQueryPausedDrawThenResume/* = SKIP
42265138 MAC AMD OPENGL : TransformFeedbackTest.TransformFeedbackPausedDrawThenResume/* = SKIP
40050013 MAC AMD OPENGL : Texture3DTestES3.PixelUnpackStateTex* = SKIP
-1296467 MAC OPENGL : VertexAttributeTestES3.emptyBuffer/* = SKIP
+1296467 MAC OPENGL : VertexAttributeTestES3.EmptyBuffer/* = SKIP
42265677 MAC INTEL OPENGL : CopyTextureTest.CopyToMipmap/* = SKIP
42265680 MAC NVIDIA OPENGL : CopyTextureTest.CubeMap*/* = SKIP
42265680 MAC NVIDIA OPENGL : CopyTextureTest.CopyToMipmap/* = SKIP
@@ -508,6 +508,7 @@
515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColor/* = SKIP
518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColorUnused/* = SKIP
+524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
@@ -768,7 +769,7 @@
42264692 PIXEL4ORXL GLES : DepthStencilTestES3.FramebufferClearThenStencilAttachedThenStencilTestState/* = SKIP
42264692 PIXEL4ORXL GLES : DepthStencilTestES3.FramebufferClearThenStencilTestStateThenStencilAttached/* = SKIP
42264692 PIXEL4ORXL GLES : DepthStencilTestES3.StencilTestStateThenFramebufferClearThenStencilAttached/* = SKIP
-1296467 PIXEL4ORXL GLES : VertexAttributeTestES3.emptyBuffer/* = SKIP
+1296467 PIXEL4ORXL GLES : VertexAttributeTestES3.EmptyBuffer/* = SKIP
42265757 PIXEL4ORXL GLES : TextureBufferTestES31.TextureBufferThenBufferData/* = SKIP
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index ff95017..52af888 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -83,6 +83,7 @@
42264831 : FramebufferTest_ES3.RenderAndInvalidateImmutableTextureWithBeyondMaxLevel/* = SKIP_FOR_CAPTURE
490170083 : CopyTextureTestES3.SRGBWithPackParameters/* = SKIP_FOR_CAPTURE
498904293 : CopyTextureTestES3.PBOSynchronization/* = SKIP_FOR_CAPTURE
+42264706 : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP_FOR_CAPTURE
# The following tests fail with forceRobustResourceInit
# They were accidentally passing until http://crrev/c/5588816
diff --git a/src/tests/gl_tests/VertexAttributeTest.cpp b/src/tests/gl_tests/VertexAttributeTest.cpp
index 5b337c1..446a51b 100644
--- a/src/tests/gl_tests/VertexAttributeTest.cpp
+++ b/src/tests/gl_tests/VertexAttributeTest.cpp
@@ -5284,7 +5284,7 @@
}
// Test maxinum attribs full of Client buffers and then switch to mixed.
-TEST_P(VertexAttributeTestES3, fullClientBuffersSwitchToMixed)
+TEST_P(VertexAttributeTestES3, FullClientBuffersSwitchToMixed)
{
GLint maxAttribs;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxAttribs);
@@ -5403,16 +5403,16 @@
}
// Test bind an empty buffer for vertex attribute does not crash
-TEST_P(VertexAttributeTestES3, emptyBuffer)
+TEST_P(VertexAttributeTestES3, EmptyBuffer)
{
- constexpr char vs2[] =
+ constexpr char kVS[] =
R"(#version 300 es
in uvec4 attr0;
void main()
{
gl_Position = vec4(attr0.x, 0.0, 0.0, 0.0);
})";
- constexpr char fs[] =
+ constexpr char kFS[] =
R"(#version 300 es
precision highp float;
out vec4 color;
@@ -5420,13 +5420,13 @@
{
color = vec4(1.0, 0.0, 0.0, 1.0);
})";
- GLuint program2 = CompileProgram(vs2, fs);
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
GLBuffer buf;
glBindBuffer(GL_ARRAY_BUFFER, buf);
glEnableVertexAttribArray(0);
glVertexAttribIPointer(0, 4, GL_UNSIGNED_BYTE, 0, 0);
glVertexAttribDivisor(0, 2);
- glUseProgram(program2);
+ glUseProgram(program);
glDrawArrays(GL_POINTS, 0, 1);
swapBuffers();
@@ -5434,16 +5434,16 @@
// Test that setting a large offset on glVertexAttribPointer doesn't OOB when going
// through StoreStaticAttrib. See http://crbug.com/489369089
-TEST_P(VertexAttributeTestES3, storeStaticAttribWithLargeOffset)
+TEST_P(VertexAttributeTestES3, StoreStaticAttribWithLargeOffset)
{
- constexpr char vs2[] =
+ constexpr char kVS[] =
R"(#version 300 es
layout(location = 0) in vec3 a;
void main() {
gl_Position = vec4(a, 1.0);
gl_PointSize = 1.0;
})";
- constexpr char fs[] =
+ constexpr char kFS[] =
R"(#version 300 es
precision mediump float;
layout(location = 0) out vec4 FragColor;
@@ -5451,7 +5451,7 @@
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
})";
- GLuint program2 = CompileProgram(vs2, fs);
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
GLBuffer buf;
glBindBuffer(GL_ARRAY_BUFFER, buf);
std::array<uint8_t, 256> data;
@@ -5466,7 +5466,7 @@
glVertexAttribPointer(0, 3, GL_BYTE, GL_TRUE, 3, reinterpret_cast<void *>(0x80000000));
glEnableVertexAttribArray(0);
- glUseProgram(program2);
+ glUseProgram(program);
glDrawArrays(GL_POINTS, 0, 1);
swapBuffers();
@@ -6485,6 +6485,31 @@
EXPECT_PIXEL_COLOR_EQ(54, 54, GLColor::green);
}
+// Ensure a large offset is not interpreted as negative.
+TEST_P(VertexAttributeTestES3, LargeAttribPointerOffsetNoCrash)
+{
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ glUseProgram(program);
+
+ GLBuffer position;
+ constexpr std::array<float, 6> kTriangle = {-1, -1, 3, -1, -1, 3};
+ glBindBuffer(GL_ARRAY_BUFFER, position);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(kTriangle), kTriangle.data(), GL_STATIC_DRAW);
+
+ GLint posLoc = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
+ ASSERT_NE(-1, posLoc);
+ glEnableVertexAttribArray(posLoc);
+ glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0,
+ reinterpret_cast<const void *>(0x80000000));
+ glVertexAttribDivisor(posLoc, 256);
+
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ // Nothing that can be validated. The test shouldn't crash.
+ ASSERT_GL_NO_ERROR();
+
+ swapBuffers();
+}
+
ANGLE_INSTANTIATE_TEST_ES3(VertexAttributeResizeDefaultTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VertexAttributeUint8Test);
Original Bug Report
CPU OOB Read in ANGLE-Vulkan via Sign-Extended VertexAttribPointer Offset
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The GLES2 passthrough decoder sign-extends the VertexAttribPointer offset on 64-bit platforms, allowing a compromised renderer to specify a negative offset. In ANGLE’s Vulkan backend, CPU-side vertex divisor emulation can use this negative offset to perform an out-of-bounds read from mapped GPU memory. This vulnerability potentially allows for cross-origin information disclosure within the GPU process.
Affected files:
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.ccthird_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cppthird_party/angle/src/libANGLE/VertexArray.cpp
Estimated timestamp from git blame: 2019-07-17
Summary
A potential sign-extension vulnerability exists in the GLES2 passthrough command decoder’s handling of vertex attribute offsets. When combined with ANGLE’s vertex divisor emulation in the Vulkan backend, this can lead to a CPU-side out-of-bounds (OOB) read. On 64-bit systems, a large unsigned 32-bit offset is incorrectly treated as a signed value, resulting in a negative 64-bit pointer/offset that bypasses standard bounds checks and points before the allocated buffer memory.
Root Cause Analysis
1. Sign-Extension in Passthrough Decoder
In gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc, the handler for VertexAttribPointer (and VertexAttribIPointer) takes a renderer-controlled uint32_t c.offset and narrows it to a signed 32-bit GLsizei before converting it to a pointer:
// gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc
GLsizei offset = static_cast<GLsizei>(c.offset); // e.g., 0x80000000 -> -2147483648
const void* ptr = reinterpret_cast<const void*>(offset); // Sign-extended on 64-bit systems
On 64-bit platforms, reinterpret_cast<const void*>(offset) performs sign-extension if the most significant bit of the 32-bit offset is set. For example, 0x80000000 becomes 0xFFFFFFFF80000000.
2. Negative Offset in ANGLE
ANGLE receives this sign-extended pointer and, when a buffer is bound, stores it as a signed 64-bit GLintptr binding offset in third_party/angle/src/libANGLE/VertexArray.cpp. There is currently no front-end validation to ensure this offset is non-negative when a buffer is bound.
3. OOB Read in Vulkan Backend
In ANGLE’s Vulkan backend, if a vertex attribute divisor exceeds the hardware limit (e.g., 255), the renderer falls back to CPU-side emulation in VertexArrayVk::updateStreamedAttribs. This path maps the source vertex buffer and calculates the source pointer by adding the stored offset:
// third_party/angle/src/libANGLE/renderer/vulkan/VertexArrayVk.cpp
ANGLE_TRY(bufferVk->mapForReadAccessOnly(contextVk, &buffSrc));
// buffSrc points to the start of the suballocation
src = reinterpret_cast<const uint8_t *>(buffSrc) + binding.getOffset();
Since binding.getOffset() can be negative (e.g., -2GB), the resulting src pointer points to memory significantly before the intended buffer suballocation, leading to an OOB read during the subsequent data copy in StreamVertexDataWithDivisor.
Potential Impact
A compromised renderer could potentially leak up to 2GB of memory from the GPU process’s address space. In the Vulkan backend, buffers are often suballocated from large shared BufferBlock pools. This OOB read could allow an attacker to disclose cross-origin vertex data, pixel data, or other sensitive information belonging to different WebGL contexts or origins sharing the GPU process. This is particularly relevant on Android, where the GPU process is often unsandboxed and uses the Vulkan backend.
Potential Steps to Reproduce
- From a compromised renderer, create a large
GL_ARRAY_BUFFERand populate it with data. - Call
glVertexAttribPointerwith an offset that has the 31st bit set (e.g.,0x80000000). - Set a vertex divisor for the same attribute that is greater than 255 (e.g.,
glVertexAttribDivisor(index, 256)). - Perform a draw call while capturing output via Transform Feedback.
- Read back the Transform Feedback buffer to inspect the leaked memory contents from the GPU process.
Suggested Fix
- Decoder Level: In
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc, treat the offset as an unsigned value (uintptr_t) during the conversion tovoid*to avoid sign-extension. - ANGLE Level: Update
ValidateVertexAttribPointerandValidateVertexAttribIPointerinthird_party/angle/src/libANGLE/validationES.cppto explicitly disallow negative offsets when a buffer is bound to the attribute.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.