Overview

High
Severity
7.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Core
DescriptionIncorrect authorization in Core
ComponentCore
Sub-componentMojo
Bug ClassAuthorization 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.
Tracker516780835
Fix commit7858f67868ff (chromium/src) +203/-5
CWECWE-863
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-09-03 (public git)
Disclosed2026-09-15
Patch gap12 days — fix public in source before the advisory

Background

Broker vs. non-broker node
In Mojo’s ipcz transport, a kBroker node (e.g. the browser process) is privileged to allocate shared state and vouch for other nodes, while a kNonBroker node (e.g. a sandboxed renderer) depends on its broker.
Isolated / broker-to-broker connection
Two brokers can be joined directly to one another via ConnectNode() (rather than one introducing the other), producing a NodeLink whose local and remote node types are both kBroker.
Transport trust flags
A Transport carries a source_type()/destination_type() (kBroker or kNonBroker) and is_peer_trusted_/is_trusted_by_peer_, which together decide whether the peer may be treated as a source of privileged objects such as HandleOwner::kRecipient handles or broker-destined transports.
`Transport::Deserialize`
The mojo ipcz driver routine that reconstructs an embedded object (e.g. another Transport) received over a transport, deciding whether to honor a privileged destination/trust claim based on whether the sending peer is considered trusted.

Root Cause Analysis

The mojo ipcz driver decided whether the peer that sent a serialized object was a trusted source of privileged objects with the expression from_transport.is_peer_trusted() || from_transport.destination_type() == kBroker. That second disjunct was correct for a non-broker (whose transport is destined for its own broker, which it legitimately trusts) but was applied unconditionally: on a broker-to-broker link both endpoints have destination_type() == kBroker, so a broker implicitly treated a peer broker as trusted with no explicit grant. Because the two brokers of an isolated connection may belong to different, mutually-untrusting security domains, this violated the invariant that trust must be explicit between peers of equal privilege.

The fix replaces the check with is_source_trusted(), which only infers trust from a broker destination when the source is itself a non-broker (destination_type() == kBroker && source_type() != kBroker), otherwise requiring an explicit is_peer_trusted_ grant. Companion hardening in NodeLink closes related broker-to-broker paths: OnAcceptIntroduction now rejects any attempt to introduce one broker to another, and OnRelayMessage now rejects relay requests whose remote peer is not a kNormal (non-broker) node.

Key insight
The single mistake was treating “the destination is a broker” as sufficient proof that the sender is trusted, which silently granted mutual implicit trust between two brokers on an isolated link; the fix conditions that inference on the source being a non-broker, so co-equal brokers must mark each other trusted explicitly before conveying privileged objects.

Attack Path

  1. Establish an isolated broker-to-broker link An attacker-controlled node participating as a kBroker connects directly to a victim broker, yielding a NodeLink/Transport where both source_type() and destination_type() are kBroker and is_peer_trusted_ was never set.
  2. Exploit the implicit-trust check On the victim, Transport::Deserialize computed is_source_trusted as true purely because destination_type() == kBroker, so the untrusted peer broker was accepted as a source of privileged objects.
  3. Convey a privileged object As the regression test TransportFromUntrustedBroker demonstrates, the attacker serializes and transmits a Transport claiming a kBroker destination (or is_peer_trusted == true); pre-patch these were honored instead of rejected with IPCZ_RESULT_INVALID_ARGUMENT.
  4. Cross the trust boundary The victim broker instantiates the privileged transport/handle as trusted, giving the attacker a broker-destined or trusted-peer channel it was never authorized to hold. Related variants reach comparable state via a broker→broker introduction (OnAcceptIntroduction) or a relay request from a broker (OnRelayMessage).
  5. Escalation Possessing a privileged, trusted IPC object inside the victim broker could be leveraged to further compromise that process’s IPC surface (inferred) — the patch itself only demonstrates the incorrect authorization, not a specific memory-corruption or code-execution primitive.

Impact Assessment

