CVE-2026-91734
Overview
Background
- Broker vs. non-broker node
- In Mojo’s
ipcztransport, akBrokernode (e.g. the browser process) is privileged to allocate shared state and vouch for other nodes, while akNonBrokernode (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 aNodeLinkwhose local and remote node types are bothkBroker. - Transport trust flags
- A
Transportcarries asource_type()/destination_type()(kBrokerorkNonBroker) andis_peer_trusted_/is_trusted_by_peer_, which together decide whether the peer may be treated as a source of privileged objects such asHandleOwner::kRecipienthandles or broker-destined transports. - `Transport::Deserialize`
- The
mojoipczdriver routine that reconstructs an embedded object (e.g. anotherTransport) 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.
Attack Path
- Establish an isolated broker-to-broker link
An attacker-controlled node participating as a
kBrokerconnects directly to a victim broker, yielding aNodeLink/Transportwhere bothsource_type()anddestination_type()arekBrokerandis_peer_trusted_was never set. - Exploit the implicit-trust check
On the victim,
Transport::Deserializecomputedis_source_trustedas true purely becausedestination_type() == kBroker, so the untrusted peer broker was accepted as a source of privileged objects. - Convey a privileged object
As the regression test
TransportFromUntrustedBrokerdemonstrates, the attacker serializes and transmits aTransportclaiming akBrokerdestination (oris_peer_trusted == true); pre-patch these were honored instead of rejected withIPCZ_RESULT_INVALID_ARGUMENT. - 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). - 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
LogicError / “Incorrect authorization in Core” and reports no in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmojo/core/ipcz_driver/driver_for_ipcz_tests.cc |
modified | |
TEST_Fmojo/core/ipcz_driver/transport_test.cc |
modified | |
formojo/core/ipcz_driver/transport_test.cc |
modified | |
TEST_Fthird_party/ipcz/src/ipcz/node_link_test.cc |
modified |
Files Changed
third_party/ipcz/src/ipcz/node_link_test.ccmojo/core/ipcz_driver/transport_test.ccmojo/core/ipcz_driver/transport.ccthird_party/ipcz/src/ipcz/node_link.ccmojo/core/ipcz_driver/transport.hmojo/core/ipcz_driver/driver_for_ipcz_tests.cc
Audit Directions
- Implicit trust from endpoint roleAudit 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 linksReview
ipcz/mojopaths 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 provenanceIn
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.
Patch
From 7858f67868ff8416db134d4f9e1a38731df6504b Mon Sep 17 00:00:00 2001
From: Will Harris <wfh@chromium.org>
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 <ajgo@chromium.org>
Commit-Queue: Will Harris <wfh@chromium.org>
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
Regression Test / PoC
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