CVE-2026-8523
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/ipcz/src/ipcz/message.cc |
modified | |
ifthird_party/ipcz/src/ipcz/message_test.cc |
modified | |
TEST_Fthird_party/ipcz/src/ipcz/message_test.cc |
modified |
Files Changed
third_party/ipcz/src/ipcz/message.ccthird_party/ipcz/src/ipcz/message_test.cc
Patch
From 3d1b1ed55f6106412a19ed64d3524172b8358351 Mon Sep 17 00:00:00 2001 From: Daniel Cheng <[email protected]> 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 <[email protected]> > Reviewed-by: Andrea Orru <[email protected]> > 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 <[email protected]> Commit-Queue: Daniel Cheng <[email protected]> Reviewed-by: Andrea Orru <[email protected]> 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;
Regression Test / PoC
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;
Original Bug Report
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
mainat commit1c9c00502d4b(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
DecodeHandlerather 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:
- Creates 2 ref-counted objects (simulating
TransmissiblePlatformHandle) - Constructs 2
DriverObjectDataentries 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)
- Entry 0:
- Processes them through the same deserialization logic as
DeserializeUnknownType - Entry 0 consumes handles[1] via
TakeFromHandle, freeing the object - Entry 1 calls
TakeFromHandleon 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
TransmissiblePlatformHandleis 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]