An attacker able to act as one broker on an isolated broker-to-broker connection could get a peer broker to accept privileged IPC objects (broker-destined or trusted-peer transports, or recipient-owned handles) that should have required explicit trust, an incorrect-authorization/logic error in Core. The precondition is a broker-to-broker link where the attacker’s side is not explicitly trusted; the immediate gain is bypassing the trust boundary between two co-equal broker nodes, with any further sandbox-escape or privilege-escalation consequence being (inferred). The advisory classifies this as a LogicError / “Incorrect authorization in Core” and reports no in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
if
mojo/core/ipcz_driver/driver_for_ipcz_tests.cc
modified
TEST_F
mojo/core/ipcz_driver/transport_test.cc
modified
for
mojo/core/ipcz_driver/transport_test.cc
modified
TEST_F
third_party/ipcz/src/ipcz/node_link_test.cc
modified

Files Changed

  • third_party/ipcz/src/ipcz/node_link_test.cc
  • mojo/core/ipcz_driver/transport_test.cc
  • mojo/core/ipcz_driver/transport.cc
  • third_party/ipcz/src/ipcz/node_link.cc
  • mojo/core/ipcz_driver/transport.h
  • mojo/core/ipcz_driver/driver_for_ipcz_tests.cc

Audit Directions

  • Implicit trust from endpoint role
    Audit any check that infers a peer’s trustworthiness from a structural attribute (destination_type() == kBroker, “peer is a broker”, process type) rather than an explicit per-connection grant, especially where both peers can share the same role.
  • Symmetric-privilege peer links
    Review ipcz/mojo paths that assume one side of a link is subordinate (introductions, relays, handle ownership) and verify they behave safely on isolated broker-to-broker connections where the peer may be an untrusting equal.
  • Serialized-object provenance
    In Deserialize/handle-adoption code, confirm that any object claiming a privileged destination, trusted-peer status, or recipient ownership is validated against an explicit trust flag, not merely accepted because the receiving endpoint is privileged.
From 7858f67868ff8416db134d4f9e1a38731df6504b Mon Sep 17 00:00:00 2001
From: Will Harris <[email protected]>
Date: Thu, 03 Sep 2026 08:19:36 -0700
Subject: [PATCH] [Mojo] Limit peer trust for isolated connections

NodeLink::OnAcceptIntroduction now rejects introductions that would
introduce a broker to another broker; broker-to-broker links are only
ever established directly via ConnectNode, and HandleIntroductionRequest
already declines to introduce two brokers, so this case is never reached
by a well-behaved sender.

NodeLink::OnRelayMessage now also requires the sender to be a
non-broker, matching the only legitimate caller (NodeLink::Transmit via
GetBrokerLink()).

In the mojo ipcz driver, Transport::Deserialize no longer treats the
source as trusted purely because the destination is a broker when the
source is itself a broker: a non-broker still implicitly trusts its
broker, but two brokers connected to one another must explicitly mark
the peer as trusted before either will accept an embedded transport that
claims a broker destination or a trusted peer. The mojo-ipcz test driver
is updated so cooperating broker test nodes mark each other as trusted,
and a new TransportFromUntrustedBroker test covers the untrusted-broker
case directly.

Bug: 516780835
Change-Id: I54ca58feb080a4478acd68e12bce3af9f29ba561
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8340888
Reviewed-by: Alex Gough <[email protected]>
Commit-Queue: Will Harris <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1691601}
---
diff --git a/mojo/core/ipcz_driver/driver_for_ipcz_tests.cc b/mojo/core/ipcz_driver/driver_for_ipcz_tests.cc
index 212199e..3c3a39d 100644
--- a/mojo/core/ipcz_driver/driver_for_ipcz_tests.cc
+++ b/mojo/core/ipcz_driver/driver_for_ipcz_tests.cc
@@ -180,6 +180,12 @@
     transports = Transport::CreatePair(
         Transport::kBroker,
         for_broker_target ? Transport::kBroker : Transport::kNonBroker);
