CVE-2026-13834
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/client/gles2_cmd_helper_autogen.h |
modified |
Files Changed
gpu/BUILD.gngpu/GLES2/gl2chromium_autogen.hgpu/GLES2/gl2extchromium.hgpu/command_buffer/build_gles2_cmd_buffer.pygpu/command_buffer/client/gles2_c_lib_autogen.hgpu/command_buffer/client/gles2_cmd_helper_autogen.h
Patch
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);
- }
Regression Test / PoC
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)
Original Bug Report
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.cppthird_party/angle/src/libANGLE/State.cppthird_party/angle/src/libANGLE/TransformFeedback.cppthird_party/angle/src/libANGLE/Buffer.cppthird_party/angle/src/libANGLE/validationES2.cppthird_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/:
-
Validation Gap in
ValidateResumeTransformFeedback: InvalidationES3.cpp, theValidateBeginTransformFeedbackfunction correctly iterates through indexed buffers to ensure they are not mapped (viabuffer->isMapped()) or double-bound. However,ValidateResumeTransformFeedbackperforms no such checks. If a buffer is mapped while transform feedback is paused, resuming the operation proceeds without error, violating GLES 3.0 requirements. -
State Tracking Flaw in
onBindingChanged: When a transform feedback object is unbound from the context (e.g., by binding a different TF object),State::setTransformFeedbackBindingcallsTransformFeedback::onBindingChanged(context, false). This function decrements themTransformFeedbackIndexedBindingCountfor all associated buffers inBuffer.cpp.Crucially, this decrement occurs even if the transform feedback object is still “Active” (e.g., paused). Because
ValidateMapBufferBase(the validator forglMapBufferRange) 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)
- Create two Transform Feedback objects (
TF1,TF2) and a buffer (B). - Bind
BtoTF1and callglBeginTransformFeedbackand thenglPauseTransformFeedback. - Bind
TF2. This triggers the state tracking flaw, incorrectly decrementingB’s transform feedback binding count. - Call
glMapBufferRangeon bufferB. This succeeds becauseB’s binding count is now 0 andTF2is not active. - Bind
TF1again. - Call
glResumeTransformFeedback. This succeeds due to the validation gap inValidateResumeTransformFeedbackfailing to check the mapping state ofB. - Issue a draw call. The GPU writes to the host-mapped buffer
B.
Recommended Fix
- Enhance Validation: Update
ValidateResumeTransformFeedbackinvalidationES3.cppto include a loop that verifies none of the transform feedback object’s indexed buffers are mapped. - Correct State Tracking: Modify
TransformFeedback::onBindingChangedto ensure that a buffer’s transform feedback binding count is only decremented if the parent transform feedback object is no longerisActive().
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.