High CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
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
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-08 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-05-27
Patch gap35 days — fix public in source before the advisory

Background

`webglCompatibilityANGLE`
Extension flag set for WebGL contexts. The WebGL 2.0 spec removes MapBufferRange/UnmapBuffer from 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/UnmapBuffer remain 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; DoUnmapBuffer later memcpys 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.

Key insight
ANGLE skipped the “no drawing from a mapped buffer” check for WebGL on the false assumption that WebGL can’t map buffers — but its map entry points are still reachable via the passthrough decoder, so a WebGL2 draw on a mapped, unaligned buffer could reach an internal staging-buffer reallocation that leaves the decoder’s cached pointer dangling.

Attack Path

  1. WebGL2 context A compromised/cooperating renderer establishes a WebGL2 context on the ANGLE-Vulkan passthrough path (inferred renderer control).
  2. Create device-local buffer glBufferData with GL_STATIC_DRAW to force device-local memory requiring a host-visible staging buffer.
  3. Map a small range Issue MapBufferRange for a tiny region (e.g. 4 bytes) for writing; the decoder caches the staging buffer’s map_ptr.
  4. Unaligned attribute glVertexAttribPointer with an unaligned stride/offset to force ANGLE’s CPU conversion fallback.
  5. Draw despite mapping DrawArrays; pre-patch, the WebGL exemption lets validation pass. convertVertexBufferCPU maps the whole buffer, and allocStagingBuffer releases the 4-byte staging buffer (the free), leaving map_ptr dangling into the VMA pool (inferred; sink not in this diff).
  6. Reclaim and write Groom the VMA heap to reuse the freed slot with a sensitive Vulkan object, then UnmapBuffer; DoUnmapBuffer memcpys attacker shared-memory data into the dangling map_ptr, overwriting the reclaimed object (primitive: controlled write into freed/reused GPU memory → potential control-flow hijack).

Impact Assessment

The affected process is the GPU process on the ANGLE-Vulkan passthrough decoder; the trigger is reachable from a WebGL2 page with no privilege beyond issuing GL commands, though the full exploit requires an unaligned-attribute reallocation and VMA grooming (advisory AC:H). The security-relevant primitive described by the advisory is an arbitrary/controlled write into freed-then-reallocated VMA memory (a UAF write), with VMA memory outside PartitionAlloc so MiraclePtr does not apply; the advisory rates it CVSS 8.3 with a sandbox-escape goal. Honest scope note: this diff only restores the mapped-buffer draw check (removing the UAF precondition); it does not itself contain the free or the dangling 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.cpp
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp

Audit Directions

  • WebGL-exempted validation
    Grep ANGLE for other !extensions.webglCompatibilityANGLE (or webglCompatibility-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 reallocation
    Review BufferVk::allocStagingBuffer/mapForReadAccessOnly and passthrough MappedBuffer::map_ptr lifetime: any decoder-cached raw VMA pointer that ANGLE may invalidate via an internal release()/reallocation during conversion or defragmentation.
  • Map/unmap exposure on WebGL
    Consider the alternate hardening (blocking MapBufferRange/UnmapBuffer for all WebGL contexts in the decoder or ValidateMapBufferRangeBase) and audit whether other internal features rely on those entry points for WebGL contexts.
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