+    if (for_broker_target) {
+      // Cooperating broker test nodes explicitly trust each other so that
+      // each end may convey broker-destined transports to the other.
+      transports.first->set_is_peer_trusted(true);
+      transports.second->set_is_peer_trusted(true);
+    }
     return {
         .ours = Transport::ReleaseAsHandle(std::move(transports.first)),
         .theirs = Transport::ReleaseAsHandle(std::move(transports.second)),
@@ -225,10 +231,14 @@
     }
 #endif  // BUILDFLAG(IS_WIN)
     const bool is_broker = parent_process.IsValid();
-    return Transport::ReleaseAsHandle(Transport::Create(
+    scoped_refptr<Transport> transport = Transport::Create(
         {.source = is_broker ? Transport::kBroker : Transport::kNonBroker,
          .destination = Transport::kBroker},
-        std::move(endpoint), std::move(parent_process)));
+        std::move(endpoint), std::move(parent_process));
+    if (is_broker) {
+      transport->set_is_peer_trusted(true);
+    }
+    return Transport::ReleaseAsHandle(std::move(transport));
   }
 
  private:
diff --git a/mojo/core/ipcz_driver/transport.cc b/mojo/core/ipcz_driver/transport.cc
index 8d3ea1e..831a773 100644
--- a/mojo/core/ipcz_driver/transport.cc
+++ b/mojo/core/ipcz_driver/transport.cc
@@ -682,8 +682,10 @@
     return nullptr;
   }
 
-  const bool is_source_trusted = from_transport.is_peer_trusted() ||
-                                 from_transport.destination_type() == kBroker;
+  // A non-broker implicitly trusts its broker; otherwise the peer must be
+  // explicitly trusted. When both endpoints are brokers, the peer being a
+  // broker does not by itself confer any trust on this end.
+  const bool is_source_trusted = from_transport.is_source_trusted();
 
   const bool is_new_peer_trusted = header.is_peer_trusted;
   const bool is_trusted_by_peer = header.is_trusted_by_peer;
diff --git a/mojo/core/ipcz_driver/transport.h b/mojo/core/ipcz_driver/transport.h
index e90011f..54b87dd9 100644
--- a/mojo/core/ipcz_driver/transport.h
+++ b/mojo/core/ipcz_driver/transport.h
@@ -99,6 +99,17 @@
   void set_is_trusted_by_peer(bool trusted) { is_trusted_by_peer_ = trusted; }
   bool is_trusted_by_peer() const { return is_trusted_by_peer_; }
 
+  // Indicates whether the remote peer on this transport is trusted as a source
+  // of privileged objects (e.g. HandleOwner::kRecipient handles or
+  // broker-destined transports). A non-broker implicitly trusts its broker;
+  // otherwise the peer must be explicitly trusted via is_peer_trusted(). When
+  // both endpoints are brokers, the peer being a broker does not by itself
+  // confer any trust on this end.
+  bool is_source_trusted() const {
+    return is_peer_trusted_ ||
+           (destination_type() == kBroker && source_type() != kBroker);
+  }
+
   ProcessTrust remote_process_trust() const { return remote_process_trust_; }
 
   void SetErrorHandler(MojoProcessErrorHandler handler, uintptr_t context) {
diff --git a/third_party/ipcz/src/ipcz/node_link.cc b/third_party/ipcz/src/ipcz/node_link.cc
index 0befa9b7..adf9b75 100644
--- a/third_party/ipcz/src/ipcz/node_link.cc
+++ b/third_party/ipcz/src/ipcz/node_link.cc
@@ -490,6 +490,13 @@
     return false;
   }
 
