High watch CVSS 9.6 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect handle provided in unspecified circumstances in Mojo
DescriptionIncorrect handle provided in unspecified circumstances in Mojo
ComponentMojo
Sub-componentInternals>Mojo>Core
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker412578726
Fix commit295a4a1b14b8 (chromium/src) +260/-34
CWECWE-732
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedMicky
Fix landed2025-05-06 (public git)
Disclosed2025-05-14
Patch gap8 days — fix public in source before the advisory

Background

Mojo ipcz driver `Transport`
the object that wraps a PlatformChannelEndpoint used to move messages and platform handles between two processes over an ipcz node connection.
Broker vs. non-broker node
a kBroker endpoint (e.g. the browser) has the privilege to broker handles for other nodes, while a kNonBroker endpoint (e.g. a renderer) does not.
`ProcessTrust`
the new per-transport trust attribute (kTrusted / kUntrusted on Windows, kUntracked elsewhere) that replaces the old bool is_remote_process_untrusted and governs how handles may be encoded to a peer.
Transitive trust
the flawed property whereby a transport deserialized out of another transport inherited or defaulted to trusted status regardless of the source transport’s trust.

Root Cause Analysis

When a Transport object is serialized and sent inside a message, the receiving side reconstructs it in the transport-deserialization path of mojo/core/ipcz_driver/transport.cc by reading a TransportHeader from from_transport and calling Transport::Create. Before the patch this path did not validate that header.destination_type held an in-range enum value, and it never rejected a deserialized transport whose destination_type == kBroker even when the source transport was untrusted, so an untrusted node could reflect a broker-initiated transport back toward a broker. It also failed to propagate the source’s process trust: the newly created transport was constructed with a default (effectively kTrusted) trust rather than from_transport.remote_process_trust(), so a link that originated from an untrusted process was silently treated as trusted for later handle encoding. Because handle encoding (EncodeHandle) keys its Windows handle-duplication constraints on this trust value, a reflected/untrusted-origin transport that was later used to deserialize another object containing handles would cause handles to be improperly provided in the broker — the “incorrect handle provided” behavior the advisory describes.

The fix adds an in-range check on destination_type (returning nullptr), adds a check rejecting kBroker destinations from untrusted sources (returning nullptr), and threads from_transport.remote_process_trust() into Transport::Create so trust is inherited rather than upgraded. Making the trust a distinct ProcessTrust enum and turning the peer-elevation DCHECK into a CHECK further ensures peers are only elevated when expected.

Key insight
The single core mistake was allowing transitive trust across transport deserialization: a transport carried inside a message from an untrusted peer was created without inheriting the source’s ProcessTrust and without forbidding broker-destined reflections, so the fix propagates remote_process_trust and rejects both out-of-range and untrusted-broker destination_type values before the new Transport is built.

Attack Path

  1. Untrusted node holds a broker transport A compromised or untrusted non-broker process (e.g. a renderer) obtains a transport that was originally initiated by the broker.
  2. Reflect the transport back The untrusted node serializes and sends that broker transport back toward a broker; in the pre-patch deserialization path the missing header.destination_type == kBroker && !is_source_trusted check let the broker-destined transport be accepted.
  3. Trust is silently upgraded Transport::Create was called without from_transport.remote_process_trust(), so the reconstructed transport defaulted to trusted even though it came from an untrusted source.
  4. Leak handles via the reflected transport The reflected transport is later used to deserialize another transport containing handles; because it is treated as trusted, EncodeHandle provides handles into the broker that should not have been transferred, producing the incorrect-handle/handle-leak condition. (inferred exact downstream handle target)

Impact Assessment

An attacker in an untrusted process could cause handles to be incorrectly provided in a broker (higher-privilege) process by exploiting transitive trust during transport deserialization, matching the advisory’s “Incorrect handle provided in unspecified circumstances in Mojo.” The preconditions are the ability to send a serialized transport to a broker and, on Windows, the process-trust distinction the patch formalizes. Any escalation from a leaked handle to broader compromise such as a sandbox escape is (inferred) and not demonstrated by the patch; the advisory states neither in-the-wild exploitation nor a specific severity here.

