Critical watch ~ plausible CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassInput Validation
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible (tier revised from patch-now)
Reviewer notes: Precondition overstated: these commands are not WebGL-exposed, so emitting them needs a compromised renderer, not default web content. GPU-process corruption not shown in diff (service handlers absent); fix removes the surface.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker514769383
Fix commit7cc1fc67ea1a (chromium/src) +484/-3748
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-19 (per vendor advisory)
Fix landed2026-05-22 (public git)
Disclosed2026-06-30
Patch gap39 days — fix public in source before the advisory

Background

Command buffer / gles2 cmd decoder
the GPU-process component that decodes and executes GL commands serialized by an untrusted renderer over shared memory.
Buffer mapping commands
MapBufferRange, UnmapBuffer, and FlushMappedBufferRange, which let a client obtain a CPU-visible pointer into a GL buffer’s storage over a caller-supplied offset/size/access and shared-memory region.
`GetBufferSubDataCHROMIUM`
the new, narrower Custom command (data_transfer_methods: ['shm'], cmd_args: GLenumBufferTarget target, GLintptr offset, GLsizeiptr size, uint32_t data_shm_id, uint32_t data_shm_offset) added to perform the buffer mapping entirely inside the GPU process on behalf of WebGL getBufferSubData.
ANGLE
the GL translation layer the GPU process uses to service these commands, named as the vulnerable component with bug class InputValidation.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Precondition overstated: these commands are not WebGL-exposed, so emitting them needs a compromised renderer, not default web content. GPU-process corruption not shown in diff (service handlers absent); fix removes the surface.

The commit message states that the buffer mapping commands (MapBufferRange, UnmapBuffer, FlushMappedBufferRange) are “not exposed on WebGL and un-used except to implement the WebGL getBufferSubData command,” yet they remained decodable by the gles2 cmd decoder and thus reachable by any renderer that serialized them. The message further states these calls “are particularly difficult to validate and are commonly targeted in compromised renderer attacks on the GPU process,” identifying insufficient validation of the attacker-controlled offset/size/access and shared-memory arguments against the bound buffer as the violated invariant. Rather than harden the map/unmap validation, the fix eliminates the attack surface entirely: it removes the client-side entry points GLES2MapBufferRange, GLES2UnmapBuffer, GLES2FlushMappedBufferRange and the MapBufferRange command helper, drops their name-table registrations, and replaces the one legitimate consumer with GetBufferSubDataCHROMIUM, which does the mapping inside the GPU process so the renderer never drives a raw map/unmap. The removed regression test UnmapBufferWithOffsetFreesCorrectBlock — annotated as “a regression test for a use-after-free bug” in the client FencedAllocator when unmapping at a non-zero offset — is deleted alongside the API it exercised. Because the actual service-side decoder handlers and their validation logic are not present in the shown hunks, the precise validation defect and its memory-corruption sink are established by the commit’s own description rather than by the diff.

The fix works by removing the difficult-to-validate command surface from the decoder and confining the mapping operation to a specialized in-process command.

Key insight
The core mistake was exposing general-purpose, hard-to-validate buffer map/unmap commands to an untrusted renderer when only a single narrow WebGL operation needed them; the fix removes those commands from the decoder and replaces them with GetBufferSubDataCHROMIUM, which performs the mapping inside the GPU process where the renderer cannot supply the risky arguments.