+  if (node()->type() == Node::Type::kBroker &&
+      accept.v0()->remote_node_type == Node::Type::kBroker) {
+    // Brokers are never introduced to other brokers; broker-to-broker links
+    // are always established directly via ConnectNode().
+    return false;
+  }
+
   DriverMemoryMapping mapping =
       DriverMemory(accept.TakeDriverObject(accept.v0()->memory)).Map();
   if (!mapping.is_valid()) {
@@ -835,7 +842,9 @@
 }
 
 bool NodeLink::OnRelayMessage(msg::RelayMessage& relay) {
-  if (node_->type() != Node::Type::kBroker) {
+  // Relay requests are only sent by non-brokers to their own broker.
+  if (node_->type() != Node::Type::kBroker ||
+      remote_node_type_ != Node::Type::kNormal) {
     return false;
   }
 
diff --git a/mojo/core/ipcz_driver/transport_test.cc b/mojo/core/ipcz_driver/transport_test.cc
index ee57b3f..0b4733a 100644
--- a/mojo/core/ipcz_driver/transport_test.cc
+++ b/mojo/core/ipcz_driver/transport_test.cc
@@ -942,6 +942,86 @@
   EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
 }
 
+DEFINE_TEST_CLIENT_TEST_WITH_PIPE(TransportFromUntrustedBrokerClient,
+                                  MojoIpczTransportTest,
+                                  h) {
+  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::kBroker, Transport::kNonBroker);
+    EXPECT_EQ(Transport::kBroker, their_new_transport->destination_type());
+    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(Transport::kNonBroker, Transport::kNonBroker);
+    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(Transport::kNonBroker, Transport::kNonBroker);
+    SerializeObjectFor(*transport, std::move(their_new_transport))
+        .Transmit(*transport);
+    EXPECT_EQ("got untrusted", listener.WaitForNextMessage().as_string());
+  }
+
+  EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
+}
+
+TEST_F(MojoIpczTransportTest, TransportFromUntrustedBroker) {
+  // When both endpoints of a transport are brokers, the peer being a broker
+  // does not by itself grant it any additional trust on this end. A broker
+  // receiving a serialized transport over such a link must reject it if it
+  // claims its own peer is trusted or is itself a broker.
+  RunTestClientWithController(
+      "TransportFromUntrustedBrokerClient", [&](ClientController& c) {
+        PlatformChannel channel;
+        MojoHandle transport_for_client =
+            WrapPlatformHandle(
+                channel.TakeRemoteEndpoint().TakePlatformHandle())
+                .release()
+                .value();
+        WriteMessageWithHandles(c.pipe(), "", &transport_for_client, 1);
+        scoped_refptr<Transport> transport = Transport::Create(
+            {.source = Transport::kBroker, .destination = Transport::kBroker},
+            channel.TakeLocalEndpoint(), c.process().Duplicate());
+        EXPECT_FALSE(transport->is_peer_trusted());
+
+        TransportListener listener(*transport);
+        TestMessage("ready").Transmit(*transport);
+
+        for (int i = 0; i < 2; i++) {
+          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();
+          scoped_refptr<Transport> received =
+              DeserializeObjectFrom<Transport>(*transport, message);
+          EXPECT_FALSE(received->is_peer_trusted());
+          EXPECT_EQ(Transport::kNonBroker, received->destination_type());
+          TestMessage("got untrusted").Transmit(*transport);
+        }
+
+        listener.WaitForDisconnect();
+      });
+}
+
 TEST_F(MojoIpczTransportTest, TransportFromUntrusted) {
 #if BUILDFLAG(IS_WIN)
   // TODO(crbug.com/414392683) default to untrusted/untracked.
diff --git a/third_party/ipcz/src/ipcz/node_link_test.cc b/third_party/ipcz/src/ipcz/node_link_test.cc
index 7d8ad77..5f11258 100644
--- a/third_party/ipcz/src/ipcz/node_link_test.cc
+++ b/third_party/ipcz/src/ipcz/node_link_test.cc
@@ -11,6 +11,7 @@
 #include "ipcz/link_side.h"
 #include "ipcz/link_type.h"
 #include "ipcz/node_link_memory.h"
+#include "ipcz/node_messages.h"
 #include "ipcz/remote_router_link.h"
 #include "ipcz/router.h"
 #include "ipcz/sublink_id.h"
@@ -65,6 +66,43 @@
   return {link0, link1};
 }
 