Changed Functions

FunctionChangeNotes
if
mojo/core/ipcz_driver/invitation.cc
modified
if
mojo/core/ipcz_driver/transport.cc
modified
for
mojo/core/ipcz_driver/transport.cc
modified

Files Changed

  • mojo/core/ipcz_driver/invitation.cc
  • mojo/core/ipcz_driver/transport.cc

Audit Directions

  • Trust propagation across deserialization
    Audit every place an object reconstructed from an IPC message inherits or defaults a security attribute (trust, origin, privilege) and confirm the attribute is copied from the verified source rather than reset to a permissive default.
  • Reflection of privileged endpoints
    Look for paths where a privileged/broker-initiated capability can be sent back to the privileged side, and verify the receiver rejects capabilities that claim privileged roles when the sender is untrusted.
  • Enum validation on the wire
    Check deserialization of enum fields read from untrusted messages (like destination_type) for explicit in-range validation before the value is used to branch or index.
From 295a4a1b14b8fe12929bb61e6e00a74ac43098e8 Mon Sep 17 00:00:00 2001
From: Alex Gough <ajgo@chromium.org>
Date: Mon, 05 May 2025 19:09:15 -0700
Subject: [PATCH] Drop transitive trust from transports

Untrusted nodes could reflect a broker initiated transport back to
a broker. This ultimately allows for handle leaks if the reflected
transport was later used to deserialize another transport containing
handles in the broker.

This CL addresses this along several axes:

1. untrusted transports cannot return new links to brokers.
2. process trustiness on Windows is propagated when a transport is
deserialized from a transport.

Windows has a special additional level of trustiness associated with
mojo peers via the is_remote_process_untrusted attribute (the
MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS in invitations). This
affects how handles are sent between processes. This was a bool on all
platforms which was confusing.

This CL makes this attribute clearer. On Windows it is now a bi-state
enum, while on other platforms it is simply kUntracked. This makes it
easier to use default constructed values, and the same API on all
platforms without using too many buildflag differences.

This state was not being propagated correctly during transport
deserialization, and is now set as the same trust as the process from
which a deserialized transport came. Processes currently default to
being kTrusted, which matches the current behavior of the bool flag.

Finally, this CL turns a DCHECK into a CHECK to ensure peers are only
elevated when expected.

Bug: 412578726
Change-Id: I6741a3f53b26c3df854731177cdc886e9c8f7f11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6497400
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Alex Gough <ajgo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1456055}
---

diff --git a/mojo/core/ipcz_driver/invitation.cc b/mojo/core/ipcz_driver/invitation.cc
index 4b3f717d..001b146 100644
--- a/mojo/core/ipcz_driver/invitation.cc
+++ b/mojo/core/ipcz_driver/invitation.cc
@@ -90,7 +90,7 @@
     base::Process remote_process = base::Process(),
     MojoProcessErrorHandler error_handler = nullptr,
     uintptr_t error_handler_context = 0,
-    bool is_remote_process_untrusted = false) {
+    Transport::ProcessTrust remote_process_trust = Transport::ProcessTrust{}) {
   CHECK_EQ(endpoint.num_platform_handles, 1u);
   auto handle =
       PlatformHandle::FromMojoPlatformHandle(&endpoint.platform_handles[0]);
@@ -100,7 +100,7 @@
 
   auto transport = base::MakeRefCounted<Transport>(
       endpoint_types, PlatformChannelEndpoint(std::move(handle)),
-      std::move(remote_process), is_remote_process_untrusted);
+      std::move(remote_process), remote_process_trust);
   transport->SetErrorHandler(error_handler, error_handler_context);
   transport->set_leak_channel_on_shutdown(options.leak_channel_on_shutdown);
   transport->set_is_peer_trusted(options.is_peer_trusted);
