CVE-2026-79127
Overview
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 errorangle::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
DynamicBufferfast path that assumes a validmBufferand 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.
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
- 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. - Leave `mBuffer` in a bad state
Pre-patch,
mBufferremains a non-null pointer to the failed, uninitializedBufferHelper, so the “valid buffer” invariant is violated. - Reach `allocateFromCurrentBuffer`
A subsequent allocation calls
allocateFromCurrentBuffer, which (pre-patch) proceeds without a null check, computing an offset against the invalid buffer. - 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
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.cppsrc/libANGLE/renderer/vulkan/ContextVk.cpp
Audit Directions
- Eager commit before init succeedsLook for
make_unique/assignment of a member resource followed by a fallibleinit()whose failure leaves the member non-null but invalid. - Fast paths without null/validity guardsAudit allocation and reuse fast paths (
allocateFromCurrentBuffer-style) that assume a prior successful setup without checking it. - Error-path state cleanupReview Vulkan abandon/flush/error paths for tracking pointers (command buffers, current buffers) that must be reset to avoid desynchronized reuse.
Patch
From 7d65fc5ed3e3610c9156b9b9856eebeff57ff21d Mon Sep 17 00:00:00 2001 From: wangra <[email protected]> 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 <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- 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));
Original Bug Report
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.cppthird_party/angle/src/libANGLE/renderer/vulkan/Suballocation.cppthird_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:
mBufferremains pointing to the partially-initialized, unmappedBufferHelperinstance.mNextAllocationOffsetis not reset to0, 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.
- From a compromised renderer, create a WebGL context backed by ANGLE/Vulkan.
- Issue drawing commands with default uniforms to advance
mDefaultUniformStorage.mNextAllocationOffsetnear its limit (~56 KB out of 64 KB). - Artificially exhaust host virtual-address (VA) space to force the next
vkMapMemorycall to returnVK_ERROR_MEMORY_MAP_FAILEDduring a new buffer allocation. - 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 leavesmBufferandmNextAllocationOffsetun-reset. - Release VA pressure.
- Issue a smaller draw command where the uniform requirements fit within the stale buffer boundaries (
stale_offset + size <= mSize).allocateFromCurrentBuffersucceeds, returning the unmapped buffer helper. - A
memcpycopy 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.