Attack Path

  1. Compromised renderer Per the advisory (“compromised renderer attacks on the GPU process”), the attacker first controls a renderer and can therefore serialize arbitrary command-buffer commands to the GPU process. (inferred as the stated precondition rather than proven by the diff.)
  2. Emit a buffer mapping command The renderer issues MapBufferRange (or UnmapBuffer / FlushMappedBufferRange), which before this patch was registered and decodable by the gles2 cmd decoder, supplying attacker-controlled target, offset, size, access, and shared-memory ids/offsets.
  3. Insufficient validation in the GPU process The decoder’s validation of these arguments against the bound buffer is insufficient (commit message: “particularly difficult to validate”), so the map/unmap proceeds on out-of-range or otherwise malformed parameters. (inferred: the exact check and its handler are not in the shown hunks.)
  4. Memory corruption sink This yields out-of-bounds or use-after-free memory access in the GPU process; the deleted UnmapBufferWithOffsetFreesCorrectBlock test points to a FencedAllocator use-after-free reachable via unmapping at a non-zero offset, though that test exercises client-side allocator state. (inferred: the specific sink, offset, and object are not contained in the diff.)
  5. Downstream escalation Corruption of GPU-process memory could be leveraged toward a sandbox escape or code execution. (inferred; neither asserted by the advisory text provided nor demonstrated by the patch.)

Impact Assessment

An attacker who already controls a renderer gains the ability to drive difficult-to-validate buffer map/unmap commands into the GPU process, where insufficient input validation (bug class InputValidation in ANGLE) can lead to memory corruption in the GPU process; the precondition is a compromised renderer able to emit command-buffer commands. Any further escalation to a sandbox escape or remote code execution is (inferred) and not established by the diff. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/client/gles2_cmd_helper_autogen.h
modified

Files Changed

  • gpu/BUILD.gn
  • gpu/GLES2/gl2chromium_autogen.h
  • gpu/GLES2/gl2extchromium.h
  • gpu/command_buffer/build_gles2_cmd_buffer.py
  • gpu/command_buffer/client/gles2_c_lib_autogen.h
  • gpu/command_buffer/client/gles2_cmd_helper_autogen.h

Audit Directions

  • Renderer-controllable offset/size against buffer bounds
    Audit every command-buffer command that accepts a client GLintptr offset / GLsizeiptr size plus a shared-memory region, verifying the decoder bounds-checks the range against the bound buffer’s size before mapping or copying.
  • Broad GL commands retained only for one narrow client use
    Look for other decoder-exposed commands that WebGL does not use but that survive solely to back a single CHROMIUM helper; these are candidates for the same “remove and specialize in the GPU process” treatment.
  • Client-side mapped-memory allocator lifetime
    Review FencedAllocator and mapped-memory free paths (e.g. freeing by offset, as flagged by the deleted UnmapBufferWithOffsetFreesCorrectBlock test) for use-after-free when unmapping at non-zero offsets or after buffer data replacement/deletion.
From 7cc1fc67ea1af8bb1d39eb510d28b6687d0346f5 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Fri, 22 May 2026 15:42:22 -0700
Subject: [PATCH] Remove buffer mapping commands from the gles2 cmd decoder

Buffer mapping commands are not exposed on WebGL and un-used except to
implement the WebGL getBufferSubData command. Implement it with a
specialized command which does the mapping in the GPU process.

These calls are particularly difficult to validate and are commonly
targeted in compromised renderer attacks on the GPU process.

Fixed: chromium:514769383
Fixed: chromium:513925114
Change-Id: I49d57ad190722c0bfb2d2a5535ee5d50819d825c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7863315
Reviewed-by: Kenneth Russell <[email protected]>
Commit-Queue: Geoff Lang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1635266}
---

diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn
index f80abce..225e9187 100644
--- a/gpu/BUILD.gn
+++ b/gpu/BUILD.gn
@@ -243,11 +243,11 @@
     "command_buffer/tests/gl_ext_multisample_compatibility_unittest.cc",
     "command_buffer/tests/gl_ext_srgb_unittest.cc",
     "command_buffer/tests/gl_ext_window_rectangles_unittest.cc",
+    "command_buffer/tests/gl_get_buffer_sub_data_unittest.cc",
     "command_buffer/tests/gl_invalidate_framebuffer_unittest.cc",
     "command_buffer/tests/gl_lose_context_chromium_unittest.cc",
     "command_buffer/tests/gl_manager.cc",
     "command_buffer/tests/gl_manager.h",
-    "command_buffer/tests/gl_map_buffer_range_unittest.cc",
     "command_buffer/tests/gl_object_bindings_unittest.cc",
     "command_buffer/tests/gl_offscreen_surface_unittest.cc",
     "command_buffer/tests/gl_oob_attrib_unittest.cc",
