Skip to content

Let the broker create native fork children's control sockets - #1758

Draft
Weiteng Chen (CvvT) wants to merge 7 commits into
uliteboxfrom
weiteng/broker-fork-socket
Draft

Weiteng Chen (CvvT) wants to merge 7 commits into
uliteboxfrom
weiteng/broker-fork-socket

Conversation

@CvvT

Copy link
Copy Markdown
Contributor

A Linux runner that forks natively used to create its clone's control socket pair itself and send the broker the descriptor number of the broker's end inside the clone, which the broker then pulled out of the clone with pidfd_getfd. That needed a dumpable runner and a Yama ptrace_scope below 2, had the broker reach into an untrusted runner's descriptor table at a number the runner chose, and needed the runner's syscall filter to allow socketpair.

Now the broker creates the pair, keeps its end with the pending child, and delivers the other end over the parent's shared-object socket; the clone inherits it.

@CvvT
Weiteng Chen (CvvT) force-pushed the weiteng/broker-fork-socket branch from 747065c to e337088 Compare October 29, 2026 23:52
Weiteng Chen (CvvT) and others added 3 commits October 30, 2026 15:48
A Linux runner that forks natively used to create its clone's control
socket pair itself and send the broker the descriptor number of the
broker's end inside the clone, which the broker then pulled out of the
clone with pidfd_getfd. That needed a dumpable runner and a Yama
ptrace_scope below 2, had the broker reach into an untrusted runner's
descriptor table at a number the runner chose, and needed the runner's
syscall filter to allow socketpair.

Now the runner asks the broker for its pending child's connection
before cloning (ProcessRequest::ConnectChild). The broker creates the
pair, keeps its end with the pending child, and delivers the other end
over the parent's shared-object socket; the clone inherits it, and
adoption takes the broker's end by the child's broker-issued process ID.

- Protocol: ConnectChild(child) answered with the delivery token.
- Broker host: routes it to ProcessLauncher::connect_child after
  checking the child is the caller's pending child (new
  BrokerProcess::check_pending_child); launchers that cannot deliver
  objects keep the default, which reports it unsupported.
- Userland broker: ChildConnections registers each association's
  shared-object sender and keeps at most one connection per parent, as a
  process has at most one pending child; adoption no longer uses pidfds
  to take a descriptor, keeping its waitid and supervision checks.
  ExistingRunnerDescriptor now holds only the process ID.
- Runner: ForkRunner::create_control_channel receives the pending
  child; the Linux runner takes the delivered socket and moves it to the
  control-socket number in the clone. Its syscall filter no longer
  allows socketpair, and native_fork_supported checks only for pidfds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ending child

The broker now enables SO_PASSCRED on its end of a clone's control socket and,
before adopting the clone, checks that the process named in the runner
descriptor sent the first data on it, binding the adopted PID to the
connection.

The broker's end is kept with the pending child in the broker core, so it is
dropped when the child is cancelled, started without it, or replaced, and is
handed to launch_existing atomically with the pending-child check. This
replaces check_pending_child, the launcher's pending map, and the Windows
connection stubs. pidfd_open is shared from the Linux platform crate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ProcessLauncher declares its connection as an associated type, so launchers
see their own concrete type; the broker core keeps the type erasure private
and refuses a connection of the wrong type without taking the child.

The token under which the broker delivers a shared object becomes
SharedObjectToken in the protocol. The userland broker separates creating a
child's control connection from delivering it, so SharedObjectSenders::send
delivers any host object.

native_fork_supported only probed pidfd_open (Linux 5.3), but the userland
broker already requires Linux 5.11 to spawn any runner (close_range with
CLOSE_RANGE_CLOEXEC), and fork requires that broker, so it is removed.

Also trims ProcessLauncher and seccomp comments and drops a trivial
descriptor round-trip test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@CvvT
Weiteng Chen (CvvT) force-pushed the weiteng/broker-fork-socket branch from 173ed0d to ba500e9 Compare October 30, 2026 22:50
Weiteng Chen (CvvT) and others added 4 commits October 30, 2026 23:49
The kernel stamps SCM_CREDENTIALS with a PID number taken when data is sent, so data queued
before the broker checked the named clone could carry a PID that was since reused. The broker
now pins the clone (an unreaped child, via waitid WNOWAIT), requires the connection's queue to
be empty, and admits the clone with a byte it waits for before sending. Only then does it
accept the first data, whose stamp must name the clone.

Also:
- Parse credentials as libc::ucred; rustix's UCred cannot hold the PID 0 Linux stamps on data
  from senders it cannot name.
- Return an AuthenticatedClone that adopt consumes, so adoption cannot skip authentication.
- Map clone adoption failures to EPERM, EIO, or EAGAIN instead of always EPERM.
- Share one take path for pending children in broker core, renaming connect_child_process
  there to attach_child_connection.
- Unregister shared-object senders through an RAII guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…registry lock

