Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Mojo
DescriptionUse after free in Mojo
ComponentMojo
Bug ClassUAF
Tracker483956252
Fix commit3d1b1ed55f61 (chromium/src) +77/-3
CISA KEVNot listed
CreditedPaul Seekamp / nullenc0de
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
if
third_party/ipcz/src/ipcz/message.cc
modified
if
third_party/ipcz/src/ipcz/message_test.cc
modified
TEST_F
third_party/ipcz/src/ipcz/message_test.cc
modified

Files Changed

  • third_party/ipcz/src/ipcz/message.cc
  • third_party/ipcz/src/ipcz/message_test.cc
From 3d1b1ed55f6106412a19ed64d3524172b8358351 Mon Sep 17 00:00:00 2001
From: Daniel Cheng <dcheng@chromium.org>
Date: Thu, 19 Mar 2026 16:51:33 -0700
Subject: [PATCH] Reland "Gracefully handle overlapping driver handle ranges"

This is a reland of commit 94b006154082f83ff8f3ca493311d19d6e8e11bc
This fixes the test to allocate the data storage for handles as an array
of uint8_t, since that's what the mock deserialization routine expects.
This was missed in the initial review since `DCHECK`-enabled builds do
not include `ABSL_ASSERT()`.

gemini-cli provided the diagnosis and initial fix. However, upon further
investigation, the author discovered that `Message::GetArrayView<T>`
does not behave per the author's expectations: the returned span always
contains `ArrayHeader::num_elements`, not `ArrayHeader::num_bytes /
sizeof(T)` elements.

This has several implications:

1. Allocating an array with `AllocateArray<uint16_t>(1)`, and then
   reading it back with `GetArrayView<uint8_t>(...)` will produce a span
   of **one** element.

2. Similarly, changing:
     uint32_t first_object_bytes = in.AllocateArray<16_t>(1);
     in.GetArrayView<uint16_t>(first_object_bytes)[0] = 0x90ab;

   to:
     uint32_t first_object_bytes = in.AllocateArray<uint8_t>(2);
     in.GetArrayView<uint16_t>(first_object_bytes)[0] = 0x90ab;

   as suggested by gemini-cli produces an invalid fix. The returned
   span from `GetArrayView<uint16_t>()` has **two** elements, even
   though it only has logical storage for a single uint16_t element.

   The various asserts in `GetArrayView()` do not catch these issues
   today, for various reasons. This will be separately addressed.

The proper way to do this is to allocate the array as a `uint8_t` and
also read it back as a `uint8_t`. Unfortunately, this means sprinkling
`reinterpret_cast<uint16_t>` in various places but there's no real
alternative in ipcz.

Original change's description:
> Gracefully handle overlapping driver handle ranges
>
> Test originally authored by gemini-cli; substantially rewritten
> afterwards to interoperate better with MockDriver.
>
> Bug: 483956252
> Change-Id: I6e45c05114986853bdc78f2f48b7c37350f11086
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7677890
> Commit-Queue: Daniel Cheng <dcheng@chromium.org>
> Reviewed-by: Andrea Orru <andreaorru@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1601577}

Bug: 483956252
Change-Id: I3f307483a7a00008bfd5f7cafe973ded0bb1c662
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7685275
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Commit-Queue: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1602329}
---

diff --git a/third_party/ipcz/src/ipcz/message.cc b/third_party/ipcz/src/ipcz/message.cc
index 436473c8..270abb5 100644
--- a/third_party/ipcz/src/ipcz/message.cc
+++ b/third_party/ipcz/src/ipcz/message.cc
@@ -131,6 +131,20 @@
     return {};
   }
 