@@ -268,7 +268,6 @@
     "command_buffer/tests/gl_texture_storage_unittest.cc",
     "command_buffer/tests/gl_unallocated_texture_unittest.cc",
     "command_buffer/tests/gl_unittest.cc",
-    "command_buffer/tests/gl_vertex_arrays_unittest.cc",
     "command_buffer/tests/gl_virtual_contexts_ext_window_rectangles_unittest.cc",
     "command_buffer/tests/gl_virtual_contexts_unittest.cc",
     "command_buffer/tests/gl_webgl_multi_draw_test.cc",
diff --git a/gpu/GLES2/gl2chromium_autogen.h b/gpu/GLES2/gl2chromium_autogen.h
index 18a6fa5e..1a7cbdb0 100644
--- a/gpu/GLES2/gl2chromium_autogen.h
+++ b/gpu/GLES2/gl2chromium_autogen.h
@@ -283,9 +283,6 @@
 #define glUnmapBufferCHROMIUM GLES2_GET_FUN(UnmapBufferCHROMIUM)
 #define glMapBufferSubDataCHROMIUM GLES2_GET_FUN(MapBufferSubDataCHROMIUM)
 #define glUnmapBufferSubDataCHROMIUM GLES2_GET_FUN(UnmapBufferSubDataCHROMIUM)
-#define glMapBufferRange GLES2_GET_FUN(MapBufferRange)
-#define glUnmapBuffer GLES2_GET_FUN(UnmapBuffer)
-#define glFlushMappedBufferRange GLES2_GET_FUN(FlushMappedBufferRange)
 #define glMapTexSubImage2DCHROMIUM GLES2_GET_FUN(MapTexSubImage2DCHROMIUM)
 #define glUnmapTexSubImage2DCHROMIUM GLES2_GET_FUN(UnmapTexSubImage2DCHROMIUM)
 #define glGetRequestableExtensionsCHROMIUM \
@@ -318,6 +315,7 @@
 #define glFlushDriverCachesCHROMIUM GLES2_GET_FUN(FlushDriverCachesCHROMIUM)
 #define glGetLastFlushIdCHROMIUM GLES2_GET_FUN(GetLastFlushIdCHROMIUM)
 #define glSetActiveURLCHROMIUM GLES2_GET_FUN(SetActiveURLCHROMIUM)
+#define glGetBufferSubDataCHROMIUM GLES2_GET_FUN(GetBufferSubDataCHROMIUM)
 #define glContextVisibilityHintCHROMIUM \
   GLES2_GET_FUN(ContextVisibilityHintCHROMIUM)
 #define glGetGraphicsResetStatusKHR GLES2_GET_FUN(GetGraphicsResetStatusKHR)
diff --git a/gpu/GLES2/gl2extchromium.h b/gpu/GLES2/gl2extchromium.h
index 0628039..2aa0406d 100644
--- a/gpu/GLES2/gl2extchromium.h
+++ b/gpu/GLES2/gl2extchromium.h
@@ -52,6 +52,17 @@
 #endif
 #endif  /* GL_CHROMIUM_pixel_transfer_buffer_object */
 
+#ifdef GL_GLEXT_PROTOTYPES
+GL_APICALL void GL_APIENTRY glGetBufferSubDataCHROMIUM(GLenum target,
+                                                       GLintptr offset,
+                                                       GLsizeiptr size,
+                                                       void* data);
+#endif
+typedef void(GL_APIENTRY PFNGLGETBUFFERSUBDATACHROMIUM)(GLenum target,
+                                                        GLintptr offset,
+                                                        GLsizeiptr size,
+                                                        void* data);
+
 /* GL_CHROMIUM_deschedule */
 #ifndef GL_CHROMIUM_deschedule
 #define GL_CHROMIUM_deschedule 1