+std::pair<Ref<NodeLink>, Ref<NodeLink>> LinkBrokers(Ref<Node> broker0,
+                                                    Ref<Node> broker1) {
+  IpczDriverHandle handle0, handle1;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            GetDriver().CreateTransports(
+                IPCZ_INVALID_DRIVER_HANDLE, IPCZ_INVALID_DRIVER_HANDLE,
+                IPCZ_NO_FLAGS, nullptr, &handle0, &handle1));
+
+  auto transport0 =
+      MakeRefCounted<DriverTransport>(DriverObject(GetDriver(), handle0));
+  auto transport1 =
+      MakeRefCounted<DriverTransport>(DriverObject(GetDriver(), handle1));
+
+  DriverMemoryWithMapping buffer = NodeLinkMemory::AllocateMemory(GetDriver());
+  ABSL_ASSERT(buffer.mapping.is_valid());
+
+  auto link0 = NodeLink::CreateInactive(
+      broker0, LinkSide::kA, broker0->GetAssignedName(),
+      broker1->GetAssignedName(), Node::Type::kBroker, 0, broker1->features(),
+      transport0,
+      NodeLinkMemory::Create(broker0, LinkSide::kA, broker1->features(),
+                             std::move(buffer.mapping)));
+  auto link1 = NodeLink::CreateInactive(
+      broker1, LinkSide::kB, broker1->GetAssignedName(),
+      broker0->GetAssignedName(), Node::Type::kBroker, 0, broker0->features(),
+      transport1,
+      NodeLinkMemory::Create(broker1, LinkSide::kB, broker0->features(),
+                             buffer.memory.Map()));
+  broker0->AddConnection(broker1->GetAssignedName(),
+                         {.link = link0, .broker = link0});
+  broker1->AddConnection(broker0->GetAssignedName(),
+                         {.link = link1, .broker = link1});
+  link0->Activate();
+  link1->Activate();
+  return {link0, link1};
+}
+
 std::pair<Ref<Router>, Ref<Router>> AttachRouters(Ref<NodeLink> link0,
                                                   Ref<NodeLink> link1) {
   auto router0 = MakeRefCounted<Router>();
@@ -303,5 +341,53 @@
   link1->Deactivate();
 }
 
+TEST_F(NodeLinkTest, BrokerRejectsIntroductionToBroker) {
+  // Two brokers connected to one another, as in a broker-to-broker handshake.
+  // A broker should never be the target of an introduction to another broker:
+  // broker-to-broker links are only ever established directly via ConnectNode.
+  Ref<Node> node0 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  Ref<Node> node1 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  auto [link0, link1] = LinkBrokers(node0, node1);
+  auto [router0, router1] = AttachRouters(link0, link1);
+
+  auto new_transports = DriverTransport::CreatePair(GetDriver());
+  DriverMemoryWithMapping new_buffer =
+      NodeLinkMemory::AllocateMemory(GetDriver());
+  const NodeName introduced_name = node0->GenerateRandomName();
+  EXPECT_FALSE(node1->GetLink(introduced_name));
+  link0->AcceptIntroduction(introduced_name, LinkSide::kA, Node::Type::kBroker,
+                            0, Features{}, std::move(new_transports.first),
+                            std::move(new_buffer.memory));
+
+  // The introduction is rejected and no new connection is established.
+  EXPECT_FALSE(node1->GetLink(introduced_name));
+  EXPECT_TRUE(router1->IsPeerClosed());
+
+  router0->CloseRoute();
+  router1->CloseRoute();
+  node0->Close();
+  node1->Close();
+}
+
+TEST_F(NodeLinkTest, BrokerRejectsRelayFromBroker) {
+  // Relay requests are only ever sent by non-brokers to their own broker, so a
+  // broker must reject any relay request arriving over a broker-to-broker link.
+  Ref<Node> node0 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  Ref<Node> node1 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  auto [link0, link1] = LinkBrokers(node0, node1);
+  auto [router0, router1] = AttachRouters(link0, link1);
+
+  msg::AddBlockBuffer payload;
+  link0->RelayMessage(node0->GetAssignedName(), payload);
+
+  // The relay request is rejected and the link is torn down.
+  EXPECT_TRUE(router1->IsPeerClosed());
+
+  router0->CloseRoute();
+  router1->CloseRoute();
+  node0->Close();
+  node1->Close();
+}
+
 }  // namespace
 }  // namespace ipcz
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 ee57b3f..0b4733a 100644
--- a/mojo/core/ipcz_driver/transport_test.cc
+++ b/mojo/core/ipcz_driver/transport_test.cc
@@ -942,6 +942,86 @@
   EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
 }
 
