High CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
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
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-05-05 (public git)
Disclosed2026-05-27
Patch gap22 days — fix public in source before the advisory

Background

Mach send right / dead name
A Mach port is referenced by a per-task name. A send right lets a task message the port; if the backing receive right is destroyed, the send right transitions to a dead name. Names are refcounted integers that the kernel recycles once their user-reference count hits zero.
`mach_port_mod_refs`
Syscall that adjusts a right’s user-reference count. For a dead name it fails (KERN_INVALID_RIGHT/KERN_INVALID_NAME) because there is no live send right to increment.
`base::apple::RetainMachSendRight`
Existing helper that increments a send right and returns a ScopedMachSendRight that is empty (MACH_PORT_NULL) if the increment failed — i.e. it never hands back a name it did not actually acquire a reference to.
`CALayerParams` / `OnDisplayReceivedCALayerParams`
Browser-process struct (holding a ScopedRefCountedIOSurfaceMachPort) populated from a GPU-process Mojo message and copy-assigned into AcceleratedWidgetMac::last_ca_layer_params_ (per the advisory; copy-assignment invokes Retain).

Root Cause Analysis

The violated invariant is ownership accounting: a ScopedTypeRef-style traits Retain must return a valid port name only if it successfully took a reference, otherwise the scoper adopts a reference it does not own. The vulnerable IOSurfaceMachPortTraits::Retain did 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) << ...; return port; — it logged the failure but unconditionally returned the port name. When port is a dead name, mach_port_mod_refs fails, no reference is taken, yet the returned name is adopted by the destination ScopedRefCountedIOSurfaceMachPort (e.g. during last_ca_layer_params_ = ca_layer_params;). The source temporary and the destination now each believe they own one reference, but only one (or zero) exists. When each later runs Release → mach_port_deallocate, the name is deallocated more times than it was referenced: a Mach-port over-release.

The patch replaces the body with return base::apple::RetainMachSendRight(port).release();. RetainMachSendRight increments the send right and, on failure, yields an empty ScopedMachSendRight; .release() then returns MACH_PORT_NULL rather than the dead name. The destination scoper therefore adopts MACH_PORT_NULL (a no-op to deallocate) instead of a name it never referenced, eliminating the extra deallocate. The new IOSurface.MachPortRetainDeadName unit test allocates a receive right, inserts a send right, removes the receive right to force a dead name, and asserts Retain(deadname) == MACH_PORT_NULL. Note the advisory/metadata label this CWE-416 “Use after free in GFX”; the diff substantiates a Mach-port send-right over-release leading to port-name reuse / handle confusion — a use-after-free of a kernel handle name rather than a heap object. The realized impact is a cross-process handle-confusion sandbox-escape vector, not a classic heap UAF.

Key insight
Retain returned the port name even when it failed to acquire a reference, so a scoped owner adopted an unowned (dead-name) reference and later over-released it.

Attack Path

  1. Send a to-be-dead right A compromised GPU process allocates a Mach receive right, makes a send right, and sends it to the browser in a CALayerParams Mojo message via viz::mojom::DisplayClient::OnDisplayReceivedCALayerParams.
  2. Kill the backing right Immediately after queuing the IPC, destroy the receive right in the GPU process so the in-flight send right becomes a dead name by the time the browser processes it. (inferred timing; the diff proves only the dead-name handling defect.)
  3. Browser deserializes The browser accepts the dead name (dead names are considered valid handles) into a temporary CALayerParams.
  4. Copy-assign triggers Retain AcceleratedWidgetMac::UpdateCALayerTree executes last_ca_layer_params_ = ca_layer_params;; the member ScopedRefCountedIOSurfaceMachPort copy-assignment calls Retain(deadname). Pre-patch, mach_port_mod_refs fails but the dead name is returned and adopted.
  5. Over-release The temporary goes out of scope and Release deallocates the dead name; last_ca_layer_params_ still holds the same (now-freed) name and will deallocate it again on its next update/destroy — a second deallocate of a name with no outstanding reference.
  6. Handle confusion → sandbox escape (inferred) Using Mach name/generation reuse, a different sensitive port (IOKit user client, shared-memory object) is allocated at the recycled name; the stray mach_port_deallocate then over-releases that port, giving cross-process handle confusion — a known GPU→browser sandbox-escape primitive (escalation inferred; the diff proves the over-release, not RCE).

Impact Assessment

The bug is in the unsandboxed browser process but is driven by a compromised/malicious GPU process over the display-client Mojo interface, so it is a sandbox-escape vector (CVSS S:C, scope change). No user privilege is needed beyond GPU-process code execution; the precondition is winning the race to turn an in-flight send right into a dead name before the browser’s copy-assignment. The primitive proven by the diff is a Mach send-right over-release (one extra mach_port_deallocate); escalation to controlled handle confusion and a full sandbox escape is plausible and matches known XNU exploitation patterns but is inferred, not demonstrated by the diff. This is macOS-specific.

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

Audit Directions

  • Other Mach `Retain`/`Traits` implementations
    Audit every ScopedTypeRef/ScopedMachPort traits and ad-hoc mach_port_mod_refs(..., +1) caller that returns a name without checking kr, especially in GPU↔browser IPC deserialization paths.
  • Dead-name handling at trust boundaries
    Review where PlatformHandle::ReleaseMachSendRight and Mojo deserialization accept dead names into refcounted owners, since dead names are the trigger class.
  • Copy/move of handle-owning IPC structs
    Check structs like CALayerParams whose copy/move assignment indirectly calls Retain while being populated from untrusted (GPU/renderer) input.
  • `RetainMachSendRight` adoption everywhere
    Confirm the preferred helper is used uniformly rather than hand-rolled increment+return patterns.
From f88c4aae15d560eaf60f64e26b0175ab5b8d5c21 Mon Sep 17 00:00:00 2001
From: Bryan Oltman <bryanoltman@google.com>
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 <bryanoltman@google.com>
Reviewed-by: Mark Mentovai <mark@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
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 vm...@google.com

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