diff --git a/gpu/command_buffer/build_gles2_cmd_buffer.py b/gpu/command_buffer/build_gles2_cmd_buffer.py
index e6a366b..67300ec 100755
--- a/gpu/command_buffer/build_gles2_cmd_buffer.py
+++ b/gpu/command_buffer/build_gles2_cmd_buffer.py
@@ -2283,6 +2283,16 @@
     'expectation': False,
     'shadowed': True,
   },
+    'GetBufferSubDataCHROMIUM': {
+    'type': 'Custom',
+    'data_transfer_methods': ['shm'],
+    'impl_func': False,
+    'client_test': False,
+    'cmd_args':
+        'GLenumBufferTarget target, GLintptr offset, GLsizeiptr size,'
+        'uint32_t data_shm_id, uint32_t data_shm_offset',
+    'trace_level': 1,
+  },
   'GetError': {
     'type': 'Is',
     'decoder_func': 'GetErrorState()->GetGLError',
diff --git a/gpu/command_buffer/client/gles2_c_lib_autogen.h b/gpu/command_buffer/client/gles2_c_lib_autogen.h
index 640a029..2143483d 100644
--- a/gpu/command_buffer/client/gles2_c_lib_autogen.h
+++ b/gpu/command_buffer/client/gles2_c_lib_autogen.h
@@ -1299,20 +1299,6 @@
 void GL_APIENTRY GLES2UnmapBufferSubDataCHROMIUM(const void* mem) {
   gles2::GetGLContext()->UnmapBufferSubDataCHROMIUM(mem);
 }
-void* GL_APIENTRY GLES2MapBufferRange(GLenum target,
-                                      GLintptr offset,
-                                      GLsizeiptr size,
-                                      GLbitfield access) {
-  return gles2::GetGLContext()->MapBufferRange(target, offset, size, access);
-}
-GLboolean GL_APIENTRY GLES2UnmapBuffer(GLenum target) {
-  return gles2::GetGLContext()->UnmapBuffer(target);
-}
-void GL_APIENTRY GLES2FlushMappedBufferRange(GLenum target,
-                                             GLintptr offset,
-                                             GLsizeiptr size) {
-  gles2::GetGLContext()->FlushMappedBufferRange(target, offset, size);
-}
 void* GL_APIENTRY GLES2MapTexSubImage2DCHROMIUM(GLenum target,
                                                 GLint level,
                                                 GLint xoffset,
@@ -1474,6 +1460,12 @@
 void GL_APIENTRY GLES2SetActiveURLCHROMIUM(const char* url) {
   gles2::GetGLContext()->SetActiveURLCHROMIUM(url);
 }
+void GL_APIENTRY GLES2GetBufferSubDataCHROMIUM(GLenum target,
+                                               GLintptr offset,
+                                               GLsizeiptr size,
+                                               void* data) {
+  gles2::GetGLContext()->GetBufferSubDataCHROMIUM(target, offset, size, data);
+}
 void GL_APIENTRY GLES2ContextVisibilityHintCHROMIUM(GLboolean visibility) {
   gles2::GetGLContext()->ContextVisibilityHintCHROMIUM(visibility);
 }
@@ -2783,18 +2775,6 @@
         reinterpret_cast<GLES2FunctionPointer>(glUnmapBufferSubDataCHROMIUM),
     },
     {
-        "glMapBufferRange",
-        reinterpret_cast<GLES2FunctionPointer>(glMapBufferRange),
-    },
-    {
-        "glUnmapBuffer",
-        reinterpret_cast<GLES2FunctionPointer>(glUnmapBuffer),
-    },
-    {
-        "glFlushMappedBufferRange",
-        reinterpret_cast<GLES2FunctionPointer>(glFlushMappedBufferRange),
-    },
-    {
         "glMapTexSubImage2DCHROMIUM",
         reinterpret_cast<GLES2FunctionPointer>(glMapTexSubImage2DCHROMIUM),
     },
@@ -2905,6 +2885,10 @@
         reinterpret_cast<GLES2FunctionPointer>(glSetActiveURLCHROMIUM),
     },
     {
+        "glGetBufferSubDataCHROMIUM",
+        reinterpret_cast<GLES2FunctionPointer>(glGetBufferSubDataCHROMIUM),
+    },
+    {
         "glContextVisibilityHintCHROMIUM",
         reinterpret_cast<GLES2FunctionPointer>(glContextVisibilityHintCHROMIUM),
     },
