Critical chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker514769383
Fix commit7cc1fc67ea1a (chromium/src) +484/-3748
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

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
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