+DEFINE_TEST_CLIENT_TEST_WITH_PIPE(TransportFromUntrustedBrokerClient,
+                                  MojoIpczTransportTest,
+                                  h) {
+  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::kBroker, Transport::kNonBroker);
+    EXPECT_EQ(Transport::kBroker, their_new_transport->destination_type());
+    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(Transport::kNonBroker, Transport::kNonBroker);
+    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(Transport::kNonBroker, Transport::kNonBroker);
+    SerializeObjectFor(*transport, std::move(their_new_transport))
+        .Transmit(*transport);
+    EXPECT_EQ("got untrusted", listener.WaitForNextMessage().as_string());
+  }
+
+  EXPECT_EQ(MOJO_RESULT_OK, MojoClose(h));
+}
+
+TEST_F(MojoIpczTransportTest, TransportFromUntrustedBroker) {
+  // When both endpoints of a transport are brokers, the peer being a broker
+  // does not by itself grant it any additional trust on this end. A broker
+  // receiving a serialized transport over such a link must reject it if it
+  // claims its own peer is trusted or is itself a broker.
+  RunTestClientWithController(
+      "TransportFromUntrustedBrokerClient", [&](ClientController& c) {
+        PlatformChannel channel;
+        MojoHandle transport_for_client =
+            WrapPlatformHandle(
+                channel.TakeRemoteEndpoint().TakePlatformHandle())
+                .release()
+                .value();
+        WriteMessageWithHandles(c.pipe(), "", &transport_for_client, 1);
+        scoped_refptr<Transport> transport = Transport::Create(
+            {.source = Transport::kBroker, .destination = Transport::kBroker},
+            channel.TakeLocalEndpoint(), c.process().Duplicate());
+        EXPECT_FALSE(transport->is_peer_trusted());
+
+        TransportListener listener(*transport);
+        TestMessage("ready").Transmit(*transport);
+
+        for (int i = 0; i < 2; i++) {
+          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();
+          scoped_refptr<Transport> received =
+              DeserializeObjectFrom<Transport>(*transport, message);
+          EXPECT_FALSE(received->is_peer_trusted());
+          EXPECT_EQ(Transport::kNonBroker, received->destination_type());
+          TestMessage("got untrusted").Transmit(*transport);
+        }
+
+        listener.WaitForDisconnect();
+      });
+}
+
 TEST_F(MojoIpczTransportTest, TransportFromUntrusted) {
 #if BUILDFLAG(IS_WIN)
   // TODO(crbug.com/414392683) default to untrusted/untracked.
diff --git a/third_party/ipcz/src/ipcz/node_link_test.cc b/third_party/ipcz/src/ipcz/node_link_test.cc
index 7d8ad77..5f11258 100644
--- a/third_party/ipcz/src/ipcz/node_link_test.cc
+++ b/third_party/ipcz/src/ipcz/node_link_test.cc
@@ -11,6 +11,7 @@
 #include "ipcz/link_side.h"
 #include "ipcz/link_type.h"
 #include "ipcz/node_link_memory.h"
+#include "ipcz/node_messages.h"
 #include "ipcz/remote_router_link.h"
 #include "ipcz/router.h"
 #include "ipcz/sublink_id.h"
@@ -65,6 +66,43 @@
   return {link0, link1};
 }
 
