Medium watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Sub-componentVulkan
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517045394
Fix commit7d65fc5ed3e3 (angle/angle) +11/-2
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

`DynamicBuffer`
An ANGLE Vulkan helper that sub-allocates GPU memory from a current BufferHelper, advancing an offset for each allocation.
`BufferHelper`
A wrapper around a Vulkan buffer and its device memory; init() creates the buffer and can fail (e.g. out-of-memory), returning an error angle::Result.
`RendererScoped<T>`
An RAII holder that owns a resource tied to the renderer and releases it safely, used here to build the buffer before committing it to mBuffer.
`allocateFromCurrentBuffer`
A DynamicBuffer fast path that assumes a valid mBuffer and computes an allocation offset within it.

Root Cause Analysis

DynamicBuffer’s allocation routine created the buffer eagerly with mBuffer = std::make_unique<BufferHelper>() and then return mBuffer->init(...). If init() failed, mBuffer was left as a non-null pointer to a BufferHelper that never successfully initialized, breaking the invariant that a non-null mBuffer denotes a valid, sized buffer. A later call to allocateFromCurrentBuffer trusted that invariant, advancing mNextAllocationOffset and handing back the invalid buffer, so subsequent writes could go out of bounds of the (uninitialized) backing memory.

The fix builds the buffer in a local RendererScoped<BufferHelper>, calls ANGLE_TRY(buffer.get().init(...)) so a failure returns before mBuffer is set, and only assigns mBuffer from buffer.release() on success. It additionally hardens allocateFromCurrentBuffer with an explicit if (mBuffer == nullptr) return false; guard, and clears mRenderPassCommandBuffer in ContextVk on the abandon/error path so command-buffer tracking cannot desynchronize.

Key insight
The core mistake was publishing mBuffer before its init() succeeded, leaving an invalid buffer that downstream allocation trusted; the fix defers assignment until init succeeds and adds a null-buffer guard in allocateFromCurrentBuffer.

Attack Path

  1. Force an allocation/map failure Drive the Vulkan backend to a BufferHelper::init() (buffer creation or memory map) failure, e.g. via a large or memory-pressured allocation.
  2. Leave `mBuffer` in a bad state Pre-patch, mBuffer remains a non-null pointer to the failed, uninitialized BufferHelper, so the “valid buffer” invariant is violated.
  3. Reach `allocateFromCurrentBuffer` A subsequent allocation calls allocateFromCurrentBuffer, which (pre-patch) proceeds without a null check, computing an offset against the invalid buffer.
  4. Out-of-bounds write The returned invalid buffer is used for a write that lands out of bounds of its backing memory ((inferred) as to the exact sink and controllability).

Impact Assessment

An attacker able to induce a Vulkan buffer allocation/map failure in the GPU process can leave DynamicBuffer in an inconsistent state that leads to an out-of-bounds write during a later allocation. Reliable exploitation depends on controlling the failure timing and the subsequent allocation, so escalation beyond the OOB write is (inferred). The advisory classifies this as an out-of-bounds write in ANGLE; no exploited-in-the-wild status is stated.

Files Changed

  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/ContextVk.cpp

Audit Directions

  • Eager commit before init succeeds
    Look for make_unique/assignment of a member resource followed by a fallible init() whose failure leaves the member non-null but invalid.
  • Fast paths without null/validity guards
    Audit allocation and reuse fast paths (allocateFromCurrentBuffer-style) that assume a prior successful setup without checking it.
  • Error-path state cleanup
    Review Vulkan abandon/flush/error paths for tracking pointers (command buffers, current buffers) that must be reset to avoid desynchronized reuse.
From 7d65fc5ed3e3610c9156b9b9856eebeff57ff21d Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Sun, 19 Jul 2026 14:55:07 -0400
Subject: [PATCH] Vulkan: Reset DynamicBuffer state on allocation failure

- Reset DynamicBuffer state on map failure and harden
allocateFromCurrentBuffer in release builds.
- Clean up ContextVk active render pass tracking on error to prevent
command buffer state desynchronization.