diff --git a/gpu/command_buffer/client/gles2_cmd_helper_autogen.h b/gpu/command_buffer/client/gles2_cmd_helper_autogen.h
index dcaa7e6b..c6b0c07e 100644
--- a/gpu/command_buffer/client/gles2_cmd_helper_autogen.h
+++ b/gpu/command_buffer/client/gles2_cmd_helper_autogen.h
@@ -2524,36 +2524,6 @@
   }
 }
 
-void MapBufferRange(GLenum target,
-                    GLintptr offset,
-                    GLsizeiptr size,
-                    GLbitfield access,
-                    uint32_t data_shm_id,
-                    uint32_t data_shm_offset,
-                    uint32_t result_shm_id,
-                    uint32_t result_shm_offset) {
-  gles2::cmds::MapBufferRange* c = GetCmdSpace<gles2::cmds::MapBufferRange>();
-  if (c) {
-    c->Init(target, offset, size, access, data_shm_id, data_shm_offset,
-            result_shm_id, result_shm_offset);
-  }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/client/gles2_implementation_unittest.cc b/gpu/command_buffer/client/gles2_implementation_unittest.cc
index e7e8e081..51380ad6c 100644
--- a/gpu/command_buffer/client/gles2_implementation_unittest.cc
+++ b/gpu/command_buffer/client/gles2_implementation_unittest.cc
@@ -3505,158 +3505,6 @@
   UNSAFE_TODO(EXPECT_EQ(0, memcmp(&expected, commands_, sizeof(expected))));
 }
 
