CVE-2026-9925
Overview
Background
- `webglCompatibilityANGLE`
- Extension flag set for WebGL contexts. The WebGL 2.0 spec removes
MapBufferRange/UnmapBufferfrom the JS API, so ANGLE historically assumed WebGL buffers could never be mapped and skipped the mapped-buffer draw check for WebGL contexts. - ANGLE map entry points still exposed
- Although hidden from WebGL JS, ANGLE’s internal
MapBufferRange/UnmapBufferremain reachable through the passthrough command decoder, which is used to implement features layered on WebGL. - Passthrough decoder `map_ptr` / VMA staging
- On the Vulkan backend, the decoder caches a raw host-visible staging-buffer pointer (
MappedBuffer::map_ptr) from the Vulkan Memory Allocator;DoUnmapBufferlatermemcpys shared-memory data into that pointer.
Root Cause Analysis
ValidateDrawStates skipped the mapped-buffer safety check whenever extensions.webglCompatibilityANGLE was true: the guard read if (!extensions.webglCompatibilityANGLE && ANGLE_UNLIKELY(vertexArray->hasInvalidMappedArrayBuffer())). The assumption — that a WebGL context can never have a mapped vertex buffer — was false, because ANGLE’s MapBufferRange is still exposed through the passthrough decoder even though it is absent from the WebGL JS API. A WebGL2 context could therefore map a vertex buffer and still pass draw validation. The advisory ties this validation gap to a GPU-process use-after-free: drawing with an unaligned vertex attribute forces ANGLE’s CPU vertex-conversion fallback (convertVertexBufferCPU), which internally maps the whole buffer and makes BufferVk::allocStagingBuffer release/reallocate the original staging buffer; the passthrough decoder’s cached map_ptr is left dangling and is then written during UnmapBuffer’s memcpy.
The patch closes the validation gap directly: it removes the !extensions.webglCompatibilityANGLE condition so hasInvalidMappedArrayBuffer() is enforced for all contexts, including WebGL. A draw issued while a VAO buffer is mapped now returns kBufferMapped (GL_INVALID_OPERATION) regardless of WebGL compatibility, preventing the draw that would trigger the internal reallocation. The added WebGL2CompatibilityTest.MappedArrayBufferValidation confirms a draw succeeds normally, fails with GL_INVALID_OPERATION once the array buffer is mapped, and succeeds again after unmap. Classification: the advisory labels this a UAF (CWE-416), and that is the ultimate security sink; however, this diff substantiates only the draw-time validation gap. The free/dangling-map_ptr/memcpy sink lives in BufferVk.cpp and gles2_cmd_decoder_passthrough_doers.cc, which are listed as affected but are not present in this diff — so the UAF chain is inferred from the advisory, while the diff itself is a missing-validation (input-validation) fix that removes the precondition for the UAF.
Attack Path
- WebGL2 context A compromised/cooperating renderer establishes a WebGL2 context on the ANGLE-Vulkan passthrough path (inferred renderer control).
- Create device-local buffer
glBufferDatawithGL_STATIC_DRAWto force device-local memory requiring a host-visible staging buffer. - Map a small range
Issue
MapBufferRangefor a tiny region (e.g. 4 bytes) for writing; the decoder caches the staging buffer’smap_ptr. - Unaligned attribute
glVertexAttribPointerwith an unaligned stride/offset to force ANGLE’s CPU conversion fallback. - Draw despite mapping
DrawArrays; pre-patch, the WebGL exemption lets validation pass.convertVertexBufferCPUmaps the whole buffer, andallocStagingBufferreleases the 4-byte staging buffer (the free), leavingmap_ptrdangling into the VMA pool (inferred; sink not in this diff). - Reclaim and write
Groom the VMA heap to reuse the freed slot with a sensitive Vulkan object, then
UnmapBuffer;DoUnmapBuffermemcpys attacker shared-memory data into the danglingmap_ptr, overwriting the reclaimed object (primitive: controlled write into freed/reused GPU memory → potential control-flow hijack).
Impact Assessment
memcpy, so the UAF sink and any escalation are inferred from the advisory rather than shown here. The fix is a robust validation hardening that blocks the whole chain at its entry.Files Changed
src/libANGLE/validationES.cppsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
Audit Directions
- WebGL-exempted validationGrep ANGLE for other
!extensions.webglCompatibilityANGLE(orwebglCompatibility-gated) checks that assume WebGL-restricted JS semantics but remain reachable via the passthrough decoder’s broader entry-point surface (map/unmap, flush-mapped-range, etc.). - Cached raw pointers across internal reallocationReview
BufferVk::allocStagingBuffer/mapForReadAccessOnlyand passthroughMappedBuffer::map_ptrlifetime: any decoder-cached raw VMA pointer that ANGLE may invalidate via an internalrelease()/reallocation during conversion or defragmentation. - Map/unmap exposure on WebGLConsider the alternate hardening (blocking
MapBufferRange/UnmapBufferfor all WebGL contexts in the decoder orValidateMapBufferRangeBase) and audit whether other internal features rely on those entry points for WebGL contexts.
Patch
From bc20a9487479ce58fd95b3cd5a24e420e29168e8 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Mon, 20 Apr 2026 17:01:04 -0400
Subject: [PATCH] Validate that no VAO buffers are mapped in WebGL.
This validation was skipped for WebGL contexts because it's not possible
to map buffers from the WebGL JS API. Mapping is still exposed from
ANGLE however and buffer mapping is sometimes used to implement other
features on top of WebGL.
Fixed: chromium:500536458
Change-Id: I214ca71d1b14d150c0fbd68624319ae654f2984e
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7779747
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 1878dbd..badd410 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -4091,14 +4091,10 @@
const Extensions &extensions = context->getExtensions();
const State &state = context->getState();
- // WebGL buffers cannot be mapped/unmapped because the MapBufferRange, FlushMappedBufferRange,
- // and UnmapBuffer entry points are removed from the WebGL 2.0 API.
- // https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.14
VertexArray *vertexArray = state.getVertexArray();
ASSERT(vertexArray);
- if (!extensions.webglCompatibilityANGLE &&
- ANGLE_UNLIKELY(vertexArray->hasInvalidMappedArrayBuffer()))
+ if (ANGLE_UNLIKELY(vertexArray->hasInvalidMappedArrayBuffer()))
{
return kBufferMapped;
}
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index a57b0bf..8b13fd0 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4277,6 +4277,45 @@
EXPECT_GLENUM_EQ(GL_RENDERBUFFER, attachmentType);
}
+// The WebGL javascript API has no map functionality but ANGLE still exposes the Map entrypoints
+// since they can be used for other internal operations. Verify you cannot draw with a mapped
+// buffer.
+TEST_P(WebGL2CompatibilityTest, MappedArrayBufferValidation)
+{
+ constexpr char kVS[] =
+ R"(attribute float a_pos;
+void main()
+{
+ gl_Position = vec4(a_pos, a_pos, a_pos, 1.0);
+})";
+
+ ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red());
+ GLint posLocation = glGetAttribLocation(program, "a_pos");
+ ASSERT_NE(-1, posLocation);
+ glUseProgram(program);
+
+ GLBuffer buffer;
+ glBindBuffer(GL_ARRAY_BUFFER, buffer);
+ glBufferData(GL_ARRAY_BUFFER, 16, nullptr, GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(posLocation);
+ glVertexAttribPointer(posLocation, 1, GL_UNSIGNED_BYTE, GL_FALSE, 0,
+ reinterpret_cast<const void *>(12));
+ glDrawArrays(GL_POINTS, 0, 4);
+ ASSERT_GL_NO_ERROR();
+
+ glMapBufferRange(GL_ARRAY_BUFFER, 0, 16, GL_MAP_READ_BIT);
+ EXPECT_GL_NO_ERROR();
+
+ glDrawArrays(GL_POINTS, 0, 4);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ glUnmapBuffer(GL_ARRAY_BUFFER);
+ EXPECT_GL_NO_ERROR();
+ glDrawArrays(GL_POINTS, 0, 4);
+ EXPECT_GL_NO_ERROR();
+}
+
// This tests that rendering feedback loops works as expected with WebGL 2.
// Based on WebGL test conformance2/rendering/rendering-sampling-feedback-loop.html
TEST_P(WebGL2CompatibilityTest, RenderingFeedbackLoopWithDrawBuffers)
Regression Test / PoC
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index a57b0bf..8b13fd0 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4277,6 +4277,45 @@
EXPECT_GLENUM_EQ(GL_RENDERBUFFER, attachmentType);
}
+// The WebGL javascript API has no map functionality but ANGLE still exposes the Map entrypoints
+// since they can be used for other internal operations. Verify you cannot draw with a mapped
+// buffer.
+TEST_P(WebGL2CompatibilityTest, MappedArrayBufferValidation)
+{
+ constexpr char kVS[] =
+ R"(attribute float a_pos;
+void main()
+{
+ gl_Position = vec4(a_pos, a_pos, a_pos, 1.0);
+})";
+
+ ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red());
+ GLint posLocation = glGetAttribLocation(program, "a_pos");
+ ASSERT_NE(-1, posLocation);
+ glUseProgram(program);
+
+ GLBuffer buffer;
+ glBindBuffer(GL_ARRAY_BUFFER, buffer);
+ glBufferData(GL_ARRAY_BUFFER, 16, nullptr, GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(posLocation);
+ glVertexAttribPointer(posLocation, 1, GL_UNSIGNED_BYTE, GL_FALSE, 0,
+ reinterpret_cast<const void *>(12));
+ glDrawArrays(GL_POINTS, 0, 4);
+ ASSERT_GL_NO_ERROR();
+
+ glMapBufferRange(GL_ARRAY_BUFFER, 0, 16, GL_MAP_READ_BIT);
+ EXPECT_GL_NO_ERROR();
+
+ glDrawArrays(GL_POINTS, 0, 4);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ glUnmapBuffer(GL_ARRAY_BUFFER);
+ EXPECT_GL_NO_ERROR();
+ glDrawArrays(GL_POINTS, 0, 4);
+ EXPECT_GL_NO_ERROR();
+}
+
// This tests that rendering feedback loops works as expected with WebGL 2.
// Based on WebGL test conformance2/rendering/rendering-sampling-feedback-loop.html
TEST_P(WebGL2CompatibilityTest, RenderingFeedbackLoopWithDrawBuffers)
Original Bug Report
UAF write in GPU process via unaligned vertex draw on mapped buffers
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 without the security team.
Overview: A validation gap in ANGLE’s WebGL compatibility layer allows WebGL 2 contexts to map buffers and issue draw calls simultaneously. Drawing with an unaligned vertex attribute on a mapped buffer triggers an internal staging buffer reallocation, leaving a dangling pointer in the passthrough command decoder, which is subsequently written to during unmapping.
Affected files:
third_party/angle/src/libANGLE/validationES.cppthird_party/angle/src/libANGLE/renderer/vulkan/BufferVk.cppgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
Estimated timestamp from git blame: 2025-11-28
Description
There is a potential Use-After-Free (UAF) vulnerability in the GPU process when using the ANGLE-Vulkan backend with the passthrough command decoder. This issue allows a compromised renderer to write arbitrary data into freed memory managed by the Vulkan Memory Allocator (VMA), which is not protected by MiraclePtr, potentially leading to a sandbox escape.
Root Cause Analysis
The vulnerability stems from a combination of validation gaps and internal state mismanagement:
- Mapped Buffer Allowed in WebGL 2: The
GLES2DecoderPassthroughImplallows WebGL 2 contexts to issueMapBufferRangecommands. ANGLE’sValidateMapBufferRangeBasedoes not block this for WebGL contexts. The decoder allocates a host-visible staging buffer in VMA and caches its raw pointer inMappedBuffer::map_ptr. - Draw Validation Bypass: Normally, OpenGL forbids drawing from a buffer while it is mapped. However, in
third_party/angle/src/libANGLE/validationES.cpp:4044(ValidateDrawStates), the checkvertexArray->hasInvalidMappedArrayBuffer()is skipped ifextensions.webglCompatibilityANGLEis true. Because this is a WebGL context, the extension is enabled, allowing the draw call to proceed despite the buffer being mapped. - Internal Reallocation: If the draw call uses an unaligned vertex attribute, the GPU cannot consume it directly. ANGLE falls back to CPU conversion via
VertexArrayVk::convertVertexBufferCPU. To do this, it internally maps the entire buffer for reading (srcBuffer->mapForReadAccessOnly). - The Free: If the attacker initially mapped only a small portion of the buffer, this new internal request to map the entire buffer forces
BufferVk::allocStagingBufferto reallocate the staging buffer. It callsmStagingBuffer.release(contextVk), freeing the original small staging buffer back to the VMA pool. - The Use (Write): The passthrough decoder is unaware of ANGLE’s internal reallocation, leaving its cached
map_ptrdangling. When the attacker subsequently issues anUnmapBuffercommand,GLES2DecoderPassthroughImpl::DoUnmapBufferperforms amemcpyfrom attacker-controlled shared memory directly into the danglingmap_ptr.
Potential Steps to Trigger
Note: These are suggested theoretical steps as we do not yet have a working proof of concept.
- A compromised renderer process establishes a WebGL 2 context.
- The attacker creates a buffer (
glBufferDatawithGL_STATIC_DRAWto ensure device-local memory). - The attacker issues
MapBufferRangeto map a very small portion of the buffer (e.g., 4 bytes) for writing. The decoder caches the staging buffer pointer (map_ptr). - The attacker sets up a vertex attribute (
glVertexAttribPointer) bound to the mapped buffer, using an unaligned stride (e.g., 9) to force the CPU conversion fallback. - The attacker issues a
DrawArrayscommand. The validation bypass allows this. ANGLE’s internal CPU conversion forces a full-buffer map, releasing the 4-byte staging buffer. The decoder’smap_ptris now a dangling pointer into the VMA free pool. - The attacker issues standard WebGL commands (like uploading textures) to groom the VMA heap, reallocating the freed 4-byte slot with a sensitive Vulkan object.
- The attacker places a malicious payload in shared memory and calls
UnmapBuffer. The decoder executesmemcpyinto the danglingmap_ptr, overwriting the target object and potentially hijacking control flow in the GPU process.
Suggested Fix
The primary issue is the validation gap. To fix this:
- Update
ValidateDrawStatesinthird_party/angle/src/libANGLE/validationES.cppso it does not unconditionally skip the mapped buffer check for WebGL contexts. It should still enforcevertexArray->hasInvalidMappedArrayBuffer()if a buffer is genuinely mapped. - Alternatively, completely block
MapBufferRangeandUnmapBufferfor all WebGL contexts, as these endpoints are explicitly removed in the WebGL 2.0 specification. This can be enforced inGLES2DecoderPassthroughImplor ANGLE’sValidateMapBufferRangeBase.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.