[llvm] [orc-rt] Add VettedPeer, require it for the socket transport (PR #228336)
Lang Hames via llvm-commits
llvm-commits at lists.llvm.org
Thu Oct 1 23:00:05 PDT 2026
https://github.com/lhames created https://github.com/llvm/llvm-project/pull/228336
The controller at the other end of a channel can make the executor run arbitrary code, so attaching to it is a trust decision. VettedPeer<ChannelT> makes that decision explicit: it can only be made through one of three named factories -- inherited, checked or unchecked -- each naming the reason the peer is trusted.
createSimpleRemoteCAOverSocket now takes a VettedPeer<SocketHandle> rather than a bare SocketHandle.
VettedPeer neither verifies nor records the reason. The factories exist so that the decision can't be skipped by omission, and so that every choice is visible in the source.
The socket:adopt connector trusts its socket as inherited, and documents the resulting precondition on its callers.
Assisted-by: Claude
>From 3b36b2422dffd7c78e95ee1fac7a792d84a0db8d Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Fri, 2 Oct 2026 15:23:15 +1000
Subject: [PATCH] [orc-rt] Add VettedPeer, require it for the socket transport
The controller at the other end of a channel can make the executor run
arbitrary code, so attaching to it is a trust decision.
VettedPeer<ChannelT> makes that decision explicit: it can only be made
through one of three named factories -- inherited, checked or unchecked
-- each naming the reason the peer is trusted.
createSimpleRemoteCAOverSocket now takes a VettedPeer<SocketHandle>
rather than a bare SocketHandle.
VettedPeer neither verifies nor records the reason. The factories exist
so that the decision can't be skipped by omission, and so that every
choice is visible in the source.
The socket:adopt connector trusts its socket as inherited, and documents
the resulting precondition on its callers.
Assisted-by: Claude
---
orc-rt/include/CMakeLists.txt | 1 +
.../include/orc-rt/bedrock/SocketConnector.h | 5 ++
orc-rt/include/orc-rt/bedrock/VettedPeer.h | 68 +++++++++++++++++++
.../bedrock/sps/SimpleRemoteCAOverSocket.h | 12 ++--
.../lib/bedrock/sys/posix/SocketConnector.cpp | 4 +-
.../posix/sps/SimpleRemoteCAOverSocket.cpp | 4 +-
orc-rt/test/unit/CMakeLists.txt | 1 +
orc-rt/test/unit/bedrock/VettedPeerTest.cpp | 44 ++++++++++++
.../sps/SimpleRemoteCAOverSocketTest.cpp | 12 +++-
9 files changed, 142 insertions(+), 9 deletions(-)
create mode 100644 orc-rt/include/orc-rt/bedrock/VettedPeer.h
create mode 100644 orc-rt/test/unit/bedrock/VettedPeerTest.cpp
diff --git a/orc-rt/include/CMakeLists.txt b/orc-rt/include/CMakeLists.txt
index b7a8453d0f488f9..18b75d96dd38365 100644
--- a/orc-rt/include/CMakeLists.txt
+++ b/orc-rt/include/CMakeLists.txt
@@ -27,6 +27,7 @@ set(ORC_RT_HEADERS
orc-rt/bedrock/SocketHandle.h
orc-rt/bedrock/TaskGroup.h
orc-rt/bedrock/ThreadPoolRunner.h
+ orc-rt/bedrock/VettedPeer.h
orc-rt/bedrock/sps/AllSPSCI.h
orc-rt/bedrock/sps/CallSPSCI.h
orc-rt/bedrock/sps/GDBJITRegistrarSPSCI.h
diff --git a/orc-rt/include/orc-rt/bedrock/SocketConnector.h b/orc-rt/include/orc-rt/bedrock/SocketConnector.h
index ec23d17eb241df7..f8002b1787d6577 100644
--- a/orc-rt/include/orc-rt/bedrock/SocketConnector.h
+++ b/orc-rt/include/orc-rt/bedrock/SocketConnector.h
@@ -20,6 +20,11 @@ namespace orc_rt {
/// Registers the connector for the "socket" transport, whose only action is
/// "adopt": a stream socket this process was handed, already connected.
///
+/// The connector trusts the socket's peer as inherited, so the caller must
+/// ensure that the spec comes from whoever set up the descriptor (normally the
+/// process that started this one), and that the conditions on
+/// VettedPeer::inherited hold.
+///
/// If the descriptor named by the spec is a socket, the connector takes
/// ownership of it whether or not the connection succeeds. Otherwise it is left
/// untouched.
diff --git a/orc-rt/include/orc-rt/bedrock/VettedPeer.h b/orc-rt/include/orc-rt/bedrock/VettedPeer.h
new file mode 100644
index 000000000000000..93a3f33812bff6b
--- /dev/null
+++ b/orc-rt/include/orc-rt/bedrock/VettedPeer.h
@@ -0,0 +1,68 @@
+//===- VettedPeer.h - A channel whose peer has been vetted ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// A channel, marked with the basis on which this process chose to trust the
+// peer at its other end.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef ORC_RT_BEDROCK_VETTEDPEER_H
+#define ORC_RT_BEDROCK_VETTEDPEER_H
+
+#include <utility>
+
+namespace orc_rt {
+
+/// A channel to a peer that this process has decided to trust.
+///
+/// The peer at the other end of a controller channel can send this process code
+/// to run, so deciding to trust it is the most security-sensitive decision a
+/// JIT executor makes. Interfaces that start a conversation over a channel take
+/// a VettedPeer rather than a bare channel, so the decision can't be skipped
+/// by omission: the only way to make a VettedPeer is through one of the
+/// factories below, and each one names a reason for trusting the peer.
+///
+/// A VettedPeer can't check that the reason is sound, only that one was chosen.
+/// Because each reason is a named factory, every choice shows up in the source
+/// and is easy to search for (e.g. "VettedPeer<SocketHandle>::unchecked").
+template <typename ChannelT> class VettedPeer {
+public:
+ /// The channel was handed to this process by whoever started it, e.g. a
+ /// socket descriptor inherited across exec.
+ ///
+ /// Trusting this peer grants it nothing new, since whoever started this
+ /// process could already have made it run any code they liked. That holds
+ /// only if starting it gained no privilege, which the caller must ensure: no
+ /// setuid or setgid, no file capabilities, no change of security domain (e.g.
+ /// SELinux or AppArmor), and no added entitlements.
+ static VettedPeer inherited(ChannelT C) { return VettedPeer(std::move(C)); }
+
+ /// The peer's identity was checked against a requirement before this was
+ /// made, e.g. a unix domain socket peer's user id, or an XPC peer's code
+ /// signature.
+ static VettedPeer checked(ChannelT C) { return VettedPeer(std::move(C)); }
+
+ /// The peer is not identified, e.g. the peer of a TCP connection, which is
+ /// known only by its address.
+ ///
+ /// Anyone who can impersonate the peer, or anyone at all who can reach a
+ /// listening endpoint, can run code in this process.
+ static VettedPeer unchecked(ChannelT C) { return VettedPeer(std::move(C)); }
+
+ /// Gives up the channel, for the interface that this VettedPeer was made for.
+ ChannelT take() && { return std::move(C); }
+
+private:
+ explicit VettedPeer(ChannelT C) : C(std::move(C)) {}
+
+ ChannelT C;
+};
+
+} // namespace orc_rt
+
+#endif // ORC_RT_BEDROCK_VETTEDPEER_H
diff --git a/orc-rt/include/orc-rt/bedrock/sps/SimpleRemoteCAOverSocket.h b/orc-rt/include/orc-rt/bedrock/sps/SimpleRemoteCAOverSocket.h
index afd2bbfc851d210..7ff6117272413f8 100644
--- a/orc-rt/include/orc-rt/bedrock/sps/SimpleRemoteCAOverSocket.h
+++ b/orc-rt/include/orc-rt/bedrock/sps/SimpleRemoteCAOverSocket.h
@@ -16,15 +16,19 @@
#include "orc-rt/bedrock/Session.h"
#include "orc-rt/bedrock/SocketHandle.h"
+#include "orc-rt/bedrock/VettedPeer.h"
#include "orc-rt/support/Error.h"
#include <memory>
namespace orc_rt {
-/// Creates a ControllerAccess that carries SimpleRemote messages over Sock,
-/// taking ownership of it. Fails if Sock is not a stream socket. Sock must be
-/// connected.
+/// Creates a ControllerAccess that carries SimpleRemote messages over Peer's
+/// socket, taking ownership of it. Fails if the socket is not a stream socket.
+/// The socket must be connected.
+///
+/// Takes a VettedPeer rather than a bare socket because whoever is at the other
+/// end can send this process code to run. See VettedPeer for how to choose.
///
/// The result is ready to hand to Session::attach, which is what starts the
/// conversation; nothing is sent before then.
@@ -35,7 +39,7 @@ namespace orc_rt {
/// need something else entirely. The wire format is the same either way, and
/// matches LLVM's SimpleRemoteEPC.
Expected<std::shared_ptr<Session::ControllerAccess>>
-createSimpleRemoteCAOverSocket(Session &S, SocketHandle Sock);
+createSimpleRemoteCAOverSocket(Session &S, VettedPeer<SocketHandle> Peer);
} // namespace orc_rt
diff --git a/orc-rt/lib/bedrock/sys/posix/SocketConnector.cpp b/orc-rt/lib/bedrock/sys/posix/SocketConnector.cpp
index dd1fff2719417f4..616434f1a2f77de 100644
--- a/orc-rt/lib/bedrock/sys/posix/SocketConnector.cpp
+++ b/orc-rt/lib/bedrock/sys/posix/SocketConnector.cpp
@@ -60,7 +60,9 @@ Error socketConnector(const ConnectionSpec &CS, Session &S,
" is not a socket (" + sys::strError(ErrNum) + ")");
}
- auto CA = createSimpleRemoteCAOverSocket(S, SocketHandle(FD));
+ // Inherited, under the preconditions documented on registerSocketConnector.
+ auto CA = createSimpleRemoteCAOverSocket(
+ S, VettedPeer<SocketHandle>::inherited(SocketHandle(FD)));
if (!CA)
return CA.takeError();
diff --git a/orc-rt/lib/bedrock/sys/posix/sps/SimpleRemoteCAOverSocket.cpp b/orc-rt/lib/bedrock/sys/posix/sps/SimpleRemoteCAOverSocket.cpp
index 739aa580bf3b578..4a8ee4e474bcf76 100644
--- a/orc-rt/lib/bedrock/sys/posix/sps/SimpleRemoteCAOverSocket.cpp
+++ b/orc-rt/lib/bedrock/sys/posix/sps/SimpleRemoteCAOverSocket.cpp
@@ -551,8 +551,8 @@ SocketSimpleRemoteCA::takePendingCall(uint64_t SeqNo) {
}
Expected<std::shared_ptr<Session::ControllerAccess>>
-createSimpleRemoteCAOverSocket(Session &S, SocketHandle Sock) {
- return SocketSimpleRemoteCA::Create(S, std::move(Sock));
+createSimpleRemoteCAOverSocket(Session &S, VettedPeer<SocketHandle> Peer) {
+ return SocketSimpleRemoteCA::Create(S, std::move(Peer).take());
}
} // namespace orc_rt
diff --git a/orc-rt/test/unit/CMakeLists.txt b/orc-rt/test/unit/CMakeLists.txt
index c2686dff5cfe704..44869f97b4d7c49 100644
--- a/orc-rt/test/unit/CMakeLists.txt
+++ b/orc-rt/test/unit/CMakeLists.txt
@@ -125,6 +125,7 @@ add_orc_rt_unittest(BedrockTests
bedrock/SimpleSymbolTableTest.cpp
bedrock/TaskGroupTest.cpp
bedrock/ThreadPoolRunnerTest.cpp
+ bedrock/VettedPeerTest.cpp
bedrock/sps/CallSPSCITest.cpp
bedrock/sps/MemoryAccessSPSCITest.cpp
diff --git a/orc-rt/test/unit/bedrock/VettedPeerTest.cpp b/orc-rt/test/unit/bedrock/VettedPeerTest.cpp
new file mode 100644
index 000000000000000..049dfa09d9fd697
--- /dev/null
+++ b/orc-rt/test/unit/bedrock/VettedPeerTest.cpp
@@ -0,0 +1,44 @@
+//===- VettedPeerTest.cpp -------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Tests for VettedPeer.
+//
+//===----------------------------------------------------------------------===//
+
+#include "orc-rt/bedrock/VettedPeer.h"
+
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <type_traits>
+
+using namespace orc_rt;
+
+namespace {
+
+// A stand-in for a move-only channel such as SocketHandle.
+using Channel = std::unique_ptr<int>;
+
+// The point of VettedPeer: a bare channel does not become one by accident.
+// Only the named factories make one.
+static_assert(!std::is_constructible_v<VettedPeer<Channel>, Channel>);
+static_assert(!std::is_convertible_v<Channel, VettedPeer<Channel>>);
+
+TEST(VettedPeerTest, TakeYieldsTheChannel) {
+ auto Check = [](VettedPeer<Channel> (*Make)(Channel)) {
+ auto C = std::make_unique<int>(42);
+ int *Raw = C.get();
+ Channel Out = Make(std::move(C)).take();
+ EXPECT_EQ(Out.get(), Raw);
+ };
+ Check(VettedPeer<Channel>::inherited);
+ Check(VettedPeer<Channel>::checked);
+ Check(VettedPeer<Channel>::unchecked);
+}
+
+} // namespace
diff --git a/orc-rt/test/unit/bedrock/sps/SimpleRemoteCAOverSocketTest.cpp b/orc-rt/test/unit/bedrock/sps/SimpleRemoteCAOverSocketTest.cpp
index 9a51eb7a58c2b9d..174939e1bd3a4be 100644
--- a/orc-rt/test/unit/bedrock/sps/SimpleRemoteCAOverSocketTest.cpp
+++ b/orc-rt/test/unit/bedrock/sps/SimpleRemoteCAOverSocketTest.cpp
@@ -39,6 +39,7 @@
#include <string>
#include <string_view>
#include <thread>
+#include <type_traits>
#include <utility>
#include <vector>
@@ -92,7 +93,8 @@ class SimpleRemoteCAOverSocketTest : public ::testing::Test {
/// Creates a CA over Near and attaches it, the way a connector would.
Error attachOverSocket() {
- auto CA = createSimpleRemoteCAOverSocket(S, std::move(Near));
+ auto CA = createSimpleRemoteCAOverSocket(
+ S, VettedPeer<SocketHandle>::unchecked(std::move(Near)));
if (!CA)
return CA.takeError();
S.attach(std::move(*CA), BootstrapInfo(S));
@@ -284,6 +286,11 @@ void outOfBandErrorWrapper(orc_rt_SessionRef S,
} // namespace
+// Starting a conversation means deciding to trust the peer, so the factory
+// takes only a VettedPeer, never a bare socket.
+static_assert(!std::is_invocable_v<decltype(createSimpleRemoteCAOverSocket),
+ Session &, SocketHandle>);
+
TEST_F(SimpleRemoteCAOverSocketTest, RejectsANonStreamSocket) {
// The framing reads a message in as many parts as the stream delivers it, so
// a socket that preserves message boundaries would truncate one.
@@ -291,7 +298,8 @@ TEST_F(SimpleRemoteCAOverSocketTest, RejectsANonStreamSocket) {
ASSERT_TRUE(H.has_value()) << "could not create a socket for the test";
EXPECT_THAT_EXPECTED(
- createSimpleRemoteCAOverSocket(S, SocketHandle(*H)),
+ createSimpleRemoteCAOverSocket(
+ S, VettedPeer<SocketHandle>::unchecked(SocketHandle(*H))),
FailedWithMessage(HasSubstr("requires a stream socket")));
EXPECT_FALSE(isNativeSocketOpen(*H))
<< "a rejected socket is still owned, and must be closed";
More information about the llvm-commits
mailing list