Bug: b/517045394
Change-Id: Ic39e6e8d4b7d9bb49a5f5ff114ad37ae0bb827ad
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8118297
Reviewed-by: Charlie Lao <cclao@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 3684745..1154257 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -6987,6 +6987,7 @@
     {
         mLastFlushedQueueSerial = mRenderPassCommands->getQueueSerial();
         mRenderPassCommands->abandon(this, &collector);
+        mRenderPassCommandBuffer = nullptr;
     }
     collector.releaseCommandBuffers();
 
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index 46a7e0e..c80590c 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -2972,7 +2972,7 @@
 {
     // Allocate the buffer
     ASSERT(!mBuffer);
-    mBuffer = std::make_unique<BufferHelper>();
+    RendererScoped<BufferHelper> buffer(context->getRenderer());
 
     VkBufferCreateInfo createInfo    = {};
     createInfo.sType                 = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
@@ -2983,11 +2983,19 @@
     createInfo.queueFamilyIndexCount = 0;
     createInfo.pQueueFamilyIndices   = nullptr;
 
-    return mBuffer->init(context, createInfo, mMemoryPropertyFlags);
+    ANGLE_TRY(buffer.get().init(context, createInfo, mMemoryPropertyFlags));
+
+    mBuffer = std::make_unique<BufferHelper>(buffer.release());
+    return angle::Result::Continue;
 }
 
 bool DynamicBuffer::allocateFromCurrentBuffer(size_t sizeInBytes, BufferHelper **bufferHelperOut)
 {
+    if (mBuffer == nullptr)
+    {
+        return false;
+    }
+
     mNextAllocationOffset =
         roundUp<uint32_t>(mNextAllocationOffset, static_cast<uint32_t>(mAlignment));
 
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential wild write in GPU process due to un-reset stale offset on vkMapMemory failure

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 logic error in ANGLE’s Vulkan backend can cause DynamicBuffer to retain a stale mNextAllocationOffset and a partially-initialized mBuffer pointer if vkMapMemory fails. Because this failure translates to GL_INVALID_OPERATION rather than context loss, subsequent allocations can succeed using the stale state. This can lead to a wild memory write in the GPU process when downstream code attempts to copy uniform or vertex data.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/Suballocation.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp

Estimated timestamp from git blame: 2020-12-01

Root Cause Analysis

In third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp, DynamicBuffer::allocateNewBuffer assigns mBuffer before invoking BufferHelper::init:

angle::Result DynamicBuffer::allocateNewBuffer(ErrorContext *context)
{
    ...
    ASSERT(!mBuffer);
    mBuffer = std::make_unique<BufferHelper>();
    ...
    return mBuffer->init(context, createInfo, mMemoryPropertyFlags);
}

If mBuffer->init fails (for example, if vkMapMemory returns VK_ERROR_MEMORY_MAP_FAILED), the failure propagates immediately inside DynamicBuffer::allocate:

if (mBufferFreeList.empty() || ...)
{
    ANGLE_TRY(allocateNewBuffer(context));
}
...
mNextAllocationOffset = 0; // Bypassed on failure

Because the allocation failure propagates early, the following critical states are not rolled back:

  1. mBuffer remains pointing to the partially-initialized, unmapped BufferHelper instance.
  2. mNextAllocationOffset is not reset to 0, retaining its previous stale, high value.

Survival of the GL Context

When vkMapMemory fails with VK_ERROR_MEMORY_MAP_FAILED, it maps to GL_INVALID_OPERATION via DefaultGLErrorCode in ContextVk.cpp:

GLenum DefaultGLErrorCode(VkResult result) {
    switch (result) {
        ...
        default:
            return GL_INVALID_OPERATION;      // VK_ERROR_MEMORY_MAP_FAILED falls here
    }
}

Since ANGLE only triggers context loss on GL_OUT_OF_MEMORY or GL_CONTEXT_LOST, the WebGL context survives, records GL_INVALID_OPERATION, and continues processing GLES command buffers.

Downstream Wild Write in Release Builds

Upon a subsequent draw call requiring a smaller allocation, allocateFromCurrentBuffer checks if the requested size fits within the stale bounds:

bool DynamicBuffer::allocateFromCurrentBuffer(size_t sizeInBytes, BufferHelper **bufferHelperOut) {
    mNextAllocationOffset = roundUp<uint32_t>(mNextAllocationOffset, mAlignment);
    ...
    if (!checkedNextWriteOffset.IsValid() || checkedNextWriteOffset.ValueOrDie() > mSize)
        return false;
    ...
    mBuffer->setSuballocationOffsetAndSize(mNextAllocationOffset, sizeToAllocate);
    *bufferHelperOut = mBuffer.get();
}

Since ASSERT(mBuffer->getMappedMemory()) is compiled out in release builds, the function returns the half-initialized BufferHelper. When downstream code (such as default uniform copy in ProgramExecutableVk.cpp or vertex streamed buffer copy) writes data to the buffer, it calls getMappedMemory(), which evaluates to (spec-undefined mMappedMemory) + stale_offset:

uint8_t *bufferData = defaultUniformBuffer->getMappedMemory();
memcpy(&bufferData[offsets[shaderType]], uniformData.data(), uniformData.size()); // WILD WRITE

Potential Steps to Trigger (Hypothetical)

Note: These steps are based on static code analysis and have not been executed on a running system.

  1. From a compromised renderer, create a WebGL context backed by ANGLE/Vulkan.
  2. Issue drawing commands with default uniforms to advance mDefaultUniformStorage.mNextAllocationOffset near its limit (~56 KB out of 64 KB).
  3. Artificially exhaust host virtual-address (VA) space to force the next vkMapMemory call to return VK_ERROR_MEMORY_MAP_FAILED during a new buffer allocation.
  4. Submit a draw call that triggers the new allocation. The allocation fails, but because it is handled as GL_INVALID_OPERATION, the context survives and leaves mBuffer and mNextAllocationOffset un-reset.
  5. Release VA pressure.
  6. Issue a smaller draw command where the uniform requirements fit within the stale buffer boundaries (stale_offset + size <= mSize). allocateFromCurrentBuffer succeeds, returning the unmapped buffer helper.
  7. A memcpy copy of uniform data writes to an invalid address in the GPU process.

Suggested Fix

In DynamicBuffer::allocate (inside third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp), ensure that if allocateNewBuffer fails, mBuffer is explicitly reset to nullptr and mNextAllocationOffset is cleared or rolled back to a safe state to prevent subsequent draw calls from utilizing stale states.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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