Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker500536458
Fix commitbc20a9487479 (angle/angle) +40/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • src/libANGLE/validationES.cpp
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp
From bc20a9487479ce58fd95b3cd5a24e420e29168e8 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
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 <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

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)
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/BufferVk.cpp
  • gpu/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:

  1. Mapped Buffer Allowed in WebGL 2: The GLES2DecoderPassthroughImpl allows WebGL 2 contexts to issue MapBufferRange commands. ANGLE’s ValidateMapBufferRangeBase does not block this for WebGL contexts. The decoder allocates a host-visible staging buffer in VMA and caches its raw pointer in MappedBuffer::map_ptr.
  2. 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 check vertexArray->hasInvalidMappedArrayBuffer() is skipped if extensions.webglCompatibilityANGLE is true. Because this is a WebGL context, the extension is enabled, allowing the draw call to proceed despite the buffer being mapped.
  3. 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).
  4. 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::allocStagingBuffer to reallocate the staging buffer. It calls mStagingBuffer.release(contextVk), freeing the original small staging buffer back to the VMA pool.
  5. The Use (Write): The passthrough decoder is unaware of ANGLE’s internal reallocation, leaving its cached map_ptr dangling. When the attacker subsequently issues an UnmapBuffer command, GLES2DecoderPassthroughImpl::DoUnmapBuffer performs a memcpy from attacker-controlled shared memory directly into the dangling map_ptr.

Potential Steps to Trigger

Note: These are suggested theoretical steps as we do not yet have a working proof of concept.

  1. A compromised renderer process establishes a WebGL 2 context.
  2. The attacker creates a buffer (glBufferData with GL_STATIC_DRAW to ensure device-local memory).
  3. The attacker issues MapBufferRange to map a very small portion of the buffer (e.g., 4 bytes) for writing. The decoder caches the staging buffer pointer (map_ptr).
  4. 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.
  5. The attacker issues a DrawArrays command. The validation bypass allows this. ANGLE’s internal CPU conversion forces a full-buffer map, releasing the 4-byte staging buffer. The decoder’s map_ptr is now a dangling pointer into the VMA free pool.
  6. 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.
  7. The attacker places a malicious payload in shared memory and calls UnmapBuffer. The decoder executes memcpy into the dangling map_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:

  1. Update ValidateDrawStates in third_party/angle/src/libANGLE/validationES.cpp so it does not unconditionally skip the mapped buffer check for WebGL contexts. It should still enforce vertexArray->hasInvalidMappedArrayBuffer() if a buffer is genuinely mapped.
  2. Alternatively, completely block MapBufferRange and UnmapBuffer for all WebGL contexts, as these endpoints are explicitly removed in the WebGL 2.0 specification. This can be enforced in GLES2DecoderPassthroughImpl or ANGLE’s ValidateMapBufferRangeBase.

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.

View on issue tracker