@@ -268,15 +268,21 @@
   // bit essentially means that the remote process is especially untrustworthy
   // (e.g. a Chrome renderer) and should be subject to additional constraints
   // regarding what types of objects can be transferred to it.
-  const bool is_remote_process_untrusted =
-      options &&
-      (options->flags & MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS) != 0;
+  Transport::ProcessTrust remote_process_trust{};
+#if BUILDFLAG(IS_WIN)
+  if (options &&
+      (options->flags & MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS) != 0) {
+    remote_process_trust = Transport::ProcessTrust::kUntrusted;
+  } else {
+    remote_process_trust = Transport::ProcessTrust::kTrusted;
+  }
+#endif
 
   const bool is_peer_elevated =
       options && (options->flags & MOJO_SEND_INVITATION_FLAG_ELEVATED);
 #if !BUILDFLAG(IS_WIN)
   // For now, the concept of an elevated process is only meaningful on Windows.
-  DCHECK(!is_peer_elevated);
+  CHECK(!is_peer_elevated);
 #endif
 
 #if BUILDFLAG(IS_WIN)
@@ -296,7 +302,7 @@
       *transport_endpoint,
       {.is_peer_trusted = is_peer_elevated, .is_trusted_by_peer = true},
       std::move(remote_process), error_handler, error_handler_context,
-      is_remote_process_untrusted);
+      remote_process_trust);
   if (transport == IPCZ_INVALID_DRIVER_HANDLE) {
     return MOJO_RESULT_INVALID_ARGUMENT;
   }
diff --git a/mojo/core/ipcz_driver/transport.cc b/mojo/core/ipcz_driver/transport.cc
index c1516bae..bca98fd 100644
--- a/mojo/core/ipcz_driver/transport.cc
+++ b/mojo/core/ipcz_driver/transport.cc
@@ -136,7 +136,7 @@
                   const base::Process& remote_process,
                   HandleOwner handle_owner,
                   HandleData& out_handle_data,
-                  bool is_remote_process_untrusted) {
+                  Transport::ProcessTrust remote_process_trust) {
   CHECK(handle.is_valid());
   // Duplicating INVALID_HANDLE_VALUE passes a process handle. If you intend to
   // do this, you must open a valid process handle, not pass the result of
@@ -158,7 +158,7 @@
   DCHECK_EQ(handle_owner, HandleOwner::kRecipient);
   DCHECK(remote_process.IsValid());
 #if BUILDFLAG(IS_WIN)
-  if (is_remote_process_untrusted) {
+  if (remote_process_trust == Transport::ProcessTrust::kUntrusted) {
     DcheckIfFileHandleIsUnsafe(handle.GetHandle().get());
   }
 #endif
@@ -242,23 +242,20 @@
 Transport::Transport(EndpointTypes endpoint_types,
                      PlatformChannelEndpoint endpoint,
                      base::Process remote_process,
-                     bool is_remote_process_untrusted)
+                     ProcessTrust remote_process_trust)
     : endpoint_types_(endpoint_types),
       remote_process_(std::move(remote_process)),
-#if BUILDFLAG(IS_WIN)
-      is_remote_process_untrusted_(is_remote_process_untrusted),
-#endif
-      inactive_endpoint_(std::move(endpoint)) {
-}
+      remote_process_trust_(remote_process_trust),
+      inactive_endpoint_(std::move(endpoint)) {}
 
 // static
 scoped_refptr<Transport> Transport::Create(EndpointTypes endpoint_types,
                                            PlatformChannelEndpoint endpoint,
                                            base::Process remote_process,
-                                           bool is_remote_process_untrusted) {
+                                           ProcessTrust remote_process_trust) {
   return base::MakeRefCounted<Transport>(endpoint_types, std::move(endpoint),
                                          std::move(remote_process),
-                                         is_remote_process_untrusted);
+                                         remote_process_trust);
 }
 
 // static