+std::pair<Ref<NodeLink>, Ref<NodeLink>> LinkBrokers(Ref<Node> broker0,
+                                                    Ref<Node> broker1) {
+  IpczDriverHandle handle0, handle1;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            GetDriver().CreateTransports(
+                IPCZ_INVALID_DRIVER_HANDLE, IPCZ_INVALID_DRIVER_HANDLE,
+                IPCZ_NO_FLAGS, nullptr, &handle0, &handle1));
+
+  auto transport0 =
+      MakeRefCounted<DriverTransport>(DriverObject(GetDriver(), handle0));
+  auto transport1 =
+      MakeRefCounted<DriverTransport>(DriverObject(GetDriver(), handle1));
+
+  DriverMemoryWithMapping buffer = NodeLinkMemory::AllocateMemory(GetDriver());
+  ABSL_ASSERT(buffer.mapping.is_valid());
+
+  auto link0 = NodeLink::CreateInactive(
+      broker0, LinkSide::kA, broker0->GetAssignedName(),
+      broker1->GetAssignedName(), Node::Type::kBroker, 0, broker1->features(),
+      transport0,
+      NodeLinkMemory::Create(broker0, LinkSide::kA, broker1->features(),
+                             std::move(buffer.mapping)));
+  auto link1 = NodeLink::CreateInactive(
+      broker1, LinkSide::kB, broker1->GetAssignedName(),
+      broker0->GetAssignedName(), Node::Type::kBroker, 0, broker0->features(),
+      transport1,
+      NodeLinkMemory::Create(broker1, LinkSide::kB, broker0->features(),
+                             buffer.memory.Map()));
+  broker0->AddConnection(broker1->GetAssignedName(),
+                         {.link = link0, .broker = link0});
+  broker1->AddConnection(broker0->GetAssignedName(),
+                         {.link = link1, .broker = link1});
+  link0->Activate();
+  link1->Activate();
+  return {link0, link1};
+}
+
 std::pair<Ref<Router>, Ref<Router>> AttachRouters(Ref<NodeLink> link0,
                                                   Ref<NodeLink> link1) {
   auto router0 = MakeRefCounted<Router>();
@@ -303,5 +341,53 @@
   link1->Deactivate();
 }
 
+TEST_F(NodeLinkTest, BrokerRejectsIntroductionToBroker) {
+  // Two brokers connected to one another, as in a broker-to-broker handshake.
+  // A broker should never be the target of an introduction to another broker:
+  // broker-to-broker links are only ever established directly via ConnectNode.
+  Ref<Node> node0 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  Ref<Node> node1 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  auto [link0, link1] = LinkBrokers(node0, node1);
+  auto [router0, router1] = AttachRouters(link0, link1);
+
+  auto new_transports = DriverTransport::CreatePair(GetDriver());
+  DriverMemoryWithMapping new_buffer =
+      NodeLinkMemory::AllocateMemory(GetDriver());
+  const NodeName introduced_name = node0->GenerateRandomName();
+  EXPECT_FALSE(node1->GetLink(introduced_name));
+  link0->AcceptIntroduction(introduced_name, LinkSide::kA, Node::Type::kBroker,
+                            0, Features{}, std::move(new_transports.first),
+                            std::move(new_buffer.memory));
+
+  // The introduction is rejected and no new connection is established.
+  EXPECT_FALSE(node1->GetLink(introduced_name));
+  EXPECT_TRUE(router1->IsPeerClosed());
+
+  router0->CloseRoute();
+  router1->CloseRoute();
+  node0->Close();
+  node1->Close();
+}
+
+TEST_F(NodeLinkTest, BrokerRejectsRelayFromBroker) {
+  // Relay requests are only ever sent by non-brokers to their own broker, so a
+  // broker must reject any relay request arriving over a broker-to-broker link.
+  Ref<Node> node0 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  Ref<Node> node1 = MakeRefCounted<Node>(Node::Type::kBroker, GetDriver());
+  auto [link0, link1] = LinkBrokers(node0, node1);
+  auto [router0, router1] = AttachRouters(link0, link1);
+
+  msg::AddBlockBuffer payload;
+  link0->RelayMessage(node0->GetAssignedName(), payload);
+
+  // The relay request is rejected and the link is torn down.
+  EXPECT_TRUE(router1->IsPeerClosed());
+
+  router0->CloseRoute();
+  router1->CloseRoute();
+  node0->Close();
+  node1->Close();
+}
+
 }  // namespace
 }  // namespace ipcz
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.