CVE-2026-9936
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTui/gfx/mac/io_surface_unittest.cc |
modified |
Files Changed
ui/gfx/mac/io_surface.ccui/gfx/mac/io_surface_unittest.cc
Patch
From f88c4aae15d560eaf60f64e26b0175ab5b8d5c21 Mon Sep 17 00:00:00 2001 From: Bryan Oltman <[email protected]> Date: Tue, 05 May 2026 12:10:23 -0700 Subject: [PATCH] Fix over-release handling in IOSurfaceMachPortTraits::Retain Retain() now correctly returns MACH_PORT_NULL if mach_port_mod_refs fails, preventing the adoption of unreferenced port names. Fixed: 502104354 Change-Id: If9b731f548e354ca101df659152ac5b6aacf9fd9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7815394 Commit-Queue: Bryan Oltman <[email protected]> Reviewed-by: Mark Mentovai <[email protected]> Reviewed-by: Avi Drissman <[email protected]> Cr-Commit-Position: refs/heads/main@{#1625633} --- diff --git a/ui/gfx/mac/io_surface.cc b/ui/gfx/mac/io_surface.cc index 9b0a2f4..f138f75 100644 --- a/ui/gfx/mac/io_surface.cc +++ b/ui/gfx/mac/io_surface.cc @@ -10,6 +10,7 @@ #include <stdint.h> #include "base/apple/mach_logging.h" +#include "base/apple/scoped_mach_port.h" #include "base/bits.h" #include "base/command_line.h" #include "base/feature_list.h" @@ -77,11 +78,7 @@ // static mach_port_t IOSurfaceMachPortTraits::Retain(mach_port_t port) { - kern_return_t kr = - mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_SEND, 1); - MACH_LOG_IF(ERROR, kr != KERN_SUCCESS, kr) - << "IOSurfaceMachPortTraits::Retain mach_port_mod_refs"; - return port; + return base::apple::RetainMachSendRight(port).release(); } // static diff --git a/ui/gfx/mac/io_surface_unittest.cc b/ui/gfx/mac/io_surface_unittest.cc index 51d4439..8940bb0 100644 --- a/ui/gfx/mac/io_surface_unittest.cc +++ b/ui/gfx/mac/io_surface_unittest.cc @@ -21,6 +21,31 @@ EXPECT_EQ(IOSurfaceGetHeightOfPlane(io_surface.get(), 1), 50u); } +TEST(IOSurface, MachPortRetainDeadName) { + const mach_port_t task = mach_task_self(); + + // Create a port and give it send rights so that it will transition to a dead + // name when the receive right is removed. + mach_port_t port = MACH_PORT_NULL; + ASSERT_EQ(KERN_SUCCESS, + mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &port)); + ASSERT_EQ(KERN_SUCCESS, + mach_port_insert_right(task, port, port, MACH_MSG_TYPE_MAKE_SEND)); + + // Remove the receive right. + ASSERT_EQ(KERN_SUCCESS, + mach_port_mod_refs(task, port, MACH_PORT_RIGHT_RECEIVE, -1)); + mach_port_type_t port_type = MACH_PORT_TYPE_NONE; + ASSERT_EQ(KERN_SUCCESS, mach_port_type(task, port, &port_type)); + ASSERT_TRUE(port_type & MACH_PORT_TYPE_DEAD_NAME) + << "port should have transitioned to a dead name"; + + // Attempting to retain a dead name fails, `Retain(port)` should return NULL. + mach_port_t result = internal::IOSurfaceMachPortTraits::Retain(port); + EXPECT_EQ(result, static_cast<mach_port_t>(MACH_PORT_NULL)) + << "IOSurface should not retain a dead port"; +} + } // namespace } // namespace gfx
Regression Test / PoC
diff --git a/ui/gfx/mac/io_surface_unittest.cc b/ui/gfx/mac/io_surface_unittest.cc
index 51d4439..8940bb0 100644
--- a/ui/gfx/mac/io_surface_unittest.cc
+++ b/ui/gfx/mac/io_surface_unittest.cc
@@ -21,6 +21,31 @@
EXPECT_EQ(IOSurfaceGetHeightOfPlane(io_surface.get(), 1), 50u);
}
+TEST(IOSurface, MachPortRetainDeadName) {
+ const mach_port_t task = mach_task_self();
+
+ // Create a port and give it send rights so that it will transition to a dead
+ // name when the receive right is removed.
+ mach_port_t port = MACH_PORT_NULL;
+ ASSERT_EQ(KERN_SUCCESS,
+ mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &port));
+ ASSERT_EQ(KERN_SUCCESS,
+ mach_port_insert_right(task, port, port, MACH_MSG_TYPE_MAKE_SEND));
+
+ // Remove the receive right.
+ ASSERT_EQ(KERN_SUCCESS,
+ mach_port_mod_refs(task, port, MACH_PORT_RIGHT_RECEIVE, -1));
+ mach_port_type_t port_type = MACH_PORT_TYPE_NONE;
+ ASSERT_EQ(KERN_SUCCESS, mach_port_type(task, port, &port_type));
+ ASSERT_TRUE(port_type & MACH_PORT_TYPE_DEAD_NAME)
+ << "port should have transitioned to a dead name";
+
+ // Attempting to retain a dead name fails, `Retain(port)` should return NULL.
+ mach_port_t result = internal::IOSurfaceMachPortTraits::Retain(port);
+ EXPECT_EQ(result, static_cast<mach_port_t>(MACH_PORT_NULL))
+ << "IOSurface should not retain a dead port";
+}
+
} // namespace
} // namespace gfx
Original Bug Report
Mach port handle confusion in Browser process via CALayerParams copy-assignment
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A failure to check the return value of mach_port_mod_refs in IOSurfaceMachPortTraits::Retain leads to a Mach port over-release in the browser process. This potential vulnerability can be exploited by a compromised GPU process to achieve a sandbox escape via Mach port handle confusion.
Affected files:
ui/gfx/mac/io_surface.ccui/gfx/ca_layer_params.hui/gfx/mojom/ca_layer_params_mojom_traits.ccui/accelerated_widget_mac/accelerated_widget_mac.mmcomponents/viz/host/host_display_client.cc
Estimated timestamp from git blame: 2016-01-05
Vulnerability Summary
A Mach port over-release vulnerability exists in the browser process due to improper error handling in IOSurfaceMachPortTraits::Retain (ui/gfx/mac/io_surface.cc). The function calls mach_port_mod_refs to increment a send right’s reference count, but returns the port name even if the call fails. This causes base::apple::ScopedTypeRef to adopt a reference it failed to acquire, resulting in an extra mach_port_deallocate. This can lead to handle confusion of Mach port names in the browser process.
Technical Details
In ui/gfx/mac/io_surface.cc, IOSurfaceMachPortTraits::Retain is implemented as follows:
mach_port_t IOSurfaceMachPortTraits::Retain(mach_port_t port) {
kern_return_t kr =
mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_SEND, 1);
MACH_LOG_IF(ERROR, kr != KERN_SUCCESS, kr)
<< "IOSurfaceMachPortTraits::Retain mach_port_mod_refs";
return port;
}
If mach_port_mod_refs fails (for instance, if the port name is a dead name right), the function logs an error but unconditionally returns the port name. base::apple::ScopedTypeRef (which is used by ScopedRefCountedIOSurfaceMachPort) stores this return value during operations like copy assignment and assumes the reference count was successfully incremented.
Attack Vector and Reachability
A compromised GPU process can trigger this vulnerability via the viz::mojom::DisplayClient::OnDisplayReceivedCALayerParams interface.
- The attacker allocates a Mach receive right and sends a corresponding send right to the Browser process via a
CALayerParamsMojo message. - Immediately after queuing the IPC, the attacker destroys the receive right in the GPU process. This causes the send right currently in-flight to the Browser process to become a dead name right.
- The Browser process deserializes the Mojo message.
PlatformHandle::ReleaseMachSendRightconsiders dead names valid, so the dead name is safely placed into a temporaryCALayerParamsstruct. - The Browser calls
AcceleratedWidgetMac::UpdateCALayerTree(ui/accelerated_widget_mac/accelerated_widget_mac.mm), which executeslast_ca_layer_params_ = ca_layer_params;. - The copy-assignment of
CALayerParamstriggers the copy-assignment of itsScopedRefCountedIOSurfaceMachPortmember. This invokesIOSurfaceMachPortTraits::Retainon the dead name. mach_port_mod_refsfails because it operates on a dead name right, not a send right. However,Retainunconditionally returns the dead name, whichlast_ca_layer_params_adopts.- The temporary
CALayerParamsstruct goes out of scope. ItsScopedTypeRefcallsTraits::Release, which executesmach_port_deallocateon the dead name. This frees the dead name entirely, returning the Mach port name (an integer) to the kernel’s freelist. last_ca_layer_params_now holds a dangling Mach port name. When it is next updated or destroyed, it will callmach_port_deallocateon this freed name.
Impact
By leveraging the generation count mechanism of XNU Mach ports, an attacker can arrange for a different, sensitive Mach port (e.g., an IOKit user client or shared memory object) to be allocated at the exact same name as the freed dead name. The subsequent over-release targets this unrelated, newly allocated port, resulting in handle confusion. This primitive is a known vector for achieving a full sandbox escape from the GPU process to the unsandboxed browser process.
Recommendation
Modify IOSurfaceMachPortTraits::Retain to return MACH_PORT_NULL when the reference count cannot be incremented. Alternatively, use the existing base::apple::RetainMachSendRight helper, which correctly handles this failure case:
mach_port_t IOSurfaceMachPortTraits::Retain(mach_port_t port) {
return base::apple::RetainMachSendRight(port).release();
}
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.