CVE-2026-9925
Overview
Files Changed
src/libANGLE/validationES.cppsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
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.