-TEST_F(GLES2ImplementationTest, MapBufferRangeUnmapBufferWrite) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(1)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_WRITE_BIT);
-  EXPECT_TRUE(mem != nullptr);
-
-  EXPECT_TRUE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-}
-
-TEST_F(GLES2ImplementationTest, MapBufferRangeWriteWithInvalidateBit) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(1)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  GLsizeiptr kSize = 64;
-  void* mem = gl_->MapBufferRange(
-      GL_ARRAY_BUFFER, 10, kSize,
-      GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
-  EXPECT_TRUE(mem != nullptr);
-  std::vector<int8_t> zero(kSize);
-  UNSAFE_TODO(memset(&zero[0], 0, kSize));
-  UNSAFE_TODO(EXPECT_EQ(0, memcmp(mem, &zero[0], kSize)));
-}
-
-TEST_F(GLES2ImplementationTest, MapBufferRangeWriteWithGLError) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  // Return a result of 0 to indicate an GL error.
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(0)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_WRITE_BIT);
-  EXPECT_TRUE(mem == nullptr);
-}
-
-TEST_F(GLES2ImplementationTest, MapBufferRangeUnmapBufferRead) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(1)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_READ_BIT);
-  EXPECT_TRUE(mem != nullptr);
-
-  EXPECT_TRUE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-}
-
-TEST_F(GLES2ImplementationTest, MapBufferRangeReadWithGLError) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  // Return a result of 0 to indicate an GL error.
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(0)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_READ_BIT);
-  EXPECT_TRUE(mem == nullptr);
-}
-
-TEST_F(GLES2ImplementationTest, UnmapBufferFails) {
-  // No bound buffer.
-  EXPECT_FALSE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-  EXPECT_EQ(GL_INVALID_OPERATION, CheckError());
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  // Buffer is unmapped.
-  EXPECT_FALSE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-  EXPECT_EQ(GL_INVALID_OPERATION, CheckError());
-}
-
-TEST_F(GLES2ImplementationTest, BufferDataUnmapsDataStore) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(1)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_WRITE_BIT);
-  EXPECT_TRUE(mem != nullptr);
-
-  std::vector<uint8_t> data(16);
-  // BufferData unmaps the data store.
-  gl_->BufferData(GL_ARRAY_BUFFER, 16, &data[0], GL_STREAM_DRAW);
-
-  EXPECT_FALSE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-  EXPECT_EQ(GL_INVALID_OPERATION, CheckError());
-}
-
-TEST_F(GLES2ImplementationTest, DeleteBuffersUnmapsDataStore) {
-  ExpectedMemoryInfo result =
-      GetExpectedResultMemory(sizeof(cmds::MapBufferRange::Result));
-
-  EXPECT_CALL(*command_buffer(), OnFlush())
-      .WillOnce(SetMemory(result.ptr, uint32_t(1)))
-      .RetiresOnSaturation();
-
-  GLuint buffer_id = 0;
-  gl_->GenBuffers(1, &buffer_id);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer_id);
-
-  void* mem = gl_->MapBufferRange(GL_ARRAY_BUFFER, 10, 64, GL_MAP_WRITE_BIT);
-  EXPECT_TRUE(mem != nullptr);
-
-  std::vector<uint8_t> data(16);
-  // DeleteBuffers unmaps the data store.
-  gl_->DeleteBuffers(1, &buffer_id);
-
-  EXPECT_FALSE(gl_->UnmapBuffer(GL_ARRAY_BUFFER));
-  EXPECT_EQ(GL_INVALID_OPERATION, CheckError());
-}
-
 TEST_F(GLES2ImplementationTest, GetInternalformativ) {
   const GLint kNumSampleCounts = 8;
   struct Cmds {
@@ -3928,144 +3776,6 @@
   UNSAFE_TODO(EXPECT_EQ(0, memcmp(&expected, commands_, sizeof(expected))));
 }
 
-// Test that UnmapBuffer on a readback buffer with a non-zero offset
-// doesn't erroneously free adjacent blocks in FencedAllocator.
-// This is a regression test for a use-after-free bug.
-TEST_F(GLES2ImplementationTest, UnmapBufferWithOffsetFreesCorrectBlock) {
-  // Create two readback buffers.
-  std::array<GLuint, 2> buffers;
-  gl_->GenBuffers(buffers.size(), buffers.data());
-
-  const GLsizeiptr kBufferSize = 64;
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[0]);
-  gl_->BufferData(GL_ARRAY_BUFFER, kBufferSize, nullptr, GL_STREAM_READ);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[1]);
-  gl_->BufferData(GL_ARRAY_BUFFER, kBufferSize, nullptr, GL_STREAM_READ);
-
-  // Trigger shadow copy allocation by starting a readback query.
-  GLuint query;
-  gl_->GenQueriesEXT(1, &query);
-
-  // We need to satisfy the expectations for BeginQueryEXT
-  EXPECT_CALL(*command_buffer(), OnFlush()).Times(testing::AnyNumber());
-
-  gl_->BeginQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM, query);
-  gl_->EndQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM);
-
-  // Simulate query completion to update tracker serials.
-  QueryTracker::Query* q = GetQuery(query);
-  ASSERT_TRUE(q);
-  // Mark as processed by service
-  SetQueryProcessCount(q, q->submit_count());
-  // Trigger callback
-  bool flush_if_pending = false;
-  EXPECT_TRUE(q->CheckResultsAvailable(helper_, flush_if_pending));
-
-  // Map buffer 1 at offset 0.
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[1]);
-  void* addr2 = gl_->MapBufferRange(GL_ARRAY_BUFFER, 0, 1, GL_MAP_READ_BIT);
-  ASSERT_TRUE(addr2);
-
-  // Map buffer 0 with non-zero offset.
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[0]);
-  const GLintptr kOffset = 16;
-  void* addr1_with_offset =
-      gl_->MapBufferRange(GL_ARRAY_BUFFER, kOffset, 1, GL_MAP_READ_BIT);
-  ASSERT_TRUE(addr1_with_offset);
-
-  // Unmap buffer 0.
-  // If the bug exists, this will erroneously free the block for buffer 1
-  // because it calls FreePendingToken with addr1_with_offset, and
-  // FencedAllocator::GetBlockByOffset(16) will resolve to the next block
-  // (buffer 1).
-  gl_->UnmapBuffer(GL_ARRAY_BUFFER);
-
-  // Check if buffer 1's shadow memory was incorrectly freed.
-  int32_t token = 0;
-  FencedAllocator::State state2 =
-      mapped_memory()->GetPointerStatusForTest(addr2, &token);
-  EXPECT_EQ(FencedAllocator::IN_USE, state2);
-
-  // Clean up buffer 1
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[1]);
-  gl_->UnmapBuffer(GL_ARRAY_BUFFER);
-}
-
-// Test that deleting a buffer or clearing the mapping map correctly handles
-// shadow buffers without triggering misaligned frees.
-TEST_F(GLES2ImplementationTest, ReadbackShadowMixedCleanup) {
-  std::array<GLuint, 3> buffers;
-  gl_->GenBuffers(buffers.size(), buffers.data());
-
-  // Setup shadow buffers for all 3
-  for (auto buffer : buffers) {
-    gl_->BindBuffer(GL_ARRAY_BUFFER, buffer);
-    gl_->BufferData(GL_ARRAY_BUFFER, 64, nullptr, GL_STREAM_READ);
-  }
-
-  // Trigger shadow allocation
-  GLuint query;
-  gl_->GenQueriesEXT(1, &query);
-  EXPECT_CALL(*command_buffer(), OnFlush()).Times(testing::AnyNumber());
-  gl_->BeginQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM, query);
-  gl_->EndQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM);
-  QueryTracker::Query* q = GetQuery(query);
-  SetQueryProcessCount(q, q->submit_count());
-  bool flush_if_pending = false;
-  EXPECT_TRUE(q->CheckResultsAvailable(helper_, flush_if_pending));
-
-  // Create mixed mappings
-  // Buffer 0: Shadow, Offset 16
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[0]);
-  void* addr0 = gl_->MapBufferRange(GL_ARRAY_BUFFER, 16, 1, GL_MAP_READ_BIT);
-  ASSERT_TRUE(addr0);
-
-  // Buffer 1: Shadow, Offset 0
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[1]);
-  void* addr1 = gl_->MapBufferRange(GL_ARRAY_BUFFER, 0, 1, GL_MAP_READ_BIT);
-  ASSERT_TRUE(addr1);
-
-  // Buffer 2: Shadow, Offset 32
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[2]);
-  void* addr2 = gl_->MapBufferRange(GL_ARRAY_BUFFER, 32, 1, GL_MAP_READ_BIT);
-  ASSERT_TRUE(addr2);
-
-  // Test unmapping a shadow-mapped buffer with offset
-  // This calls RemoveMappedBufferRangeById(buffers[0]) via UnmapBuffer
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[0]);
-  gl_->UnmapBuffer(GL_ARRAY_BUFFER);
-
-  // Test unmapping remaining shadow-mapped buffers
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[1]);
-  gl_->UnmapBuffer(GL_ARRAY_BUFFER);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffers[2]);
-  gl_->UnmapBuffer(GL_ARRAY_BUFFER);
-}
-
-// Test that ClearMappedBufferRangeMap correctly handles shadow-mapped buffers.
-TEST_F(GLES2ImplementationTest, ClearMappedBufferRangeMapShadow) {
-  GLuint buffer;
-  gl_->GenBuffers(1, &buffer);
-  gl_->BindBuffer(GL_ARRAY_BUFFER, buffer);
-  gl_->BufferData(GL_ARRAY_BUFFER, 64, nullptr, GL_STREAM_READ);
-
-  GLuint query;
-  gl_->GenQueriesEXT(1, &query);
-  EXPECT_CALL(*command_buffer(), OnFlush()).Times(testing::AnyNumber());
-  gl_->BeginQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM, query);
-  gl_->EndQueryEXT(GL_READBACK_SHADOW_COPIES_UPDATED_CHROMIUM);
-
-  // Simulate query completion
-  QueryTracker::Query* q = GetQuery(query);
-  ASSERT_TRUE(q);
-  SetQueryProcessCount(q, q->submit_count());
-  bool flush_if_pending = false;
-  EXPECT_TRUE(q->CheckResultsAvailable(helper_, flush_if_pending));
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Missing mapped buffer validation for UBOs and SSBOs in ANGLE draw calls

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: ANGLE’s draw call validation fails to verify that buffers bound as Uniform Buffer Objects (UBOs) or Shader Storage Buffer Objects (SSBOs) are not currently mapped. This oversight violates OpenGL ES invariants and, when combined with Chrome’s passthrough command decoder, allows a compromised renderer to potentially trigger memory corruption in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/validationES.cpp
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc

Estimated timestamp from git blame: 2017-11-03

Summary

A potential validation gap exists in ANGLE where draw and dispatch call validation fails to ensure that bound uniform and shader storage buffers are not currently mapped. While ANGLE performs these checks for vertex and index buffers, it omits them for indexed buffer bindings. This allows a compromised renderer to use mapped buffers during drawing operations, leading to backend-specific undefined behavior or memory corruption.

Technical Details

The OpenGL ES specification prohibits the use of a buffer in a draw call if it is currently mapped. ANGLE enforces this for most buffer targets, but has a notable gap for UBOs and SSBOs:

  1. Incomplete Validation: In third_party/angle/src/libANGLE/validationES.cpp, the ValidateProgramDrawStates function (lines 661-804) iterates through bound uniform blocks to validate their state (e.g., size requirements and transform feedback conflicts). However, it does not check if the bound uniformBuffer is currently mapped. Furthermore, SSBO validation is entirely absent from this function.
  2. Missing Global Checks: The ValidateDrawStates function, which is the primary entry point for draw-time validation, checks for mapped vertex buffers via vertexArray->hasInvalidMappedArrayBuffer() (line 4054), but performs no equivalent check for indexed buffer bindings.
  3. Command Decoder Interaction: In WebGL2 contexts, ANGLE might assume that buffer mapping is restricted. However, Chrome’s passthrough command decoder explicitly exposes cmds::MapBufferRange to WebGL2 contexts in gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc (line 1364). This allows a compromised renderer to map a buffer and subsequently use it in a draw call, bypassing the intended safety invariants.