On Linux 6.5 and later, the broker enables SO_PASSPIDFD with SO_PASSCRED on its end of a clone's
control socket, so the kernel attaches the sender's pidfd to the clone's first data. The pidfd
refers to the sending process itself, not a PID number that may be reused, so the broker admits
the clone at once and needs no pin, empty-queue check, or admission wait: it requires the stamped
PID to name the clone and keeps the pidfd. Older kernels, which reject SO_PASSPIDFD with
ENOPROTOOPT, keep the existing protocol, whose pin now opens a pidfd for the named clone.

Either way, adopt checks the clone's pidfd under the runner registry lock, under which runners
become supervised. Authentication may wait, during which a supervised runner with the named PID
could be reaped by its owner, deregistered, and its PID reused by a process that is not the
broker's child; adopting that process would let its teardown SIGKILL whatever later takes the PID.
Only supervision reaps runners, so a pidfd that still refers to an unsupervised running child keeps
its PID until the broker registers it. pidfd_open and waitid(P_PIDFD) need Linux 5.3 and 5.4,
within the userland broker's existing Linux 5.11 requirement.

Also:
- Close the descriptors that even a peek installs from SCM_RIGHTS, and reject data whose control
  messages were truncated.
- Send shared objects under the senders lock, so dropping a registration stops delivery.
- Return NotConnected rather than panic when a closed Windows runner endpoint is served.
- Rename ConnectChild and connect_child_process to RequestChildConnection and
  request_child_connection.
- Fix stale comments on runner descriptors, clone adoption errors, and the admission wait.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…the request

SenderIdentifyingStream in the Linux transport now owns the broker's end of a native
clone's control connection: it enables SO_PASSPIDFD where the kernel supports it and
falls back to process-ID identification, whose pin, empty-queue check, and admission
byte it runs itself. The userland broker only supplies the pin, and adopts the
returned stream and pidfd. Control-message parsing moves to control_message.rs.

Also:
- Rename RequestChildConnection to CreateChildConnection, documenting UnknownObject for
  a process with no such pending child.
- Drop the runner's socketpair seccomp rule, now that the broker creates the pair.
- Keep shared-object senders on the process launcher.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…private

Tests:
- Drop tests that only restated a match: the clone adoption error mapping and
  the missing-sender-pidfd mapping.
- Test pin_running_child directly, and test adoption through
  SenderIdentifyingStream::pair, so the broker's tests no longer force a sender
  identification mode. Drop cases the transport tests already cover.
- Drop the duplicated admit-then-send block from the admission-wait test.

Transport:
- Fall back to process-ID identification on the same socket pair instead of
  creating a second one when the kernel rejects SO_PASSPIDFD.
- Make SenderIdentification private, move pair_identified_by into the tests,
  and drop the unused identification getter.
- Shorten the verify_first_sender documentation, leaving the broker's
  running-child requirement to adopt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

🤖 SemverChecks 🤖 ⚠️ Potential breaking API changes detected ⚠️

Click for details
--- failure trait_associated_type_added: non-sealed public trait added associated type without default value ---

Description:
A non-sealed trait has gained an associated type without a default value, which breaks downstream implementations of the trait
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/trait_associated_type_added.ron

Failed in:
  trait associated type litebox_broker_host::ProcessLauncher::ChildConnection in file /home/runner/work/litebox/litebox/litebox_broker_host/src/lib.rs:1056

--- failure trait_method_parameter_count_changed: pub trait method parameter count changed ---

Description:
A trait method now takes a different number of parameters.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-item-signature
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/trait_method_parameter_count_changed.ron

Failed in:
  ProcessLauncher::launch_existing now takes 5 instead of 4 parameters, in file /home/runner/work/litebox/litebox/litebox_broker_host/src/lib.rs:1090

--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/enum_variant_added.ron

Failed in:
  variant ProcessRequest:CreateChildConnection in /home/runner/work/litebox/litebox/litebox_broker_protocol/src/message.rs:403
  variant ProcessResponse:CreateChildConnection in /home/runner/work/litebox/litebox/litebox_broker_protocol/src/message.rs:682

--- failure struct_pub_field_missing: pub struct's pub field removed or renamed ---

Description:
A publicly-visible struct has at least one public field that is no longer available under its prior name. It may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/struct_pub_field_missing.ron

Failed in:
  field broker_fd of struct ExistingRunnerDescriptor, previously in file /home/runner/work/litebox/litebox/target/semver-checks/git-ulitebox/19984829f29f93adf9cc3fd5e0cb37246e361713/litebox_broker_transport_linux_userland/src/existing_runner.rs:21

--- failure function_missing: pub fn removed or renamed ---

Description:
A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/function_missing.ron

Failed in:
  function litebox_platform_linux_userland::process::native_fork_supported, previously in file /home/runner/work/litebox/litebox/target/semver-checks/git-ulitebox/19984829f29f93adf9cc3fd5e0cb37246e361713/litebox_platform_linux_userland/src/process.rs:108

--- failure trait_method_parameter_count_changed: pub trait method parameter count changed ---

Description:
A trait method now takes a different number of parameters.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-item-signature
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/trait_method_parameter_count_changed.ron

Failed in:
  ForkRunner::create_control_channel now takes 1 instead of 0 parameters, in file /home/runner/work/litebox/litebox/litebox_shim_linux/src/syscalls/process.rs:85

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant