Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GFX
DescriptionUse after free in GFX
ComponentGFX
Bug ClassUAF
Tracker502104354
Fix commitf88c4aae15d5 (chromium/src) +27/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
TEST
ui/gfx/mac/io_surface_unittest.cc
modified

Files Changed

  • ui/gfx/mac/io_surface.cc
  • ui/gfx/mac/io_surface_unittest.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • ui/gfx/ca_layer_params.h
  • ui/gfx/mojom/ca_layer_params_mojom_traits.cc
  • ui/accelerated_widget_mac/accelerated_widget_mac.mm
  • components/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.

  1. The attacker allocates a Mach receive right and sends a corresponding send right to the Browser process via a CALayerParams Mojo message.
  2. 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.
  3. The Browser process deserializes the Mojo message. PlatformHandle::ReleaseMachSendRight considers dead names valid, so the dead name is safely placed into a temporary CALayerParams struct.
  4. The Browser calls AcceleratedWidgetMac::UpdateCALayerTree (ui/accelerated_widget_mac/accelerated_widget_mac.mm), which executes last_ca_layer_params_ = ca_layer_params;.
  5. The copy-assignment of CALayerParams triggers the copy-assignment of its ScopedRefCountedIOSurfaceMachPort member. This invokes IOSurfaceMachPortTraits::Retain on the dead name.
  6. mach_port_mod_refs fails because it operates on a dead name right, not a send right. However, Retain unconditionally returns the dead name, which last_ca_layer_params_ adopts.
  7. The temporary CALayerParams struct goes out of scope. Its ScopedTypeRef calls Traits::Release, which executes mach_port_deallocate on the dead name. This frees the dead name entirely, returning the Mach port name (an integer) to the kernel’s freelist.
  8. last_ca_layer_params_ now holds a dangling Mach port name. When it is next updated or destroyed, it will call mach_port_deallocate on 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.

View on issue tracker