+  // If any driver handles have already claimed, the message is invalid.
+  for (auto i = object_data.first_driver_handle;
+       i < object_data.first_driver_handle + object_data.num_driver_handles;
+       ++i) {
+    if (is_handle_consumed[i]) {
+      return {};
+    }
+  }
+
+  // These two loops cannot be merged: returning early if some handles are
+  // already consumed can result in a handle marked as consumed even though it
+  // was never passed to `DriverObject::Deserialize()`. The cleanup logic in
+  // `DeserializeUnknownType()` would also skip the already-marked handle,
+  // leaking the object's resources.
   for (auto i = object_data.first_driver_handle;
        i < object_data.first_driver_handle + object_data.num_driver_handles;
        ++i) {
diff --git a/third_party/ipcz/src/ipcz/message_test.cc b/third_party/ipcz/src/ipcz/message_test.cc
index 17e73c6c..66f52d4 100644
--- a/third_party/ipcz/src/ipcz/message_test.cc
+++ b/third_party/ipcz/src/ipcz/message_test.cc
@@ -45,7 +45,7 @@
         .WillRepeatedly([&](IpczDriverHandle driver_transport, const void* data,
                             size_t num_bytes, const IpczDriverHandle* handles,
                             size_t num_handles, uint32_t, const void*) {
-          const uint8_t* bytes = static_cast<const uint8_t*>(data);
+          const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data);
           received_messages_.push(
               {{bytes, IPCZ_UNSAFE_TODO(bytes + num_bytes)},
                {handles, IPCZ_UNSAFE_TODO(handles + num_handles)}});
@@ -73,7 +73,7 @@
           if (!data || !handles || data_capacity < 2 || handle_capacity < 1) {
             return IPCZ_RESULT_RESOURCE_EXHAUSTED;
           }
-          static_cast<volatile uint16_t*>(data)[0] =
+          reinterpret_cast<volatile uint16_t*>(data)[0] =
               static_cast<uint16_t>(handle >> 16);
           handles[0] = handle & 0xffff;
           return IPCZ_RESULT_OK;
@@ -94,7 +94,7 @@
           ABSL_ASSERT(num_bytes == 2);
           ABSL_ASSERT(num_handles == 1);
           const uint16_t data_value =
-              static_cast<const volatile uint16_t*>(data)[0];
+              reinterpret_cast<const volatile uint16_t*>(data)[0];
           *handle =
               (static_cast<IpczDriverHandle>(data_value) << 16) | handles[0];
           return IPCZ_RESULT_OK;
@@ -383,6 +383,66 @@
   EXPECT_EQ(kObjectHandle3, out.driver_objects()[2].release());
 }
 
+TEST_F(MessageTest, OverlappingDriverHandles) {
+  Message in(0, 0);
+
+  // Driver objects are serialized as an array of raw bytes and an array of
+  // driver handles. MockDriver uses 32-bit handle values and packs the high 16
+  // bits into the raw bytes and the low 16 bits into the handle value; each
+  // DriverObject is expected to be represented with exactly 2 raw bytes and 1
+  // handle value.
+  uint32_t first_object_bytes = in.AllocateArray<uint8_t>(2);
+  *reinterpret_cast<uint16_t*>(
+      in.GetArrayView<uint8_t>(first_object_bytes).data()) = 0x90ab;
+
+  uint32_t second_object_bytes = in.AllocateArray<uint8_t>(2);
+  *reinterpret_cast<uint16_t*>(
+      in.GetArrayView<uint8_t>(second_object_bytes).data()) = 0x90ab;
+
+  uint32_t object_data_offset = in.AllocateArray<internal::DriverObjectData>(2);
+  in.header().driver_object_data_array = object_data_offset;
+
+  std::vector<IpczDriverHandle> handles = {0x12345678, 0xcdef};
+
+  auto object_data =
+      in.GetArrayView<internal::DriverObjectData>(object_data_offset);
+  // The first driver object data entry references `handles[1]`.
+  object_data[0].driver_data_array = first_object_bytes;
+  object_data[0].first_driver_handle = 1;
+  object_data[0].num_driver_handles = 1;
+
+  // The second driver object data entry references `handles[0]` and
+  // `handles[1]`. This tests that:
+  // - rejection of this entry still frees the resources associated with
+  //   `handles[0]`.
+  // - a second claim on `handles[1]` is rejected.
+  //
+  // This violates MockDriver's expectations of how serialized driver objects
+  // are represented on the wire, but that should not be a problem as the
+  // message should be rejected before trying to call the mock driver's
+  // deserialization method.
+  object_data[1].driver_data_array = second_object_bytes;
+  object_data[1].first_driver_handle = 0;
+  object_data[1].num_driver_handles = 2;
+
+  // This is normally all handled by `DriverTransport::Transmit()`, but this
+  // test manually executes the steps normally taken by `Message::Serialize()`
+  // to build an invalid message.
+  transport().driver_object().driver()->Transmit(
+      transport().driver_object().handle(), in.data_view().data(),
+      in.data_view().size(), handles.data(), handles.size(), IPCZ_NO_FLAGS,
+      nullptr);
+
+  ReceivedMessage serialized = TakeNextReceivedMessage();
+
+  Message out;
+  EXPECT_CALL(driver(), Close(0x12345678, _, _));
+  EXPECT_FALSE(
+      out.DeserializeUnknownType(serialized.AsTransportMessage(), transport()));
+
+  EXPECT_CALL(driver(), Close(0x90abcdef, _, _));
+}
+
 TEST_F(MessageTest, BadEnums) {
   // Out of range enum values should be rejected.
   test::msg::MessageWithEnums m1;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/ipcz/src/ipcz/message_test.cc b/third_party/ipcz/src/ipcz/message_test.cc
index 17e73c6c..66f52d4 100644
--- a/third_party/ipcz/src/ipcz/message_test.cc
+++ b/third_party/ipcz/src/ipcz/message_test.cc
@@ -45,7 +45,7 @@
         .WillRepeatedly([&](IpczDriverHandle driver_transport, const void* data,
                             size_t num_bytes, const IpczDriverHandle* handles,
                             size_t num_handles, uint32_t, const void*) {
-          const uint8_t* bytes = static_cast<const uint8_t*>(data);
+          const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data);
           received_messages_.push(
               {{bytes, IPCZ_UNSAFE_TODO(bytes + num_bytes)},
                {handles, IPCZ_UNSAFE_TODO(handles + num_handles)}});
@@ -73,7 +73,7 @@
           if (!data || !handles || data_capacity < 2 || handle_capacity < 1) {
             return IPCZ_RESULT_RESOURCE_EXHAUSTED;
           }
-          static_cast<volatile uint16_t*>(data)[0] =
+          reinterpret_cast<volatile uint16_t*>(data)[0] =
               static_cast<uint16_t>(handle >> 16);
           handles[0] = handle & 0xffff;
           return IPCZ_RESULT_OK;
@@ -94,7 +94,7 @@
           ABSL_ASSERT(num_bytes == 2);
           ABSL_ASSERT(num_handles == 1);
           const uint16_t data_value =
-              static_cast<const volatile uint16_t*>(data)[0];
+              reinterpret_cast<const volatile uint16_t*>(data)[0];
           *handle =
               (static_cast<IpczDriverHandle>(data_value) << 16) | handles[0];
           return IPCZ_RESULT_OK;
@@ -383,6 +383,66 @@
   EXPECT_EQ(kObjectHandle3, out.driver_objects()[2].release());
 }
 
+TEST_F(MessageTest, OverlappingDriverHandles) {
+  Message in(0, 0);
+
+  // Driver objects are serialized as an array of raw bytes and an array of
+  // driver handles. MockDriver uses 32-bit handle values and packs the high 16
+  // bits into the raw bytes and the low 16 bits into the handle value; each
+  // DriverObject is expected to be represented with exactly 2 raw bytes and 1
+  // handle value.
+  uint32_t first_object_bytes = in.AllocateArray<uint8_t>(2);
+  *reinterpret_cast<uint16_t*>(
+      in.GetArrayView<uint8_t>(first_object_bytes).data()) = 0x90ab;
+
+  uint32_t second_object_bytes = in.AllocateArray<uint8_t>(2);
+  *reinterpret_cast<uint16_t*>(
+      in.GetArrayView<uint8_t>(second_object_bytes).data()) = 0x90ab;
+
+  uint32_t object_data_offset = in.AllocateArray<internal::DriverObjectData>(2);
+  in.header().driver_object_data_array = object_data_offset;
+
+  std::vector<IpczDriverHandle> handles = {0x12345678, 0xcdef};
+
+  auto object_data =
+      in.GetArrayView<internal::DriverObjectData>(object_data_offset);
+  // The first driver object data entry references `handles[1]`.
+  object_data[0].driver_data_array = first_object_bytes;
+  object_data[0].first_driver_handle = 1;
+  object_data[0].num_driver_handles = 1;
+
+  // The second driver object data entry references `handles[0]` and
+  // `handles[1]`. This tests that:
+  // - rejection of this entry still frees the resources associated with
+  //   `handles[0]`.
+  // - a second claim on `handles[1]` is rejected.
+  //
+  // This violates MockDriver's expectations of how serialized driver objects
+  // are represented on the wire, but that should not be a problem as the
+  // message should be rejected before trying to call the mock driver's
+  // deserialization method.
+  object_data[1].driver_data_array = second_object_bytes;
+  object_data[1].first_driver_handle = 0;
+  object_data[1].num_driver_handles = 2;
+
+  // This is normally all handled by `DriverTransport::Transmit()`, but this
+  // test manually executes the steps normally taken by `Message::Serialize()`
+  // to build an invalid message.
+  transport().driver_object().driver()->Transmit(
+      transport().driver_object().handle(), in.data_view().data(),
+      in.data_view().size(), handles.data(), handles.size(), IPCZ_NO_FLAGS,
+      nullptr);
+
+  ReceivedMessage serialized = TakeNextReceivedMessage();
+
+  Message out;
+  EXPECT_CALL(driver(), Close(0x12345678, _, _));
+  EXPECT_FALSE(
+      out.DeserializeUnknownType(serialized.AsTransportMessage(), transport()));
+
+  EXPECT_CALL(driver(), Close(0x90abcdef, _, _));
+}
+
 TEST_F(MessageTest, BadEnums) {
   // Out of range enum values should be rejected.
   test::msg::MessageWithEnums m1;
Loading diff…

Original Bug Report

reported by ps...@gmail.com

Use-After-Free in ipcz message deserialization via overlapping DriverObjectData handle ranges

VULNERABILITY DETAILS

A Use-After-Free vulnerability exists in ipcz’s IPC message deserialization layer. DeserializeDriverObject() in third_party/ipcz/src/ipcz/message.cc:134-138 marks transport handles as consumed (is_handle_consumed[i] = true) without checking if the handle was already consumed by a prior DriverObjectData entry.

When a crafted IPC message contains multiple DriverObjectData entries with overlapping handle index ranges (e.g., Entry 0 claims handles[0..1], Entry 1 claims handles[1]), the same IpczDriverHandle is deserialized twice. On non-Windows platforms, this calls TransmissiblePlatformHandle::TakeFromHandle() (mojo/core/ipcz_driver/object.h:90-98) on an already-freed ref-counted object, resulting in a heap-use-after-free in the receiving process.

Reachable from a compromised renderer targeting the browser (broker) process via any ipcz NodeLink transport, including unknown message IDs (DeserializeUnknownType path at message.cc:282-330).

The per-entry bounds check (message.cc:125-132) validates each entry against handles.size() independently but does not validate against other entries for overlap. ValidateParameters() (message.cc:383-498) validates driver object index uniqueness (is_object_claimed) but not driver handle index uniqueness. It also runs after deserialization — the UAF has already occurred.

Vulnerable code (message.cc:134-138):

for (auto i = object_data.first_driver_handle;
     i < object_data.first_driver_handle + object_data.num_driver_handles;
     ++i) {
    is_handle_consumed[i] = true;  // BUG: no check if already true
}

Suggested fix — add one check:

for (auto i = object_data.first_driver_handle;
     i < object_data.first_driver_handle + object_data.num_driver_handles;
     ++i) {
    if (is_handle_consumed[i]) {
        return {};  // Handle already consumed by another DriverObjectData entry
    }
    is_handle_consumed[i] = true;
}

VERSION

  • Chrome Version: Chromium main at commit 1c9c00502d4b (refs/heads/main@{#1583918}), February 2026
  • Operating System: Linux (confirmed non-Windows path). Bug also affects ChromeOS, Android, macOS. Windows has a different manifestation (handle duplication via DecodeHandle rather than pointer UAF).

REPRODUCTION CASE

Attached: ipcz_uaf_poc.cc — Standalone ASAN reproducer.

This reproducer faithfully replicates the vulnerable code pattern from:

  • third_party/ipcz/src/ipcz/message.cc (DeserializeDriverObject, lines 113-143)
  • third_party/ipcz/src/ipcz/message.cc (DeserializeUnknownType, lines 282-330)
  • mojo/core/ipcz_driver/object.h (TakeFromHandle, lines 90-98)

Build and run:

clang++ -fsanitize=address -g -O0 -o ipcz_uaf_poc ipcz_uaf_poc.cc
./ipcz_uaf_poc

What the reproducer does:

  1. Creates 2 ref-counted objects (simulating TransmissiblePlatformHandle)
  2. Constructs 2 DriverObjectData entries with overlapping handle ranges:
    • Entry 0: first_driver_handle=0, num_driver_handles=2 (claims handles[0] and handles[1])
    • Entry 1: first_driver_handle=1, num_driver_handles=1 (claims handles[1] again)
  3. Processes them through the same deserialization logic as DeserializeUnknownType
  4. Entry 0 consumes handles[1] via TakeFromHandle, freeing the object
  5. Entry 1 calls TakeFromHandle on handles[1] again — UAF on freed object

In a real attack: A compromised renderer sends a crafted ipcz message through its NodeLink transport to the browser process. The message has a valid MessageHeader with any message ID (unknown IDs route to DeserializeUnknownType), a DriverObjectData array with overlapping handle ranges, and valid transport handles attached. No authentication or special privileges needed beyond renderer compromise.


CRASH STATE

Type of crash: Browser process crash (ASAN-detected heap-use-after-free in IPC message parsing)

ASAN output:

=================================================================
==1941873==ERROR: AddressSanitizer: heap-use-after-free on address 0x5070000000d0
  at pc 0x561979a2a7d8 bp 0x7fffb933e950 sp 0x7fffb933e948
WRITE of size 4 at 0x5070000000d0 thread T0
    #0 RefCountedObject::AddRef()         ipcz_uaf_poc.cc:54   [atomic refcount increment]
    #1 RefCountedObject::TakeFromHandle() ipcz_uaf_poc.cc:82   [mirrors object.h:90-98]
    #2 DeserializeDriverObject()          ipcz_uaf_poc.cc:143  [mirrors message.cc:134-138]
    #3 DeserializeUnknownType()           ipcz_uaf_poc.cc:183  [mirrors message.cc:282-330]
    #4 main()                             ipcz_uaf_poc.cc:256

0x5070000000d0 is located 64 bytes inside of 68-byte region [0x507000000090,0x5070000000d4)
freed by thread T0 here:
    #0 operator delete(void*, unsigned long)
    #1 RefCountedObject::Release()        ipcz_uaf_poc.cc:63   [refcount hit 0]
    #2 DeserializeDriverObject()          ipcz_uaf_poc.cc:151  [Entry 0 consumed handles[1]]
    #3 DeserializeUnknownType()           ipcz_uaf_poc.cc:183
    #4 main()                             ipcz_uaf_poc.cc:256

previously allocated by thread T0 here:
    #0 operator new(unsigned long)
    #1 main()                             ipcz_uaf_poc.cc:219

SUMMARY: AddressSanitizer: heap-use-after-free ipcz_uaf_poc.cc:54 in RefCountedObject::AddRef()

Exploitation impact:

  • The freed TransmissiblePlatformHandle is a small, fixed-size heap allocation in the browser process
  • Attacker controls timing (sends message at will) and can heap-spray between free and reuse
  • AddRef() on attacker-controlled data provides a write primitive (refcount increment at controlled offset)
  • Chainable into arbitrary code execution in the browser process (sandbox escape)

CREDIT INFORMATION

Reporter credit: [Paul Seekamp / nullenc0de]

View on issue tracker