@@ -495,7 +492,7 @@
   for (size_t i = 0; i < object_num_handles; ++i) {
 #if BUILDFLAG(IS_WIN)
     ok &= EncodeHandle(platform_handles[i], remote_process_, handle_owner,
-                       handle_data[i], is_remote_process_untrusted_);
+                       handle_data[i], remote_process_trust());
 #else
     handles[i] = TransmissiblePlatformHandle::ReleaseAsHandle(
         base::MakeRefCounted<TransmissiblePlatformHandle>(
@@ -654,23 +651,39 @@
     process = base::Process(handles[1].ReleaseHandle());
   }
 #endif
+  // Reject transports with out of range enum value in destination_type.
+  if (!(header.destination_type == kBroker ||
+        header.destination_type == kNonBroker)) {
+    return nullptr;
+  }
+
   const bool is_source_trusted = from_transport.is_peer_trusted() ||
                                  from_transport.destination_type() == kBroker;
+
   const bool is_new_peer_trusted = header.is_peer_trusted;
+  const bool is_trusted_by_peer = header.is_trusted_by_peer;
+
   if (is_new_peer_trusted && !is_source_trusted) {
     // Untrusted transports cannot send us trusted transports.
     return nullptr;
   }
+
+  if (header.destination_type == kBroker && !is_source_trusted) {
+    // Do not accept broker connections from untrusted transports.
+    return nullptr;
+  }
+
   if (header.is_same_remote_process &&
       from_transport.remote_process().IsValid()) {
     process = from_transport.remote_process().Duplicate();
   }
-  auto transport = Create({.source = from_transport.source_type(),
-                           .destination = header.destination_type},
-                          PlatformChannelEndpoint(std::move(handles[0])),
-                          std::move(process));
+  auto transport =
+      Create({.source = from_transport.source_type(),
+              .destination = header.destination_type},
+             PlatformChannelEndpoint(std::move(handles[0])), std::move(process),
+             from_transport.remote_process_trust());
   transport->set_is_peer_trusted(is_new_peer_trusted);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/mojo/core/ipcz_driver/transport_test.cc b/mojo/core/ipcz_driver/transport_test.cc
index b9a1e994..f37157e 100644
--- a/mojo/core/ipcz_driver/transport_test.cc
+++ b/mojo/core/ipcz_driver/transport_test.cc
@@ -72,7 +72,13 @@
   static scoped_refptr<Transport> CreateAndSendTransport(
       MojoHandle pipe,
       const base::Process& process,
-      bool untrusted = false) {
+#if BUILDFLAG(IS_WIN)
+      Transport::ProcessTrust process_trust = Transport::ProcessTrust::kTrusted
+#else
+      // Parameter is not tracked on non-Windows platforms.
+      Transport::ProcessTrust process_trust = Transport::ProcessTrust{}
+#endif
+  ) {
     PlatformChannel channel;
     MojoHandle transport_for_client =
         WrapPlatformHandle(channel.TakeRemoteEndpoint().TakePlatformHandle())
@@ -81,7 +87,7 @@
     WriteMessageWithHandles(pipe, "", &transport_for_client, 1);
     return Transport::Create(
         {.source = Transport::kBroker, .destination = Transport::kNonBroker},
-        channel.TakeLocalEndpoint(), process.Duplicate(), untrusted);
+        channel.TakeLocalEndpoint(), process.Duplicate(), process_trust);
   }
 
   // Retrieves a PlatformChannel endpoint from `pipe` and returns a newly
@@ -263,6 +269,93 @@
   });
 }
 