Impact

Violating the mapping invariant can lead to serious memory safety issues in the GPU backend. For example, in the D3D11 backend (Buffer11.cpp), using a mapped buffer as a UBO could previously lead to a Use-After-Free (UAF) if the underlying storage was evicted from the internal range cache while still mapped (tracked in anglebug.com/505771894). While a partial mitigation exists in the D3D11 backend to return an error, the Vulkan backend lacks equivalent safeguards and could experience memory corruption or race conditions if the GPU reads from memory that the CPU is concurrently modifying.

Potential Reproduction Steps

Note: These steps are based on static analysis and have not been verified with a running proof-of-concept.

From a compromised renderer with a WebGL2 context using the passthrough decoder:

  1. Create a GL buffer and populate it with data.
  2. Bind the buffer as a uniform buffer: glBindBufferBase(GL_UNIFORM_BUFFER, 0, buf);.
  3. Map the buffer via the command buffer: gl.mapBufferRange(gl.UNIFORM_BUFFER, ...);.
  4. Execute a draw call: gl.drawArrays(...);.
  5. Observe that the draw call passes ANGLE validation despite the UBO being mapped.

Suggested Fix

Update ValidateProgramDrawStates in third_party/angle/src/libANGLE/validationES.cpp to iterate through all active uniform blocks and shader storage blocks in the program executable. For each bound buffer, verify its mapping state by calling isMapped(). If any buffer is mapped and lacks the GL_MAP_PERSISTENT_BIT_EXT access flag, return a kBufferMapped error.

Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049


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.

View on issue tracker