Overview

High
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
Tracker513925114
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 <geofflang@chromium.org>
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 <kbr@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
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 vm...@google.com

Validation bypass and state tracking flaw in ANGLE Transform Feedback

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: A validation gap in ANGLE’s ResumeTransformFeedback allows the GPU to write to host-mapped buffers. This issue is compounded by a state tracking flaw that incorrectly decrements buffer binding counts when an active transform feedback object is unbound, bypassing mapping protections.

Affected files:

  • third_party/angle/src/libANGLE/validationES3.cpp
  • third_party/angle/src/libANGLE/State.cpp
  • third_party/angle/src/libANGLE/TransformFeedback.cpp
  • third_party/angle/src/libANGLE/Buffer.cpp
  • third_party/angle/src/libANGLE/validationES2.cpp
  • third_party/angle/src/libANGLE/validationES.cpp

Estimated timestamp from git blame: 2018-01-04

Summary

A potential security vulnerability exists in ANGLE’s implementation of Transform Feedback (XFB) validation and state tracking. A validation gap in ResumeTransformFeedback allows transform feedback to resume even if output buffers are mapped by the host. Furthermore, a logic error in state tracking allows an attacker to bypass the mapping protections intended to prevent this state.

Root Cause Analysis

The vulnerability is driven by two primary flaws in third_party/angle/src/libANGLE/:

  1. Validation Gap in ValidateResumeTransformFeedback: In validationES3.cpp, the ValidateBeginTransformFeedback function correctly iterates through indexed buffers to ensure they are not mapped (via buffer->isMapped()) or double-bound. However, ValidateResumeTransformFeedback performs no such checks. If a buffer is mapped while transform feedback is paused, resuming the operation proceeds without error, violating GLES 3.0 requirements.

  2. State Tracking Flaw in onBindingChanged: When a transform feedback object is unbound from the context (e.g., by binding a different TF object), State::setTransformFeedbackBinding calls TransformFeedback::onBindingChanged(context, false). This function decrements the mTransformFeedbackIndexedBindingCount for all associated buffers in Buffer.cpp.

    Crucially, this decrement occurs even if the transform feedback object is still “Active” (e.g., paused). Because ValidateMapBufferBase (the validator for glMapBufferRange) relies on this counter to identify buffers currently in use by the transform feedback subsystem, an attacker can unbind the active TF object to drop the count to zero and successfully map the buffer.

Potential Impact

By combining these flaws, an attacker can reach a state where the GPU writes data into a buffer that is simultaneously mapped and accessible by the host. Per the OpenGL ES 3.0 specification (§6.3.2), writing to a mapped buffer via transform feedback is undefined behavior. This can lead to driver-level memory corruption or instability. On platforms like Android, where the GPU process is unsandboxed, this represents a significant path for privilege escalation from a compromised renderer.

Suggested Reproduction Steps (Potential)

  1. Create two Transform Feedback objects (TF1, TF2) and a buffer (B).
  2. Bind B to TF1 and call glBeginTransformFeedback and then glPauseTransformFeedback.
  3. Bind TF2. This triggers the state tracking flaw, incorrectly decrementing B’s transform feedback binding count.
  4. Call glMapBufferRange on buffer B. This succeeds because B’s binding count is now 0 and TF2 is not active.
  5. Bind TF1 again.
  6. Call glResumeTransformFeedback. This succeeds due to the validation gap in ValidateResumeTransformFeedback failing to check the mapping state of B.
  7. Issue a draw call. The GPU writes to the host-mapped buffer B.
  1. Enhance Validation: Update ValidateResumeTransformFeedback in validationES3.cpp to include a loop that verifies none of the transform feedback object’s indexed buffers are mapped.
  2. Correct State Tracking: Modify TransformFeedback::onBindingChanged to ensure that a buffer’s transform feedback binding count is only decremented if the parent transform feedback object is no longer isActive().

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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