+DEFINE_TEST_CLIENT_TEST_WITH_PIPE(MalformedTransportClient,
+                                  MojoIpczTransportTest,
+                                  h) {
+  // Offsets of enums that should be validated on receipt. Serialized objects
+  // use types internal to transport.cc e.g. [ObjectHeader][TransportHeader]...
+  // so supply direct offsets here.
+
+  // offsetof(ObjectHeader, type).
+  constexpr size_t object_type_offset = 4;
+  // offsetof(TransportHeader, destination_type) + sizeof(ObjectHeader)
+#if BUILDFLAG(IS_WIN)
+  constexpr size_t transport_destination_type_offset = 0x18;
+#else
+  constexpr size_t transport_destination_type_offset = 0x08;
+#endif
+
+  scoped_refptr<Transport> transport = ReceiveTransport(h);
+
+  TransportListener listener(*transport);
+  EXPECT_EQ("ready", listener.WaitForNextMessage().as_string());
+
+  {
+    auto [our_new_transport, their_new_transport] =
+        Transport::CreatePair(Transport::kNonBroker, Transport::kNonBroker);
+
+    TestMessage msg =
+        SerializeObjectFor(*transport, std::move(their_new_transport));
+    // Peek into the message to break the encoded object type by using an out
+    // of range enum value. This is uint32_t sized.
+    msg.bytes[object_type_offset] = 22;
+    msg.Transmit(*transport);
+
+    EXPECT_EQ("got null", listener.WaitForNextMessage().as_string());
+  }
+
+  {
+    auto [our_new_transport, their_new_transport] =
+        Transport::CreatePair(Transport::kNonBroker, Transport::kNonBroker);
+
+    TestMessage msg =
+        SerializeObjectFor(*transport, std::move(their_new_transport));
+    // Peek into the message to break the encoded transport type by using an out
+    // of range enum value. This is uint8_t sized.
+    msg.bytes[transport_destination_type_offset] = 22;
+    msg.Transmit(*transport);
+
+    EXPECT_EQ("got null", listener.WaitForNextMessage().as_string());
+  }
+
+  TestMessage("done").Transmit(*transport);
+  EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
+}
+
+TEST_F(MojoIpczTransportTest, MalformedTransport) {
+  RunTestClientWithController(
+      "MalformedTransportClient", [&](ClientController& c) {
+        scoped_refptr<Transport> transport =
+            CreateAndSendTransport(c.pipe(), c.process());
+
+        TransportListener listener(*transport);
+        TestMessage("ready").Transmit(*transport);
+
+        {
+          // Object type is invalid so the object should be rejected.
+          TestMessage message = listener.WaitForNextMessage();
+          scoped_refptr<ObjectBase> object;
+          const IpczResult result = transport->DeserializeObject(
+              base::span(message.bytes), base::span(message.handles), object);
+          EXPECT_EQ(result, IPCZ_RESULT_UNIMPLEMENTED);
+          TestMessage("got null").Transmit(*transport);
+        }
+
+        {
+          // Transport type is invalid so the object should be rejected.
+          TestMessage message = listener.WaitForNextMessage();
+          scoped_refptr<ObjectBase> object;
+          const IpczResult result = transport->DeserializeObject(
+              base::span(message.bytes), base::span(message.handles), object);
+          EXPECT_EQ(result, IPCZ_RESULT_INVALID_ARGUMENT);
+          TestMessage("got null").Transmit(*transport);
+        }
+
+        EXPECT_EQ("done", listener.WaitForNextMessage().as_string());
+        listener.WaitForDisconnect();
+      });
+}
+
 // Transport on Windows does not support out-of-band handle transfer, so this
 // test is impossible there. Windows handle transmission is instead covered by
 // tests which more broadly cover driver object serialization.
@@ -415,13 +508,22 @@
     return false;
 #endif
   }
+  Transport::ProcessTrust TransportProcessTrust() {
+// Enforcement only happens on Windows.
+#if BUILDFLAG(IS_WIN)
+    return IsEnforcementEnabled() ? Transport::ProcessTrust::kUntrusted
+                                  : Transport::ProcessTrust::kTrusted;
+#else
+    return Transport::ProcessTrust::kUntracked;
+#endif
+  }
   bool ShouldMarkNoExecute() { return std::get<1>(GetParam()); }
 };
 
 TEST_P(MojoIpczTransportSecurityTest, TransmitFile) {
   RunTestClientWithController("TransmitFileClient", [&](ClientController& c) {
     scoped_refptr<Transport> transport =
-        CreateAndSendTransport(c.pipe(), c.process(), IsEnforcementEnabled());
+        CreateAndSendTransport(c.pipe(), c.process(), TransportProcessTrust());
     base::ScopedTempDir temp_dir;
     CHECK(temp_dir.CreateUniqueTempDir());
     int32_t flags = base::File::FLAG_CREATE | base::File::FLAG_READ |
@@ -644,8 +746,8 @@
 TEST_F(MojoIpczTransportTest, InvalidHandleUntrusted) {
   RunTestClientWithController(
       "InvalidHandleUntrustedClient", [&](ClientController& c) {
-        scoped_refptr<Transport> transport =
-            CreateAndSendTransport(c.pipe(), c.process(), /*untrusted=*/true);
+        scoped_refptr<Transport> transport = CreateAndSendTransport(
+            c.pipe(), c.process(), Transport::ProcessTrust{});
 
         TransportListener listener(*transport);
         TestMessage(kFromTrusted).Transmit(*transport);
@@ -666,5 +768,98 @@
 
 #endif  // BUILDFLAG(IS_WIN)
 
+DEFINE_TEST_CLIENT_TEST_WITH_PIPE(TransportFromUntrustedClient,
+                                  MojoIpczTransportTest,
+                                  h) {
+  scoped_refptr<Transport> transport = ReceiveTransport(h);
+  TransportListener listener(*transport);
+  EXPECT_EQ("ready", listener.WaitForNextMessage().as_string());
+
+  for (int i = 0; i < 2; i++) {
+    auto ours = i == 0 ? Transport::kNonBroker : Transport::kBroker;
+    auto theirs = i == 0 ? Transport::kBroker : Transport::kNonBroker;
+    {
+      auto [our_new_transport, their_new_transport] =
+          Transport::CreatePair(ours, theirs);
+
+      their_new_transport->set_is_peer_trusted(true);
+
+      SerializeObjectFor(*transport, std::move(their_new_transport))
+          .Transmit(*transport);
+      EXPECT_EQ("got null", listener.WaitForNextMessage().as_string());
+    }
+
+    {
+      auto [our_new_transport, their_new_transport] =
+          Transport::CreatePair(ours, theirs);
+
+      their_new_transport->set_is_trusted_by_peer(true);
+
+      SerializeObjectFor(*transport, std::move(their_new_transport))
+          .Transmit(*transport);
+      if (ours == Transport::kNonBroker) {
+        EXPECT_EQ("got untrusted", listener.WaitForNextMessage().as_string());
+      } else {
+        EXPECT_EQ("got null", listener.WaitForNextMessage().as_string());
+      }
+    }
+  }
+
+  EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
+}
+
+TEST_F(MojoIpczTransportTest, TransportFromUntrusted) {
+#if BUILDFLAG(IS_WIN)
+  // TODO(crbug.com/414392683) default to untrusted/untracked.
+  Transport::ProcessTrust process_trust = Transport::ProcessTrust::kUntrusted;
+#else
+  Transport::ProcessTrust process_trust{};
+#endif
+  RunTestClientWithController(
+      "TransportFromUntrustedClient", [&](ClientController& c) {
+        scoped_refptr<Transport> transport =
+            CreateAndSendTransport(c.pipe(), c.process(), process_trust);
+
+        TransportListener listener(*transport);
+        TestMessage("ready").Transmit(*transport);
+
+        // A broker (this process) should reject transports from untrusted
+        // clients if they claim the transport's peer is trusted or is a broker.
+        // It is ok to allow transports from a client that indicates they trust
+        // the peer, as a broker will not make trust decisions based on that.
+        for (int i = 0; i < 2; i++) {
+          auto theirs = i == 0 ? Transport::kNonBroker : Transport::kBroker;
+          {
+            TestMessage message = listener.WaitForNextMessage();
+            scoped_refptr<ObjectBase> object;
+            const IpczResult result = transport->DeserializeObject(
+                base::span(message.bytes), base::span(message.handles), object);
+            EXPECT_EQ(result, IPCZ_RESULT_INVALID_ARGUMENT);
+            TestMessage("got null").Transmit(*transport);
+          }
+
+          {
+            TestMessage message = listener.WaitForNextMessage();
+            if (theirs == Transport::kNonBroker) {
+              scoped_refptr<Transport> transport2 =
+                  DeserializeObjectFrom<Transport>(*transport, message);
+              EXPECT_TRUE(transport2->is_trusted_by_peer());
+              EXPECT_FALSE(transport2->is_peer_trusted());
+              TestMessage("got untrusted").Transmit(*transport);
+            } else {
+              scoped_refptr<ObjectBase> object;
+              const IpczResult result = transport->DeserializeObject(
+                  base::span(message.bytes), base::span(message.handles),
+                  object);
+              EXPECT_EQ(result, IPCZ_RESULT_INVALID_ARGUMENT);
+              TestMessage("got null").Transmit(*transport);
+            }
+          }
+        }
+
+        listener.WaitForDisconnect();
+      });
+}
+
 }  // namespace
 }  // namespace mojo::core::ipcz_driver
Loading diff…

Original Bug Report

reported by ha...@gmail.com

ipcz bug can allow renderer duplicate browser process handle to escape sandbox

Steps to reproduce the problem

  1. git apply patch.diff and compile chromium.(This patch is all renderer side patch.)
  2. open chromium.
  3. If you are build chromium without component build and without official build. You will hit the check"You are attempting to duplicate a privileged handle into a sandboxed" process.\n Please contact security@chromium.org for assistance."; (Note this check will not work in official buid.)
  4. If you are with component build or official build. You can use “System Informer” to see the renderer process’s handle. You will see browser process’s handle is in one renderer process. You can see the result in handle.txt. Renderer process(58636) has full control of browser process(105724)’s thread handle.

Problem Description

In Transport::Deserialize[1]. It directly create transport using header.destination_type without any check. If a malicious renderer pass kbroker as the header.destination_type and send the request to the browser process. Then renderer can use this malicious transport to duplicate the privileged handle of browser process. Because in [2], browser will think the renderer is a broker process. And allow it to duplicate browser process’s handle.

https://source.chromium.org/chromium/chromium/src/+/main:mojo/core/ipcz_driver/transport.cc;l=642;drc=b6620a02fa498df5297e53241b54a31f488ca440;bpv=1;bpt=1 [1]

https://source.chromium.org/chromium/chromium/src/+/main:mojo/core/ipcz_driver/transport.cc;l=200;drc=b6620a02fa498df5297e53241b54a31f488ca440;bpv=0;bpt=1 [2]

Renderer process then can use the privileged handle such as thread handle to escape the sandbox.

How to exploit this bug:

  1. Renderer process send RequestIntroduction to broker with self’s node name. And then will get the transport1, transport2. ( Because in windows, Renderer process has no permission to create namepipe)
  2. Renderer send ReferNonBroker request with transport1 and pass kbroker as header.destination_type.
  3. Renderer send connect request.
  4. Renderer send RelayMessage request with transport2 to request the handle of browser process. Because we don’t know the value of thread handle. We just send RelayMessage multiple times with handle value 4 to 1000. And browser process will return all the handle which value is between 4 and 1000.
  5. Renderer process use the privileged browser process handle to escape the sandbox.( This step is still in progressing. I will attach exploit soon.)

Additional Comments

this vulnerability is similar to High CVE-2025-2783: Incorrect handle provided in unspecified circumstances in Mojo on Windows. Reported by Boris Larin (@oct0xor) and Igor Kuznetsov (@2igosha) of Kaspersky on 2025-03-20. But this vulnerability has a higher complexity. I will attach exploit soon. Bisect information: this bug was introduced in https://chromium-review.googlesource.com/c/chromium/src/+/3963307.

Summary

ipcz bug can allow renderer duplicate browser process handle to escape sandbox

Custom Questions

Reporter credit:

Micky

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker