2026-07-14 22:08:29 +09:00
|
|
|
use std::ffi::{OsStr, OsString};
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{self, Read, Write};
|
2026-07-15 19:48:42 +09:00
|
|
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
|
2026-07-14 22:08:29 +09:00
|
|
|
use std::process::Stdio;
|
|
|
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
|
|
|
|
|
|
use lazaret_core::identity::{
|
2026-07-14 23:50:30 +09:00
|
|
|
BootId, CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsDeviceId, PidfsInodeId,
|
|
|
|
|
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_core::records::{
|
|
|
|
|
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
2026-07-14 22:08:29 +09:00
|
|
|
};
|
|
|
|
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
2026-07-15 19:48:42 +09:00
|
|
|
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat, fstatfs};
|
2026-07-14 22:08:29 +09:00
|
|
|
use rustix::io::{FdFlags, fcntl_setfd};
|
|
|
|
|
use rustix::pipe::{PipeFlags, pipe_with};
|
|
|
|
|
use rustix::process::{Pid, PidfdFlags, Signal, WaitId, WaitIdOptions};
|
|
|
|
|
|
|
|
|
|
pub const PID_FS_MAGIC: i64 = 0x5049_4446;
|
|
|
|
|
const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!";
|
2026-07-14 23:50:30 +09:00
|
|
|
const PRIVATE_ROLE_ARGUMENT: &str = "--lazaret-private-process-role";
|
|
|
|
|
const WRAPPER_ROLE: &str = "launch-wrapper";
|
|
|
|
|
const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe";
|
2026-07-14 22:08:29 +09:00
|
|
|
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
#[derive(Debug)]
|
2026-07-14 22:08:29 +09:00
|
|
|
pub struct TargetCommand {
|
|
|
|
|
program: OsString,
|
|
|
|
|
arguments: Vec<OsString>,
|
2026-07-15 19:25:50 +09:00
|
|
|
parent_edge: ParentEdge,
|
2026-07-15 19:48:42 +09:00
|
|
|
executable: Option<OwnedFd>,
|
2026-07-17 05:33:22 +09:00
|
|
|
stdin: Option<File>,
|
2026-07-15 19:09:24 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 17:06:55 +09:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
enum TargetStdinAccess {
|
|
|
|
|
ReadOnly,
|
|
|
|
|
WriteOnly,
|
|
|
|
|
ReadWrite,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TargetStdinAccess {
|
|
|
|
|
fn from_flags(flags: OFlags) -> io::Result<Self> {
|
|
|
|
|
if flags.contains(OFlags::PATH) {
|
|
|
|
|
return Err(io::Error::other(
|
|
|
|
|
"target stdin descriptor has no read or write authority",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
match flags & OFlags::ACCMODE {
|
|
|
|
|
flags if flags == OFlags::RDONLY => Ok(Self::ReadOnly),
|
|
|
|
|
flags if flags == OFlags::WRONLY => Ok(Self::WriteOnly),
|
|
|
|
|
flags if flags == OFlags::RDWR => Ok(Self::ReadWrite),
|
|
|
|
|
_ => Err(io::Error::other(
|
|
|
|
|
"target stdin descriptor has an unsupported access mode",
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn from_descriptor(descriptor: i32) -> io::Result<Self> {
|
|
|
|
|
let metadata = std::fs::read_to_string(format!("/proc/self/fdinfo/{descriptor}"))?;
|
|
|
|
|
let flags = metadata
|
|
|
|
|
.lines()
|
|
|
|
|
.find_map(|line| line.strip_prefix("flags:"))
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.ok_or_else(|| io::Error::other("target stdin descriptor flags are missing"))
|
|
|
|
|
.and_then(|flags| {
|
|
|
|
|
u32::from_str_radix(flags, 8)
|
|
|
|
|
.map(OFlags::from_bits_retain)
|
|
|
|
|
.map_err(|_| io::Error::other("target stdin descriptor flags are invalid"))
|
|
|
|
|
})?;
|
|
|
|
|
Self::from_flags(flags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reopen(self, descriptor: i32) -> io::Result<File> {
|
|
|
|
|
let mut options = File::options();
|
|
|
|
|
match self {
|
|
|
|
|
Self::ReadOnly => {
|
|
|
|
|
options.read(true);
|
|
|
|
|
}
|
|
|
|
|
Self::WriteOnly => {
|
|
|
|
|
options.write(true);
|
|
|
|
|
}
|
|
|
|
|
Self::ReadWrite => {
|
|
|
|
|
options.read(true).write(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
options.open(format!("/proc/self/fd/{descriptor}"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:09:24 +09:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
2026-07-15 19:25:50 +09:00
|
|
|
pub enum ParentEdge {
|
|
|
|
|
ParentDeathKill,
|
2026-07-15 19:09:24 +09:00
|
|
|
LifetimePipe,
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TargetCommand {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn new(program: impl Into<OsString>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
program: program.into(),
|
|
|
|
|
arguments: Vec::new(),
|
2026-07-15 19:25:50 +09:00
|
|
|
parent_edge: ParentEdge::ParentDeathKill,
|
2026-07-15 19:48:42 +09:00
|
|
|
executable: None,
|
2026-07-17 05:33:22 +09:00
|
|
|
stdin: None,
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
/// Constructs an argv-only target bound to the exact inherited executable descriptor.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the descriptor cannot be made inheritable by the launch wrapper.
|
|
|
|
|
pub fn from_executable(
|
|
|
|
|
executable: OwnedFd,
|
|
|
|
|
arguments: impl IntoIterator<Item = OsString>,
|
|
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
fcntl_setfd(executable.as_fd(), FdFlags::empty())?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
program: OsString::from(format!("/proc/self/fd/{}", executable.as_raw_fd())),
|
|
|
|
|
arguments: arguments.into_iter().collect(),
|
|
|
|
|
parent_edge: ParentEdge::ParentDeathKill,
|
|
|
|
|
executable: Some(executable),
|
2026-07-17 05:33:22 +09:00
|
|
|
stdin: None,
|
2026-07-15 19:48:42 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn argument(mut self, argument: impl Into<OsString>) -> Self {
|
|
|
|
|
self.arguments.push(argument.into());
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:09:24 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn with_lifetime_pipe_parent_edge(mut self) -> Self {
|
2026-07-15 19:25:50 +09:00
|
|
|
self.parent_edge = ParentEdge::LifetimePipe;
|
2026-07-15 19:09:24 +09:00
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:17:30 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn with_parent_death_kill_edge(mut self) -> Self {
|
2026-07-15 19:25:50 +09:00
|
|
|
self.parent_edge = ParentEdge::ParentDeathKill;
|
2026-07-15 19:17:30 +09:00
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:33:22 +09:00
|
|
|
/// Attaches the target's standard input after the private wrapper consumes its barrier.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the descriptor cannot be inherited by the wrapper.
|
|
|
|
|
pub fn with_target_stdin(mut self, stdin: File) -> io::Result<Self> {
|
2026-07-17 17:06:55 +09:00
|
|
|
TargetStdinAccess::from_flags(fcntl_getfl(&stdin)?)?;
|
2026-07-17 05:33:22 +09:00
|
|
|
fcntl_setfd(stdin.as_fd(), FdFlags::empty())?;
|
|
|
|
|
self.stdin = Some(stdin);
|
|
|
|
|
Ok(self)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:25:50 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub const fn parent_edge(&self) -> ParentEdge {
|
|
|
|
|
self.parent_edge
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:28:18 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn program(&self) -> &OsStr {
|
|
|
|
|
&self.program
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn arguments(&self) -> &[OsString] {
|
|
|
|
|
&self.arguments
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[allow(clippy::disallowed_types)]
|
|
|
|
|
fn append_to(&self, command: &mut std::process::Command) {
|
2026-07-15 19:48:42 +09:00
|
|
|
let _ = &self.executable;
|
2026-07-15 19:09:24 +09:00
|
|
|
command
|
|
|
|
|
.arg("--parent-action")
|
2026-07-15 19:25:50 +09:00
|
|
|
.arg(match self.parent_edge {
|
|
|
|
|
ParentEdge::ParentDeathKill => "kill",
|
|
|
|
|
ParentEdge::LifetimePipe => "lifetime-pipe",
|
2026-07-15 19:09:24 +09:00
|
|
|
})
|
2026-07-17 05:33:22 +09:00
|
|
|
.args(
|
|
|
|
|
self.stdin
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|stdin| {
|
|
|
|
|
[
|
|
|
|
|
OsString::from("--target-stdin-fd"),
|
|
|
|
|
OsString::from(stdin.as_raw_fd().to_string()),
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flatten(),
|
|
|
|
|
)
|
2026-07-15 19:09:24 +09:00
|
|
|
.arg("--")
|
|
|
|
|
.arg(&self.program)
|
|
|
|
|
.args(&self.arguments);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ChildStandardIo {
|
|
|
|
|
stdout: Option<Stdio>,
|
|
|
|
|
stderr: Option<Stdio>,
|
2026-07-15 19:25:50 +09:00
|
|
|
policy: ChildStandardIoPolicy,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum ChildStandardIoPolicy {
|
|
|
|
|
Inherit,
|
|
|
|
|
PrivateControlWithLifetimePipe,
|
|
|
|
|
Silenced,
|
2026-07-17 05:33:22 +09:00
|
|
|
Terminal,
|
2026-07-15 19:09:24 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChildStandardIo {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn inherit() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
stdout: None,
|
|
|
|
|
stderr: None,
|
2026-07-15 19:25:50 +09:00
|
|
|
policy: ChildStandardIoPolicy::Inherit,
|
2026-07-15 19:09:24 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
2026-07-15 19:25:50 +09:00
|
|
|
pub fn private_control_with_lifetime_pipe(
|
|
|
|
|
control: impl Into<Stdio>,
|
|
|
|
|
lifetime_pipe: impl Into<Stdio>,
|
|
|
|
|
) -> Self {
|
2026-07-15 19:09:24 +09:00
|
|
|
Self {
|
2026-07-15 19:25:50 +09:00
|
|
|
stdout: Some(control.into()),
|
|
|
|
|
stderr: Some(lifetime_pipe.into()),
|
|
|
|
|
policy: ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
2026-07-15 19:09:24 +09:00
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
2026-07-15 19:25:50 +09:00
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn silenced() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
stdout: Some(Stdio::null()),
|
|
|
|
|
stderr: Some(Stdio::null()),
|
|
|
|
|
policy: ChildStandardIoPolicy::Silenced,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:33:22 +09:00
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the output descriptor cannot be duplicated for merged terminal I/O.
|
|
|
|
|
pub fn terminal(output: File) -> io::Result<Self> {
|
|
|
|
|
let error = output.try_clone()?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
stdout: Some(Stdio::from(output)),
|
|
|
|
|
stderr: Some(Stdio::from(error)),
|
|
|
|
|
policy: ChildStandardIoPolicy::Terminal,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:25:50 +09:00
|
|
|
#[must_use]
|
|
|
|
|
pub const fn policy(&self) -> ChildStandardIoPolicy {
|
|
|
|
|
self.policy
|
|
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-14 23:50:30 +09:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct ProcessOwnershipContext {
|
|
|
|
|
pub domain_id: DomainId,
|
|
|
|
|
pub generation_id: GenerationId,
|
|
|
|
|
pub coordinator_id: CoordinatorId,
|
|
|
|
|
pub slot: HostProcessOwnershipSlot,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 15:20:13 +09:00
|
|
|
pub struct AcknowledgedChildAcquisition<'target> {
|
|
|
|
|
pub expected_parent: PidfsProcessIdentityV1,
|
|
|
|
|
pub ownership: ProcessOwnershipContext,
|
|
|
|
|
pub target: &'target TargetCommand,
|
|
|
|
|
pub standard_io: ChildStandardIo,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct RetainedProcess {
|
|
|
|
|
pidfd: OwnedFd,
|
|
|
|
|
identity: PidfsProcessIdentityV1,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:38:43 +09:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum PidfsRecoveryMatch {
|
|
|
|
|
BootMismatch,
|
|
|
|
|
Matched(RetainedProcess),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum RecoveryActionResult<T> {
|
|
|
|
|
BootReconciled(T),
|
|
|
|
|
ProcessAction(T),
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct CapturedProcess {
|
|
|
|
|
process: RetainedProcess,
|
|
|
|
|
stdout: File,
|
|
|
|
|
stderr: File,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:28:18 +09:00
|
|
|
pub struct BootstrapChild {
|
|
|
|
|
process: RetainedProcess,
|
|
|
|
|
status: File,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BootstrapChild {
|
|
|
|
|
/// Reads the private bootstrap acquisition status byte.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the child closes or corrupts its status channel.
|
|
|
|
|
pub fn acquisition_status(&mut self) -> io::Result<u8> {
|
|
|
|
|
let mut status = [0_u8; 1];
|
|
|
|
|
self.status.read_exact(&mut status)?;
|
|
|
|
|
Ok(status[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when polling the exact child pidfd fails.
|
|
|
|
|
pub fn has_exited(&self) -> io::Result<bool> {
|
|
|
|
|
self.process.has_exited()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reaps the exact retained child through its pidfd.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when serialized exact reaping fails.
|
|
|
|
|
pub fn reap(self) -> io::Result<ProcessExit> {
|
|
|
|
|
child_broker().reap_status(self.process)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
impl CapturedProcess {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn process(&self) -> &RetainedProcess {
|
|
|
|
|
&self.process
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn streams(&mut self) -> (&mut File, &mut File) {
|
|
|
|
|
(&mut self.stdout, &mut self.stderr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn into_process(self) -> RetainedProcess {
|
|
|
|
|
self.process
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct ProcessExit {
|
|
|
|
|
pub exit_code: Option<i32>,
|
|
|
|
|
pub terminating_signal: Option<i32>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
impl RetainedProcess {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn identity(&self) -> PidfsProcessIdentityV1 {
|
|
|
|
|
self.identity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the exact retained-pidfd action fails.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn send_signal(&self, signal: Signal) -> io::Result<()> {
|
|
|
|
|
rustix::process::pidfd_send_signal(self.pidfd.as_fd(), signal).map_err(Into::into)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when polling the retained pidfd fails.
|
|
|
|
|
pub fn has_exited(&self) -> io::Result<bool> {
|
|
|
|
|
let mut descriptors = [PollFd::new(&self.pidfd, PollFlags::IN)];
|
|
|
|
|
poll(&mut descriptors, Some(&Timespec::default()))?;
|
|
|
|
|
Ok(descriptors[0]
|
|
|
|
|
.revents()
|
|
|
|
|
.intersects(PollFlags::IN | PollFlags::HUP))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:09:24 +09:00
|
|
|
impl AsFd for RetainedProcess {
|
|
|
|
|
fn as_fd(&self) -> BorrowedFd<'_> {
|
|
|
|
|
self.pidfd.as_fd()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct ChildBroker {
|
|
|
|
|
exclusion: Mutex<()>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn child_broker() -> &'static ChildBroker {
|
|
|
|
|
CHILD_BROKER.get_or_init(|| ChildBroker {
|
|
|
|
|
exclusion: Mutex::new(()),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:25:50 +09:00
|
|
|
fn require_parent_edge_standard_io(
|
|
|
|
|
parent_edge: ParentEdge,
|
|
|
|
|
standard_io: ChildStandardIoPolicy,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
match (parent_edge, standard_io) {
|
|
|
|
|
(ParentEdge::LifetimePipe, ChildStandardIoPolicy::PrivateControlWithLifetimePipe)
|
|
|
|
|
| (
|
|
|
|
|
ParentEdge::ParentDeathKill,
|
2026-07-17 05:33:22 +09:00
|
|
|
ChildStandardIoPolicy::Inherit
|
|
|
|
|
| ChildStandardIoPolicy::Silenced
|
|
|
|
|
| ChildStandardIoPolicy::Terminal,
|
2026-07-15 19:25:50 +09:00
|
|
|
) => Ok(()),
|
|
|
|
|
(ParentEdge::LifetimePipe, _) => Err(io::Error::other(
|
|
|
|
|
"lifetime-pipe parent edge requires private control stdout and lifetime-pipe stderr",
|
|
|
|
|
)),
|
|
|
|
|
(ParentEdge::ParentDeathKill, ChildStandardIoPolicy::PrivateControlWithLifetimePipe) => {
|
|
|
|
|
Err(io::Error::other(
|
|
|
|
|
"parent-death edge cannot substitute a lifetime-pipe stdio policy",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
fn require_captured_parent_edge(parent_edge: ParentEdge) -> io::Result<()> {
|
|
|
|
|
if parent_edge == ParentEdge::ParentDeathKill {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(io::Error::other(
|
|
|
|
|
"captured output requires the parent-death edge",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
impl ChildBroker {
|
2026-07-16 16:28:18 +09:00
|
|
|
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses a command without the lifetime-pipe edge or any spawn, pidfd, or status failure.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn spawn_bootstrap(
|
|
|
|
|
&self,
|
|
|
|
|
target: &TargetCommand,
|
|
|
|
|
lifetime_reader: File,
|
|
|
|
|
) -> io::Result<BootstrapChild> {
|
|
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
|
|
|
|
if target.parent_edge() != ParentEdge::LifetimePipe {
|
|
|
|
|
return Err(io::Error::other(
|
|
|
|
|
"bootstrap child requires the lifetime-pipe edge",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
require_default_sigchld()?;
|
|
|
|
|
require_reapable_sigchld()?;
|
|
|
|
|
let mut command = std::process::Command::new(&target.program);
|
|
|
|
|
command
|
|
|
|
|
.args(&target.arguments)
|
|
|
|
|
.stdin(Stdio::from(lifetime_reader))
|
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.stderr(Stdio::inherit());
|
|
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let status = child
|
|
|
|
|
.stdout
|
|
|
|
|
.take()
|
|
|
|
|
.ok_or_else(|| io::Error::other("bootstrap status pipe is absent"))?;
|
|
|
|
|
let process = match acquire_spawned_child(child.id()) {
|
|
|
|
|
Ok(process) => process,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
let _ = child.kill();
|
|
|
|
|
let _ = child.wait();
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
drop(child);
|
|
|
|
|
Ok(BootstrapChild {
|
|
|
|
|
process,
|
|
|
|
|
status: File::from(OwnedFd::from(status)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 23:26:37 +09:00
|
|
|
/// Runs a non-durable direct child under the same spawn/reap exclusion as owned children.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when causal launch, exact waiting, or the command exit status fails.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn run_transient(&self, target: &TargetCommand) -> io::Result<ProcessExit> {
|
2026-07-17 05:38:43 +09:00
|
|
|
self.run_transient_with_stdout(target, None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs a non-durable direct child with one private inherited stdout descriptor.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when causal launch, exact waiting, or the command exit status fails.
|
|
|
|
|
pub fn run_transient_with_private_stdout(
|
|
|
|
|
&self,
|
|
|
|
|
target: &TargetCommand,
|
|
|
|
|
stdout: OwnedFd,
|
|
|
|
|
) -> io::Result<ProcessExit> {
|
|
|
|
|
self.run_transient_with_stdout(target, Some(stdout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
fn run_transient_with_stdout(
|
|
|
|
|
&self,
|
|
|
|
|
target: &TargetCommand,
|
|
|
|
|
stdout: Option<OwnedFd>,
|
|
|
|
|
) -> io::Result<ProcessExit> {
|
2026-07-15 23:26:37 +09:00
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
|
|
|
|
require_parent_edge_standard_io(target.parent_edge(), ChildStandardIoPolicy::Silenced)?;
|
|
|
|
|
require_default_sigchld()?;
|
|
|
|
|
require_reapable_sigchld()?;
|
|
|
|
|
let parent = current_process_identity()?;
|
|
|
|
|
let (release, wait) = launch_barrier()?;
|
|
|
|
|
let mut command = private_role_command(WRAPPER_ROLE);
|
|
|
|
|
append_parent_identity(&mut command, parent);
|
|
|
|
|
target.append_to(&mut command);
|
|
|
|
|
command
|
|
|
|
|
.stdin(wait.into_stdio())
|
2026-07-17 05:38:43 +09:00
|
|
|
.stdout(stdout.map_or_else(Stdio::null, Stdio::from))
|
2026-07-15 23:26:37 +09:00
|
|
|
.stderr(Stdio::null());
|
|
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let process = match acquire_spawned_child(child.id()) {
|
|
|
|
|
Ok(process) => process,
|
|
|
|
|
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
|
|
|
|
};
|
|
|
|
|
if let Err(error) = release.release() {
|
|
|
|
|
return fail_after_pidfd_closed(process, error);
|
|
|
|
|
}
|
|
|
|
|
drop(child);
|
|
|
|
|
let status =
|
|
|
|
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
|
|
|
|
|
.ok_or_else(|| io::Error::other("transient child exited without wait status"))?;
|
|
|
|
|
let exit = ProcessExit {
|
|
|
|
|
exit_code: status.exit_status(),
|
|
|
|
|
terminating_signal: status.terminating_signal(),
|
|
|
|
|
};
|
|
|
|
|
drop(process);
|
|
|
|
|
if exit.exit_code == Some(0) && exit.terminating_signal.is_none() {
|
|
|
|
|
Ok(exit)
|
|
|
|
|
} else {
|
|
|
|
|
Err(io::Error::other(format!(
|
|
|
|
|
"transient child failed with exit {:?} and signal {:?}",
|
|
|
|
|
exit.exit_code, exit.terminating_signal
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:39:03 +09:00
|
|
|
/// Starts a non-durable direct child with split captured output and exact retained authority.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses invalid parent-edge or SIGCHLD state and cleans up on spawn, pidfd, barrier, or
|
|
|
|
|
/// pipe-acquisition failure.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn run_transient_captured(&self, target: &TargetCommand) -> io::Result<CapturedProcess> {
|
|
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
|
|
|
|
require_captured_parent_edge(target.parent_edge())?;
|
|
|
|
|
require_default_sigchld()?;
|
|
|
|
|
require_reapable_sigchld()?;
|
|
|
|
|
let parent = current_process_identity()?;
|
|
|
|
|
let (release, wait) = launch_barrier()?;
|
|
|
|
|
let mut command = private_role_command(WRAPPER_ROLE);
|
|
|
|
|
append_parent_identity(&mut command, parent);
|
|
|
|
|
target.append_to(&mut command);
|
|
|
|
|
command
|
|
|
|
|
.stdin(wait.into_stdio())
|
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.stderr(Stdio::piped());
|
|
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let process = match acquire_spawned_child(child.id()) {
|
|
|
|
|
Ok(process) => process,
|
|
|
|
|
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
|
|
|
|
};
|
|
|
|
|
if let Err(error) = release.release() {
|
|
|
|
|
return fail_after_pidfd_closed(process, error);
|
|
|
|
|
}
|
|
|
|
|
captured_process(process, child)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
/// Spawns the repository wrapper, stages exact ownership, accepts its acknowledgment,
|
|
|
|
|
/// and only then releases target execution.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses and reaps the blocked wrapper on every failed acquisition or callback.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
2026-07-14 23:50:30 +09:00
|
|
|
pub fn acquire<Stage, Acknowledge>(
|
2026-07-14 22:08:29 +09:00
|
|
|
&self,
|
|
|
|
|
expected_parent: PidfsProcessIdentityV1,
|
2026-07-14 23:50:30 +09:00
|
|
|
ownership: ProcessOwnershipContext,
|
2026-07-14 22:08:29 +09:00
|
|
|
target: &TargetCommand,
|
|
|
|
|
stage: Stage,
|
|
|
|
|
acknowledge: Acknowledge,
|
|
|
|
|
) -> io::Result<RetainedProcess>
|
2026-07-15 19:09:24 +09:00
|
|
|
where
|
|
|
|
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
|
|
|
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
|
|
|
|
{
|
|
|
|
|
self.acquire_with_stdio(
|
|
|
|
|
expected_parent,
|
|
|
|
|
ownership,
|
|
|
|
|
target,
|
|
|
|
|
ChildStandardIo::inherit(),
|
|
|
|
|
stage,
|
|
|
|
|
acknowledge,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses and reaps the blocked wrapper on every failed acquisition or callback.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn acquire_with_stdio<Stage, Acknowledge>(
|
|
|
|
|
&self,
|
|
|
|
|
expected_parent: PidfsProcessIdentityV1,
|
|
|
|
|
ownership: ProcessOwnershipContext,
|
|
|
|
|
target: &TargetCommand,
|
2026-07-15 23:26:37 +09:00
|
|
|
standard_io: ChildStandardIo,
|
2026-07-15 19:09:24 +09:00
|
|
|
stage: Stage,
|
|
|
|
|
acknowledge: Acknowledge,
|
|
|
|
|
) -> io::Result<RetainedProcess>
|
2026-07-14 22:08:29 +09:00
|
|
|
where
|
2026-07-14 23:50:30 +09:00
|
|
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
|
|
|
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
2026-07-15 23:26:37 +09:00
|
|
|
{
|
|
|
|
|
self.acquire_with_stdio_after_ack(
|
2026-07-16 15:20:13 +09:00
|
|
|
AcknowledgedChildAcquisition {
|
|
|
|
|
expected_parent,
|
|
|
|
|
ownership,
|
|
|
|
|
target,
|
|
|
|
|
standard_io,
|
|
|
|
|
},
|
2026-07-15 23:26:37 +09:00
|
|
|
stage,
|
|
|
|
|
acknowledge,
|
|
|
|
|
|| Ok(()),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Acquires an exact child while retaining its blocked wrapper across one acknowledged
|
|
|
|
|
/// parent transition.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses and reaps the blocked wrapper when staging, acknowledgment, transition, or release
|
|
|
|
|
/// fails.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn acquire_with_stdio_after_ack<Stage, Acknowledge, Transition>(
|
|
|
|
|
&self,
|
2026-07-16 15:20:13 +09:00
|
|
|
acquisition: AcknowledgedChildAcquisition<'_>,
|
2026-07-15 23:26:37 +09:00
|
|
|
stage: Stage,
|
|
|
|
|
acknowledge: Acknowledge,
|
|
|
|
|
transition: Transition,
|
|
|
|
|
) -> io::Result<RetainedProcess>
|
|
|
|
|
where
|
|
|
|
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
|
|
|
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
|
|
|
|
Transition: FnOnce() -> io::Result<()>,
|
2026-07-14 22:08:29 +09:00
|
|
|
{
|
|
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
2026-07-16 15:20:13 +09:00
|
|
|
require_parent_edge_standard_io(
|
|
|
|
|
acquisition.target.parent_edge(),
|
|
|
|
|
acquisition.standard_io.policy(),
|
|
|
|
|
)?;
|
2026-07-14 22:08:29 +09:00
|
|
|
require_default_sigchld()?;
|
2026-07-14 23:50:30 +09:00
|
|
|
require_reapable_sigchld()?;
|
2026-07-14 22:08:29 +09:00
|
|
|
let (release, wait) = launch_barrier()?;
|
2026-07-14 23:50:30 +09:00
|
|
|
let mut command = private_role_command(WRAPPER_ROLE);
|
2026-07-16 15:20:13 +09:00
|
|
|
append_parent_identity(&mut command, acquisition.expected_parent);
|
|
|
|
|
acquisition.target.append_to(&mut command);
|
2026-07-14 22:08:29 +09:00
|
|
|
command.stdin(wait.into_stdio());
|
2026-07-16 15:20:13 +09:00
|
|
|
let mut standard_io = acquisition.standard_io;
|
2026-07-15 19:09:24 +09:00
|
|
|
if let Some(stdout) = standard_io.stdout.take() {
|
|
|
|
|
command.stdout(stdout);
|
|
|
|
|
}
|
|
|
|
|
if let Some(stderr) = standard_io.stderr.take() {
|
|
|
|
|
command.stderr(stderr);
|
|
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let host_pid = child.id();
|
|
|
|
|
let acquired = acquire_spawned_child(host_pid);
|
|
|
|
|
let process = match acquired {
|
|
|
|
|
Ok(process) => process,
|
|
|
|
|
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
|
|
|
|
};
|
2026-07-14 23:50:30 +09:00
|
|
|
let staged = StagedProcessOwnership::v1(
|
2026-07-16 15:20:13 +09:00
|
|
|
acquisition.ownership.domain_id,
|
|
|
|
|
acquisition.ownership.generation_id,
|
|
|
|
|
acquisition.ownership.coordinator_id,
|
|
|
|
|
acquisition.ownership.slot,
|
2026-07-14 23:50:30 +09:00
|
|
|
process.identity,
|
|
|
|
|
);
|
|
|
|
|
if let Err(error) = stage(staged) {
|
|
|
|
|
return fail_after_pidfd(release, process, error);
|
|
|
|
|
}
|
|
|
|
|
let acknowledgment = match acknowledge(staged) {
|
|
|
|
|
Ok(acknowledgment) => acknowledgment,
|
2026-07-14 22:08:29 +09:00
|
|
|
Err(error) => return fail_after_pidfd(release, process, error),
|
|
|
|
|
};
|
2026-07-14 23:50:30 +09:00
|
|
|
if !acknowledgment.acknowledges(&staged) {
|
|
|
|
|
return fail_after_pidfd(
|
|
|
|
|
release,
|
|
|
|
|
process,
|
|
|
|
|
io::Error::other(
|
|
|
|
|
"process ownership acknowledgment does not match staged ownership",
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
2026-07-15 23:26:37 +09:00
|
|
|
if let Err(error) = transition() {
|
|
|
|
|
return fail_after_pidfd(release, process, error);
|
|
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
if let Err(error) = release.release() {
|
|
|
|
|
return fail_after_pidfd_closed(process, error);
|
|
|
|
|
}
|
|
|
|
|
drop(child);
|
|
|
|
|
Ok(process)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
/// Acquires an exact child with bounded-output pipe ownership retained by the caller.
|
|
|
|
|
///
|
2026-07-14 22:08:29 +09:00
|
|
|
/// # Errors
|
2026-07-15 19:48:42 +09:00
|
|
|
/// Refuses and reaps the blocked wrapper on every failed acquisition or callback.
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
pub fn acquire_captured<Stage, Acknowledge>(
|
|
|
|
|
&self,
|
|
|
|
|
expected_parent: PidfsProcessIdentityV1,
|
|
|
|
|
ownership: ProcessOwnershipContext,
|
|
|
|
|
target: &TargetCommand,
|
|
|
|
|
stage: Stage,
|
|
|
|
|
acknowledge: Acknowledge,
|
|
|
|
|
) -> io::Result<CapturedProcess>
|
|
|
|
|
where
|
|
|
|
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
|
|
|
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
|
|
|
|
{
|
2026-07-14 22:08:29 +09:00
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
2026-07-15 19:48:42 +09:00
|
|
|
require_captured_parent_edge(target.parent_edge())?;
|
|
|
|
|
require_default_sigchld()?;
|
|
|
|
|
require_reapable_sigchld()?;
|
|
|
|
|
let (release, wait) = launch_barrier()?;
|
|
|
|
|
let mut command = private_role_command(WRAPPER_ROLE);
|
|
|
|
|
append_parent_identity(&mut command, expected_parent);
|
|
|
|
|
target.append_to(&mut command);
|
|
|
|
|
command
|
|
|
|
|
.stdin(wait.into_stdio())
|
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.stderr(Stdio::piped());
|
|
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let host_pid = child.id();
|
|
|
|
|
let process = match acquire_spawned_child(host_pid) {
|
|
|
|
|
Ok(process) => process,
|
|
|
|
|
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
|
|
|
|
};
|
|
|
|
|
let staged = StagedProcessOwnership::v1(
|
|
|
|
|
ownership.domain_id,
|
|
|
|
|
ownership.generation_id,
|
|
|
|
|
ownership.coordinator_id,
|
|
|
|
|
ownership.slot,
|
|
|
|
|
process.identity,
|
|
|
|
|
);
|
|
|
|
|
if let Err(error) = stage(staged) {
|
|
|
|
|
return fail_after_pidfd(release, process, error);
|
|
|
|
|
}
|
|
|
|
|
let acknowledgment = match acknowledge(staged) {
|
|
|
|
|
Ok(acknowledgment) => acknowledgment,
|
|
|
|
|
Err(error) => return fail_after_pidfd(release, process, error),
|
|
|
|
|
};
|
|
|
|
|
if !acknowledgment.acknowledges(&staged) {
|
|
|
|
|
return fail_after_pidfd(
|
|
|
|
|
release,
|
|
|
|
|
process,
|
|
|
|
|
io::Error::other(
|
|
|
|
|
"process ownership acknowledgment does not match staged ownership",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if let Err(error) = release.release() {
|
|
|
|
|
return fail_after_pidfd_closed(process, error);
|
|
|
|
|
}
|
2026-07-16 18:39:03 +09:00
|
|
|
captured_process(process, child)
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
2026-07-15 19:48:42 +09:00
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when serialization or exact pidfd reaping fails.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn reap(&self, process: RetainedProcess) -> io::Result<()> {
|
|
|
|
|
self.reap_status(process).map(|_| ())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when serialization or exact pidfd reaping fails.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn reap_status(&self, process: RetainedProcess) -> io::Result<ProcessExit> {
|
|
|
|
|
let _guard = self
|
|
|
|
|
.exclusion
|
|
|
|
|
.lock()
|
|
|
|
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
|
|
|
|
let status =
|
|
|
|
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
|
|
|
|
|
.ok_or_else(|| io::Error::other("exact child exited without wait status"))?;
|
|
|
|
|
let exit = ProcessExit {
|
|
|
|
|
exit_code: status.exit_status(),
|
|
|
|
|
terminating_signal: status.terminating_signal(),
|
|
|
|
|
};
|
|
|
|
|
drop(process);
|
|
|
|
|
Ok(exit)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 18:39:03 +09:00
|
|
|
#[allow(clippy::disallowed_types)]
|
|
|
|
|
fn captured_process(
|
|
|
|
|
process: RetainedProcess,
|
|
|
|
|
mut child: std::process::Child,
|
|
|
|
|
) -> io::Result<CapturedProcess> {
|
|
|
|
|
let Some(stdout) = child.stdout.take() else {
|
|
|
|
|
return fail_running_captured(
|
|
|
|
|
process,
|
|
|
|
|
child,
|
|
|
|
|
io::Error::other("captured child stdout is unavailable"),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
let Some(stderr) = child.stderr.take() else {
|
|
|
|
|
return fail_running_captured(
|
|
|
|
|
process,
|
|
|
|
|
child,
|
|
|
|
|
io::Error::other("captured child stderr is unavailable"),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
let stdout = match child_pipe_file(stdout.into()) {
|
|
|
|
|
Ok(stdout) => stdout,
|
|
|
|
|
Err(error) => return fail_running_captured(process, child, error),
|
|
|
|
|
};
|
|
|
|
|
let stderr = match child_pipe_file(stderr.into()) {
|
|
|
|
|
Ok(stderr) => stderr,
|
|
|
|
|
Err(error) => return fail_running_captured(process, child, error),
|
|
|
|
|
};
|
|
|
|
|
drop(child);
|
|
|
|
|
Ok(CapturedProcess {
|
|
|
|
|
process,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
fn child_pipe_file(fd: OwnedFd) -> io::Result<File> {
|
|
|
|
|
let flags = fcntl_getfl(fd.as_fd())?;
|
|
|
|
|
fcntl_setfl(fd.as_fd(), flags | OFlags::NONBLOCK)?;
|
|
|
|
|
Ok(File::from(fd))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
fn fail_running_captured<T>(
|
|
|
|
|
process: RetainedProcess,
|
|
|
|
|
child: std::process::Child,
|
|
|
|
|
error: io::Error,
|
|
|
|
|
) -> io::Result<T> {
|
|
|
|
|
let _ = process.send_signal(Signal::KILL);
|
|
|
|
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
|
|
|
|
|
drop(child);
|
|
|
|
|
drop(process);
|
|
|
|
|
Err(error)
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-14 23:50:30 +09:00
|
|
|
#[allow(clippy::disallowed_types)]
|
|
|
|
|
fn private_role_command(role: &str) -> std::process::Command {
|
|
|
|
|
let mut command = std::process::Command::new("/proc/self/exe");
|
|
|
|
|
command.arg(PRIVATE_ROLE_ARGUMENT).arg(role);
|
|
|
|
|
command
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs a private self-exec role before normal argument handling.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns the role's fail-closed error when a private invocation is malformed.
|
|
|
|
|
pub fn dispatch_private_process_role(arguments: &[OsString]) -> Option<io::Result<()>> {
|
|
|
|
|
if arguments
|
|
|
|
|
.first()
|
|
|
|
|
.is_none_or(|argument| argument != PRIVATE_ROLE_ARGUMENT)
|
|
|
|
|
{
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let role = arguments.get(1).and_then(|argument| argument.to_str());
|
|
|
|
|
match role {
|
|
|
|
|
Some(SIGCHLD_PROBE_ROLE) if arguments.len() == 2 => {
|
|
|
|
|
Some(BarrierWait::from_stdin().and_then(BarrierWait::wait))
|
|
|
|
|
}
|
|
|
|
|
Some(WRAPPER_ROLE) => Some(run_wrapper_role(arguments[2..].to_vec())),
|
|
|
|
|
_ => Some(Err(io::Error::other(
|
|
|
|
|
"unknown or malformed private process role",
|
|
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
|
|
|
|
|
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
|
|
|
|
BarrierWait::from_stdin()?.wait()?;
|
|
|
|
|
rustix::io::fcntl_setfd(std::io::stdin(), rustix::io::FdFlags::CLOEXEC)?;
|
2026-07-15 19:25:50 +09:00
|
|
|
match target.parent_edge {
|
|
|
|
|
ParentEdge::ParentDeathKill => {
|
2026-07-15 19:09:24 +09:00
|
|
|
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
|
|
|
|
|
}
|
2026-07-15 19:25:50 +09:00
|
|
|
ParentEdge::LifetimePipe => {
|
2026-07-15 19:09:24 +09:00
|
|
|
require_lifetime_pipe_and_recheck(expected_parent)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-17 05:33:22 +09:00
|
|
|
if let Some(stdin) = target.stdin.as_ref() {
|
|
|
|
|
rustix::stdio::dup2_stdin(stdin)?;
|
|
|
|
|
}
|
2026-07-14 23:50:30 +09:00
|
|
|
Err(exec_target(target))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:09:24 +09:00
|
|
|
fn require_lifetime_pipe_and_recheck(
|
|
|
|
|
expected_parent: PidfsProcessIdentityV1,
|
|
|
|
|
) -> io::Result<RetainedProcess> {
|
|
|
|
|
let stderr = std::io::stderr();
|
|
|
|
|
let mut descriptors = [PollFd::new(&stderr, PollFlags::IN)];
|
|
|
|
|
poll(&mut descriptors, Some(&Timespec::default()))?;
|
|
|
|
|
if descriptors[0]
|
|
|
|
|
.revents()
|
|
|
|
|
.intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR)
|
|
|
|
|
{
|
|
|
|
|
return Err(io::Error::other("parent lifetime pipe is not held"));
|
|
|
|
|
}
|
|
|
|
|
let parent = recover_process(expected_parent)?;
|
|
|
|
|
let observed_parent = rustix::process::getppid()
|
|
|
|
|
.map(Pid::as_raw_nonzero)
|
|
|
|
|
.map(i32::from)
|
|
|
|
|
.and_then(|pid| u32::try_from(pid).ok());
|
|
|
|
|
if observed_parent != Some(expected_parent.host_pid().get()) {
|
|
|
|
|
return Err(io::Error::other(
|
|
|
|
|
"parent changed while arming lifetime pipe",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(parent)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
fn acquire_spawned_child(host_pid: u32) -> io::Result<RetainedProcess> {
|
|
|
|
|
let raw_pid = i32::try_from(host_pid)
|
|
|
|
|
.map_err(|_| io::Error::other("spawn returned out-of-range child PID"))?;
|
|
|
|
|
let pid = Pid::from_raw(raw_pid)
|
|
|
|
|
.ok_or_else(|| io::Error::other("spawn returned invalid child PID"))?;
|
|
|
|
|
require_host_pid_namespace(host_pid)?;
|
|
|
|
|
let pidfd = rustix::process::pidfd_open(pid, PidfdFlags::empty())?;
|
|
|
|
|
verify_direct_child_pidfd(pidfd.as_fd())?;
|
|
|
|
|
let authority = authority_from_pidfd(pidfd.as_fd(), read_boot_id()?)?;
|
|
|
|
|
let start_time = read_start_time(host_pid)?;
|
|
|
|
|
Ok(RetainedProcess {
|
|
|
|
|
pidfd,
|
|
|
|
|
identity: PidfsProcessIdentityV1::new(
|
|
|
|
|
authority,
|
|
|
|
|
HostPidLocator::new(host_pid),
|
|
|
|
|
ProcessStartTimeTicks::new(start_time),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses descriptors that do not name a waitable direct child of this broker process.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn verify_direct_child_pidfd(pidfd: impl AsFd) -> io::Result<()> {
|
|
|
|
|
rustix::process::waitid(
|
|
|
|
|
WaitId::PidFd(pidfd.as_fd()),
|
|
|
|
|
WaitIdOptions::EXITED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT,
|
|
|
|
|
)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
2026-07-15 19:48:42 +09:00
|
|
|
fn fail_before_pidfd<T>(
|
2026-07-14 22:08:29 +09:00
|
|
|
release: BarrierRelease,
|
|
|
|
|
child: &mut std::process::Child,
|
|
|
|
|
error: io::Error,
|
2026-07-15 19:48:42 +09:00
|
|
|
) -> io::Result<T> {
|
2026-07-14 22:08:29 +09:00
|
|
|
drop(release);
|
|
|
|
|
child.wait()?;
|
|
|
|
|
Err(error)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:48:42 +09:00
|
|
|
fn fail_after_pidfd<T>(
|
2026-07-14 22:08:29 +09:00
|
|
|
release: BarrierRelease,
|
|
|
|
|
process: RetainedProcess,
|
|
|
|
|
error: io::Error,
|
2026-07-15 19:48:42 +09:00
|
|
|
) -> io::Result<T> {
|
2026-07-14 22:08:29 +09:00
|
|
|
drop(release);
|
|
|
|
|
fail_after_pidfd_closed(process, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
2026-07-15 19:48:42 +09:00
|
|
|
fn fail_after_pidfd_closed<T>(process: RetainedProcess, error: io::Error) -> io::Result<T> {
|
2026-07-14 22:08:29 +09:00
|
|
|
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
|
|
|
|
|
drop(process);
|
|
|
|
|
Err(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses before PID lookup on boot mismatch and before action on every later mismatch.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn recover_process(identity: PidfsProcessIdentityV1) -> io::Result<RetainedProcess> {
|
2026-07-17 05:38:43 +09:00
|
|
|
match match_recovery_process(identity)? {
|
|
|
|
|
PidfsRecoveryMatch::BootMismatch => Err(io::Error::other(
|
|
|
|
|
"stored process authority belongs to another boot",
|
|
|
|
|
)),
|
|
|
|
|
PidfsRecoveryMatch::Matched(process) => Ok(process),
|
|
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:38:43 +09:00
|
|
|
/// Reopens one stored process identity or reports boot mismatch before PID lookup.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses host-PID ambiguity, PID lookup, pidfs validation, identity, and negative-evidence
|
|
|
|
|
/// failures without returning an actionable descriptor.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn match_recovery_process(identity: PidfsProcessIdentityV1) -> io::Result<PidfsRecoveryMatch> {
|
|
|
|
|
match_recovery_process_with(
|
|
|
|
|
identity,
|
|
|
|
|
read_boot_id,
|
|
|
|
|
|host_pid| {
|
|
|
|
|
let raw_pid = i32::try_from(host_pid)
|
|
|
|
|
.map_err(|_| io::Error::other("stored PID locator is out of range"))?;
|
|
|
|
|
let pid = Pid::from_raw(raw_pid)
|
|
|
|
|
.ok_or_else(|| io::Error::other("stored PID locator is invalid"))?;
|
|
|
|
|
require_host_pid_namespace(host_pid)?;
|
|
|
|
|
rustix::process::pidfd_open(pid, PidfdFlags::empty()).map_err(Into::into)
|
|
|
|
|
},
|
|
|
|
|
|pidfd, boot_id| authority_from_pidfd(pidfd, boot_id),
|
|
|
|
|
read_start_time,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reconciles reboot-safe physical evidence or acts through the exactly reopened pidfd.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Propagates reconciliation, pidfs matching, or same-descriptor action failure. The process
|
|
|
|
|
/// action is never called on boot mismatch or an unequal process identity.
|
|
|
|
|
pub fn recover_process_action<T>(
|
|
|
|
|
identity: PidfsProcessIdentityV1,
|
|
|
|
|
reconcile_boot: impl FnOnce() -> io::Result<T>,
|
|
|
|
|
action: impl FnOnce(&RetainedProcess) -> io::Result<T>,
|
|
|
|
|
) -> io::Result<RecoveryActionResult<T>> {
|
|
|
|
|
match match_recovery_process(identity)? {
|
|
|
|
|
PidfsRecoveryMatch::BootMismatch => {
|
|
|
|
|
reconcile_boot().map(RecoveryActionResult::BootReconciled)
|
|
|
|
|
}
|
|
|
|
|
PidfsRecoveryMatch::Matched(process) => {
|
|
|
|
|
action(&process).map(RecoveryActionResult::ProcessAction)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn match_recovery_process_with<Boot, Open, Inspect, Start>(
|
2026-07-14 22:08:29 +09:00
|
|
|
identity: PidfsProcessIdentityV1,
|
|
|
|
|
boot: Boot,
|
|
|
|
|
open: Open,
|
2026-07-17 05:38:43 +09:00
|
|
|
inspect: Inspect,
|
|
|
|
|
start: Start,
|
|
|
|
|
) -> io::Result<PidfsRecoveryMatch>
|
2026-07-14 22:08:29 +09:00
|
|
|
where
|
|
|
|
|
Boot: FnOnce() -> io::Result<BootId>,
|
|
|
|
|
Open: FnOnce(u32) -> io::Result<OwnedFd>,
|
2026-07-17 05:38:43 +09:00
|
|
|
Inspect: FnOnce(BorrowedFd<'_>, BootId) -> io::Result<PidfsProcessAuthorityV1>,
|
|
|
|
|
Start: FnOnce(u32) -> io::Result<u64>,
|
2026-07-14 22:08:29 +09:00
|
|
|
{
|
|
|
|
|
let current_boot = boot()?;
|
|
|
|
|
if current_boot != identity.authority().boot_id {
|
2026-07-17 05:38:43 +09:00
|
|
|
return Ok(PidfsRecoveryMatch::BootMismatch);
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
let host_pid = identity.host_pid().get();
|
2026-07-18 05:47:15 +09:00
|
|
|
let pidfd = normalize_process_absence(open(host_pid))?;
|
2026-07-17 05:38:43 +09:00
|
|
|
let observed = inspect(pidfd.as_fd(), current_boot)?;
|
2026-07-14 22:08:29 +09:00
|
|
|
if observed != identity.authority() {
|
|
|
|
|
return Err(io::Error::other("pidfs process authority mismatch"));
|
|
|
|
|
}
|
2026-07-18 05:47:15 +09:00
|
|
|
if normalize_process_absence(start(host_pid))? != identity.start_time_ticks().get() {
|
2026-07-14 22:08:29 +09:00
|
|
|
return Err(io::Error::other("process start time mismatch"));
|
|
|
|
|
}
|
2026-07-17 05:38:43 +09:00
|
|
|
Ok(PidfsRecoveryMatch::Matched(RetainedProcess {
|
|
|
|
|
pidfd,
|
|
|
|
|
identity,
|
|
|
|
|
}))
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 05:47:15 +09:00
|
|
|
fn normalize_process_absence<T>(result: io::Result<T>) -> io::Result<T> {
|
|
|
|
|
result.map_err(|error| {
|
|
|
|
|
if error.raw_os_error() == Some(rustix::io::Errno::NOENT.raw_os_error()) {
|
|
|
|
|
io::Error::from_raw_os_error(rustix::io::Errno::SRCH.raw_os_error())
|
|
|
|
|
} else {
|
|
|
|
|
error
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses if arming fails or the exact parent changes during the recheck.
|
|
|
|
|
pub fn arm_parent_death_and_recheck(
|
|
|
|
|
signal: Signal,
|
|
|
|
|
expected_parent: PidfsProcessIdentityV1,
|
|
|
|
|
) -> io::Result<RetainedProcess> {
|
|
|
|
|
rustix::process::set_parent_process_death_signal(Some(signal))?;
|
|
|
|
|
let parent = recover_process(expected_parent)?;
|
|
|
|
|
let observed_parent = rustix::process::getppid()
|
|
|
|
|
.map(Pid::as_raw_nonzero)
|
|
|
|
|
.map(i32::from)
|
|
|
|
|
.and_then(|pid| u32::try_from(pid).ok());
|
|
|
|
|
if observed_parent != Some(expected_parent.host_pid().get()) {
|
|
|
|
|
return Err(io::Error::other(
|
|
|
|
|
"parent changed while arming parent-death action",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
Ok(parent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error if the current process cannot form a same-pidfd identity.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn current_process_identity() -> io::Result<PidfsProcessIdentityV1> {
|
|
|
|
|
let pid = rustix::process::getpid();
|
|
|
|
|
let host_pid = u32::try_from(i32::from(pid.as_raw_nonzero()))
|
|
|
|
|
.map_err(|_| io::Error::other("current PID is out of range"))?;
|
|
|
|
|
require_host_pid_namespace(host_pid)?;
|
|
|
|
|
let pidfd = rustix::process::pidfd_open(pid, PidfdFlags::empty())?;
|
|
|
|
|
let authority = authority_from_pidfd(pidfd.as_fd(), read_boot_id()?)?;
|
|
|
|
|
Ok(PidfsProcessIdentityV1::new(
|
|
|
|
|
authority,
|
|
|
|
|
HostPidLocator::new(host_pid),
|
|
|
|
|
ProcessStartTimeTicks::new(read_start_time(host_pid)?),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn authority_from_pidfd(pidfd: impl AsFd, boot_id: BootId) -> io::Result<PidfsProcessAuthorityV1> {
|
|
|
|
|
authority_from_pidfd_with(
|
|
|
|
|
pidfd,
|
|
|
|
|
boot_id,
|
|
|
|
|
|fd| Ok(fstatfs(fd)?.f_type as i64),
|
|
|
|
|
|fd| {
|
|
|
|
|
let stat = fstat(fd)?;
|
|
|
|
|
Ok((stat.st_dev, stat.st_ino))
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn authority_from_pidfd_with<Fs, Stat>(
|
|
|
|
|
pidfd: impl AsFd,
|
|
|
|
|
boot_id: BootId,
|
|
|
|
|
filesystem: Fs,
|
|
|
|
|
stat: Stat,
|
|
|
|
|
) -> io::Result<PidfsProcessAuthorityV1>
|
|
|
|
|
where
|
|
|
|
|
Fs: FnOnce(std::os::fd::BorrowedFd<'_>) -> io::Result<i64>,
|
|
|
|
|
Stat: FnOnce(std::os::fd::BorrowedFd<'_>) -> io::Result<(u64, u64)>,
|
|
|
|
|
{
|
|
|
|
|
require_pidfs_magic(filesystem(pidfd.as_fd())?)?;
|
|
|
|
|
let (device, inode) = stat(pidfd.as_fd())?;
|
|
|
|
|
Ok(PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id,
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(device),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(inode),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses every filesystem magic other than `PID_FS_MAGIC`.
|
|
|
|
|
pub fn require_pidfs_magic(magic: i64) -> io::Result<()> {
|
|
|
|
|
if magic == PID_FS_MAGIC {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(io::Error::other("descriptor is not a pidfs process handle"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses unreadable or non-canonical boot identifiers.
|
|
|
|
|
pub fn read_boot_id() -> io::Result<BootId> {
|
|
|
|
|
parse_boot_id(&std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses every non-canonical UUID spelling.
|
|
|
|
|
pub fn parse_boot_id(value: &str) -> io::Result<BootId> {
|
|
|
|
|
let value = value.strip_suffix('\n').unwrap_or(value);
|
|
|
|
|
if value.len() != 36
|
|
|
|
|
|| value.as_bytes()[8] != b'-'
|
|
|
|
|
|| value.as_bytes()[13] != b'-'
|
|
|
|
|
|| value.as_bytes()[18] != b'-'
|
|
|
|
|
|| value.as_bytes()[23] != b'-'
|
|
|
|
|
|| value
|
|
|
|
|
.bytes()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.any(|(index, byte)| !matches!(index, 8 | 13 | 18 | 23) && !byte.is_ascii_hexdigit())
|
|
|
|
|
{
|
|
|
|
|
return Err(io::Error::other("boot ID is not a canonical UUID"));
|
|
|
|
|
}
|
|
|
|
|
let compact = value.replace('-', "");
|
|
|
|
|
let mut bytes = [0_u8; 16];
|
|
|
|
|
for (index, byte) in bytes.iter_mut().enumerate() {
|
|
|
|
|
*byte = u8::from_str_radix(&compact[index * 2..index * 2 + 2], 16)
|
|
|
|
|
.map_err(|_| io::Error::other("boot ID is not a canonical UUID"))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(BootId::new(bytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses unreadable or malformed process statistics.
|
|
|
|
|
pub fn read_start_time(host_pid: u32) -> io::Result<u64> {
|
|
|
|
|
let stat = std::fs::read_to_string(format!("/proc/{host_pid}/stat"))?;
|
|
|
|
|
let command_end = stat
|
|
|
|
|
.rfind(") ")
|
|
|
|
|
.ok_or_else(|| io::Error::other("process stat has no command terminator"))?;
|
|
|
|
|
stat[command_end + 2..]
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.nth(19)
|
|
|
|
|
.and_then(|value| value.parse().ok())
|
|
|
|
|
.ok_or_else(|| io::Error::other("process stat has no valid start time"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_host_pid_namespace(host_pid: u32) -> io::Result<()> {
|
|
|
|
|
let status = std::fs::read_to_string(format!("/proc/{host_pid}/status"))?;
|
|
|
|
|
let nspid = status
|
|
|
|
|
.lines()
|
|
|
|
|
.find_map(|line| line.strip_prefix("NSpid:"))
|
|
|
|
|
.ok_or_else(|| io::Error::other("process status has no PID namespace mapping"))?;
|
|
|
|
|
if nspid.trim() == host_pid.to_string() {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(io::Error::other(
|
|
|
|
|
"PID locator is not unambiguous in the host PID namespace",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_default_sigchld() -> io::Result<()> {
|
|
|
|
|
let status = std::fs::read_to_string("/proc/self/status")?;
|
|
|
|
|
let ignored = signal_mask(&status, "SigIgn:")?;
|
|
|
|
|
let caught = signal_mask(&status, "SigCgt:")?;
|
|
|
|
|
let sigchld = 1_u128 << (17 - 1);
|
|
|
|
|
if ignored & sigchld == 0 && caught & sigchld == 0 {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(io::Error::other("SIGCHLD is not at default disposition"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 23:50:30 +09:00
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
fn require_reapable_sigchld() -> io::Result<()> {
|
|
|
|
|
let (release, wait) = launch_barrier()?;
|
|
|
|
|
let mut command = private_role_command(SIGCHLD_PROBE_ROLE);
|
|
|
|
|
command.stdin(wait.into_stdio());
|
|
|
|
|
let mut child = command.spawn()?;
|
|
|
|
|
let host_pid = child.id();
|
|
|
|
|
let raw_pid = i32::try_from(host_pid)
|
|
|
|
|
.map_err(|_| io::Error::other("SIGCHLD probe returned out-of-range child PID"))?;
|
|
|
|
|
let pid = Pid::from_raw(raw_pid)
|
|
|
|
|
.ok_or_else(|| io::Error::other("SIGCHLD probe returned invalid child PID"))?;
|
|
|
|
|
let pidfd = match rustix::process::pidfd_open(pid, PidfdFlags::empty()) {
|
|
|
|
|
Ok(pidfd) => pidfd,
|
|
|
|
|
Err(error) => return fail_sigchld_probe(release, &mut child, error.into()),
|
|
|
|
|
};
|
|
|
|
|
if let Err(error) = verify_direct_child_pidfd(pidfd.as_fd()) {
|
|
|
|
|
return fail_sigchld_probe(release, &mut child, error);
|
|
|
|
|
}
|
|
|
|
|
if let Err(error) = release.release() {
|
|
|
|
|
return cleanup_sigchld_probe(pidfd.as_fd(), child, error);
|
|
|
|
|
}
|
|
|
|
|
if let Err(error) = rustix::process::waitid(WaitId::PidFd(pidfd.as_fd()), WaitIdOptions::EXITED)
|
|
|
|
|
{
|
|
|
|
|
return cleanup_sigchld_probe(
|
|
|
|
|
pidfd.as_fd(),
|
|
|
|
|
child,
|
|
|
|
|
io::Error::new(
|
|
|
|
|
error.kind(),
|
|
|
|
|
format!("SIGCHLD semantic probe was not reapable: {error}"),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
drop(child);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
fn cleanup_sigchld_probe(
|
|
|
|
|
pidfd: std::os::fd::BorrowedFd<'_>,
|
|
|
|
|
child: std::process::Child,
|
|
|
|
|
error: io::Error,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
let result = cleanup_sigchld_probe_with(
|
|
|
|
|
error,
|
|
|
|
|
|| rustix::process::pidfd_send_signal(pidfd, Signal::KILL).map_err(Into::into),
|
|
|
|
|
|| {
|
|
|
|
|
rustix::process::waitid(WaitId::PidFd(pidfd), WaitIdOptions::EXITED)
|
|
|
|
|
.map(|_| ())
|
|
|
|
|
.map_err(Into::into)
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
drop(child);
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cleanup_sigchld_probe_with<Terminate, Wait>(
|
|
|
|
|
error: io::Error,
|
|
|
|
|
terminate: Terminate,
|
|
|
|
|
mut wait: Wait,
|
|
|
|
|
) -> io::Result<()>
|
|
|
|
|
where
|
|
|
|
|
Terminate: FnOnce() -> io::Result<()>,
|
|
|
|
|
Wait: FnMut() -> io::Result<()>,
|
|
|
|
|
{
|
|
|
|
|
let _ = terminate();
|
|
|
|
|
loop {
|
|
|
|
|
match wait() {
|
|
|
|
|
Ok(()) => return Err(error),
|
|
|
|
|
Err(wait_error) if wait_error.kind() == io::ErrorKind::Interrupted => {}
|
|
|
|
|
Err(wait_error)
|
|
|
|
|
if wait_error.raw_os_error() == Some(rustix::io::Errno::CHILD.raw_os_error()) =>
|
|
|
|
|
{
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
Err(wait_error) => {
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
error.kind(),
|
|
|
|
|
format!("{error}; exact SIGCHLD probe cleanup failed: {wait_error}"),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
fn fail_sigchld_probe(
|
|
|
|
|
release: BarrierRelease,
|
|
|
|
|
child: &mut std::process::Child,
|
|
|
|
|
error: io::Error,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
drop(release);
|
|
|
|
|
let _ = child.wait();
|
|
|
|
|
Err(error)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
fn signal_mask(status: &str, name: &str) -> io::Result<u128> {
|
|
|
|
|
status
|
|
|
|
|
.lines()
|
|
|
|
|
.find_map(|line| line.strip_prefix(name))
|
|
|
|
|
.and_then(|value| u128::from_str_radix(value.trim(), 16).ok())
|
|
|
|
|
.ok_or_else(|| io::Error::other("process status has no valid signal mask"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the fail-closed pipe cannot be created.
|
|
|
|
|
pub fn launch_barrier() -> io::Result<(BarrierRelease, BarrierWait)> {
|
|
|
|
|
let (reader, writer) = pipe_with(PipeFlags::empty())?;
|
|
|
|
|
fcntl_setfd(writer.as_fd(), FdFlags::CLOEXEC)?;
|
|
|
|
|
Ok((
|
|
|
|
|
BarrierRelease(File::from(writer)),
|
|
|
|
|
BarrierWait(File::from(reader)),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BarrierRelease(File);
|
|
|
|
|
|
|
|
|
|
impl BarrierRelease {
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error unless the complete release token is written.
|
|
|
|
|
pub fn release(mut self) -> io::Result<()> {
|
|
|
|
|
self.0.write_all(BARRIER_RELEASE)?;
|
|
|
|
|
self.0.flush()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BarrierWait(File);
|
|
|
|
|
|
|
|
|
|
impl BarrierWait {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn into_stdio(self) -> Stdio {
|
|
|
|
|
Stdio::from(self.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the inherited standard-input descriptor cannot be duplicated.
|
|
|
|
|
pub fn from_stdin() -> io::Result<Self> {
|
|
|
|
|
Ok(Self(File::open("/proc/self/fd/0")?))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses EOF, partial, altered, or overlong releases.
|
|
|
|
|
pub fn wait(mut self) -> io::Result<()> {
|
|
|
|
|
let mut release = [0_u8; BARRIER_RELEASE.len()];
|
|
|
|
|
self.0.read_exact(&mut release)?;
|
|
|
|
|
let mut excess = [0_u8; 1];
|
|
|
|
|
if release != *BARRIER_RELEASE || self.0.read(&mut excess)? != 0 {
|
|
|
|
|
return Err(io::Error::other("malformed launch-barrier release"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_types)]
|
|
|
|
|
pub fn append_parent_identity(
|
|
|
|
|
command: &mut std::process::Command,
|
|
|
|
|
identity: PidfsProcessIdentityV1,
|
|
|
|
|
) {
|
|
|
|
|
let authority = identity.authority();
|
|
|
|
|
command
|
|
|
|
|
.arg("--parent-boot")
|
|
|
|
|
.arg(hex(authority.boot_id.as_bytes()))
|
|
|
|
|
.arg("--parent-device")
|
|
|
|
|
.arg(authority.pidfs_st_dev.get().to_string())
|
|
|
|
|
.arg("--parent-inode")
|
|
|
|
|
.arg(authority.pidfs_st_ino.get().to_string())
|
|
|
|
|
.arg("--parent-pid")
|
|
|
|
|
.arg(identity.host_pid().get().to_string())
|
|
|
|
|
.arg("--parent-start")
|
|
|
|
|
.arg(identity.start_time_ticks().get().to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses missing, reordered, malformed, or incomplete wrapper arguments.
|
|
|
|
|
pub fn parse_wrapper_arguments(
|
|
|
|
|
arguments: impl IntoIterator<Item = OsString>,
|
|
|
|
|
) -> io::Result<(PidfsProcessIdentityV1, TargetCommand)> {
|
|
|
|
|
let mut arguments = arguments.into_iter();
|
|
|
|
|
let boot = named_argument(&mut arguments, "--parent-boot")?;
|
|
|
|
|
let device = number_argument(&mut arguments, "--parent-device")?;
|
|
|
|
|
let inode = number_argument(&mut arguments, "--parent-inode")?;
|
|
|
|
|
let pid = number_argument(&mut arguments, "--parent-pid")?;
|
|
|
|
|
let start = number_argument(&mut arguments, "--parent-start")?;
|
2026-07-15 19:25:50 +09:00
|
|
|
let parent_edge = match named_argument(&mut arguments, "--parent-action")?.to_str() {
|
|
|
|
|
Some("kill") => ParentEdge::ParentDeathKill,
|
|
|
|
|
Some("lifetime-pipe") => ParentEdge::LifetimePipe,
|
2026-07-15 19:09:24 +09:00
|
|
|
_ => return Err(io::Error::other("wrapper parent action is invalid")),
|
|
|
|
|
};
|
2026-07-17 05:33:22 +09:00
|
|
|
let next = arguments.next();
|
|
|
|
|
let stdin = if next.as_deref() == Some(OsStr::new("--target-stdin-fd")) {
|
|
|
|
|
let descriptor = arguments
|
|
|
|
|
.next()
|
|
|
|
|
.and_then(|value| value.to_str().and_then(|value| value.parse::<i32>().ok()))
|
|
|
|
|
.filter(|descriptor| *descriptor > 2)
|
|
|
|
|
.ok_or_else(|| io::Error::other("wrapper target stdin descriptor is invalid"))?;
|
2026-07-17 17:06:55 +09:00
|
|
|
let access = TargetStdinAccess::from_descriptor(descriptor)?;
|
2026-07-17 05:33:22 +09:00
|
|
|
if arguments.next().as_deref() != Some(OsStr::new("--")) {
|
|
|
|
|
return Err(io::Error::other("wrapper target separator is missing"));
|
|
|
|
|
}
|
2026-07-17 17:06:55 +09:00
|
|
|
Some(access.reopen(descriptor)?)
|
2026-07-17 05:33:22 +09:00
|
|
|
} else if next.as_deref() == Some(OsStr::new("--")) {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
2026-07-14 22:08:29 +09:00
|
|
|
return Err(io::Error::other("wrapper target separator is missing"));
|
2026-07-17 05:33:22 +09:00
|
|
|
};
|
2026-07-14 22:08:29 +09:00
|
|
|
let program = arguments
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| io::Error::other("wrapper target is missing"))?;
|
|
|
|
|
let target = TargetCommand {
|
|
|
|
|
program,
|
|
|
|
|
arguments: arguments.collect(),
|
2026-07-15 19:25:50 +09:00
|
|
|
parent_edge,
|
2026-07-15 19:48:42 +09:00
|
|
|
executable: None,
|
2026-07-17 05:33:22 +09:00
|
|
|
stdin,
|
2026-07-14 22:08:29 +09:00
|
|
|
};
|
|
|
|
|
let boot = parse_boot_hex(&boot)?;
|
|
|
|
|
Ok((
|
|
|
|
|
PidfsProcessIdentityV1::new(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: boot,
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(device),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(inode),
|
|
|
|
|
},
|
|
|
|
|
HostPidLocator::new(
|
|
|
|
|
u32::try_from(pid).map_err(|_| io::Error::other("parent PID is out of range"))?,
|
|
|
|
|
),
|
|
|
|
|
ProcessStartTimeTicks::new(start),
|
|
|
|
|
),
|
|
|
|
|
target,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn named_argument(
|
|
|
|
|
arguments: &mut impl Iterator<Item = OsString>,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> io::Result<OsString> {
|
|
|
|
|
if arguments.next().as_deref() != Some(OsStr::new(name)) {
|
|
|
|
|
return Err(io::Error::other(format!(
|
|
|
|
|
"wrapper argument {name} is missing"
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
arguments
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| io::Error::other(format!("wrapper argument {name} has no value")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn number_argument(arguments: &mut impl Iterator<Item = OsString>, name: &str) -> io::Result<u64> {
|
|
|
|
|
named_argument(arguments, name)?
|
|
|
|
|
.to_str()
|
|
|
|
|
.and_then(|value| value.parse().ok())
|
|
|
|
|
.ok_or_else(|| io::Error::other(format!("wrapper argument {name} is invalid")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_boot_hex(value: &OsStr) -> io::Result<BootId> {
|
|
|
|
|
let value = value
|
|
|
|
|
.to_str()
|
|
|
|
|
.ok_or_else(|| io::Error::other("parent boot ID is not UTF-8"))?;
|
|
|
|
|
if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
|
|
|
|
return Err(io::Error::other("parent boot ID is invalid"));
|
|
|
|
|
}
|
|
|
|
|
let mut bytes = [0_u8; 16];
|
|
|
|
|
for (index, byte) in bytes.iter_mut().enumerate() {
|
|
|
|
|
*byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
|
|
|
|
|
.map_err(|_| io::Error::other("parent boot ID is invalid"))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(BootId::new(bytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn hex(bytes: &[u8]) -> String {
|
|
|
|
|
use std::fmt::Write as _;
|
|
|
|
|
|
|
|
|
|
bytes.iter().fold(String::new(), |mut output, byte| {
|
|
|
|
|
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
|
|
|
|
|
output
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn exec_target(target: TargetCommand) -> io::Error {
|
|
|
|
|
use std::os::unix::process::CommandExt;
|
|
|
|
|
|
|
|
|
|
std::process::Command::new(target.program)
|
|
|
|
|
.args(target.arguments)
|
|
|
|
|
.exec()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 06:59:39 +09:00
|
|
|
/// Stops this process until the host resumes it.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns an error when the kernel refuses the stop signal.
|
|
|
|
|
#[allow(clippy::disallowed_methods)]
|
|
|
|
|
pub fn suspend_current_process() -> io::Result<()> {
|
|
|
|
|
Ok(rustix::process::kill_process(
|
|
|
|
|
rustix::process::getpid(),
|
|
|
|
|
Signal::STOP,
|
|
|
|
|
)?)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::cell::Cell;
|
|
|
|
|
use std::os::fd::{AsFd, AsRawFd};
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn broker_is_process_global() {
|
|
|
|
|
assert!(std::ptr::eq(child_broker(), child_broker()));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 19:25:50 +09:00
|
|
|
#[test]
|
|
|
|
|
fn parent_edges_require_their_exact_stdio_policy() {
|
|
|
|
|
assert!(
|
|
|
|
|
require_parent_edge_standard_io(
|
|
|
|
|
ParentEdge::LifetimePipe,
|
|
|
|
|
ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
|
|
|
|
)
|
|
|
|
|
.is_ok()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
require_parent_edge_standard_io(
|
|
|
|
|
ParentEdge::LifetimePipe,
|
|
|
|
|
ChildStandardIoPolicy::Silenced,
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
require_parent_edge_standard_io(
|
|
|
|
|
ParentEdge::ParentDeathKill,
|
|
|
|
|
ChildStandardIoPolicy::Silenced,
|
|
|
|
|
)
|
|
|
|
|
.is_ok()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
require_parent_edge_standard_io(
|
|
|
|
|
ParentEdge::ParentDeathKill,
|
|
|
|
|
ChildStandardIoPolicy::PrivateControlWithLifetimePipe,
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[test]
|
|
|
|
|
fn barrier_reader_is_inheritable_and_writer_is_close_on_exec() {
|
|
|
|
|
let (release, wait) = launch_barrier().unwrap();
|
|
|
|
|
let reader_flags = rustix::io::fcntl_getfd(wait.0.as_fd()).unwrap();
|
|
|
|
|
let writer_flags = rustix::io::fcntl_getfd(release.0.as_fd()).unwrap();
|
|
|
|
|
assert!(!reader_flags.contains(FdFlags::CLOEXEC));
|
|
|
|
|
assert!(writer_flags.contains(FdFlags::CLOEXEC));
|
|
|
|
|
assert_ne!(wait.0.as_raw_fd(), release.0.as_raw_fd());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:28:18 +09:00
|
|
|
#[test]
|
|
|
|
|
fn bootstrap_child_retains_exact_pipe_owner_and_pidfd_reap() {
|
|
|
|
|
let (reader, writer) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
|
|
|
|
let target = TargetCommand::new("/bin/sh")
|
|
|
|
|
.argument("-c")
|
|
|
|
|
.argument("printf L; read -r _ || exit 0")
|
|
|
|
|
.with_lifetime_pipe_parent_edge();
|
|
|
|
|
let mut child = child_broker()
|
|
|
|
|
.spawn_bootstrap(&target, File::from(reader))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(child.acquisition_status().unwrap(), b'L');
|
|
|
|
|
assert!(!child.has_exited().unwrap());
|
|
|
|
|
drop(writer);
|
|
|
|
|
let status = child.reap().unwrap();
|
|
|
|
|
assert_eq!(status.exit_code, Some(0));
|
|
|
|
|
assert_eq!(status.terminating_signal, None);
|
|
|
|
|
|
|
|
|
|
let (reader, _writer) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
child_broker()
|
|
|
|
|
.spawn_bootstrap(&TargetCommand::new("/bin/true"), File::from(reader))
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[test]
|
|
|
|
|
fn boot_id_requires_exact_canonical_uuid_syntax() {
|
|
|
|
|
parse_boot_id("00112233-4455-6677-8899-aabbccddeeff\n").unwrap();
|
|
|
|
|
for invalid in [
|
|
|
|
|
"00112233445566778899aabbccddeeff",
|
|
|
|
|
"00112233_4455-6677-8899-aabbccddeeff",
|
|
|
|
|
"00112233-4455-6677-8899-aabbccddeeff-extra",
|
|
|
|
|
"00112233-4455-6677-8899-aabbccddeefg",
|
|
|
|
|
"00112233-4455-6677-8899-aabbccddeeff\n\n",
|
|
|
|
|
] {
|
|
|
|
|
assert!(parse_boot_id(invalid).is_err(), "accepted {invalid}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn recovery_checks_boot_before_pid_lookup() {
|
|
|
|
|
let identity = PidfsProcessIdentityV1::new(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: BootId::new([1; 16]),
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(1),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(1),
|
|
|
|
|
},
|
|
|
|
|
HostPidLocator::new(1),
|
|
|
|
|
ProcessStartTimeTicks::new(1),
|
|
|
|
|
);
|
|
|
|
|
let opened = Cell::new(false);
|
2026-07-17 05:38:43 +09:00
|
|
|
let result = match_recovery_process_with(
|
2026-07-14 22:08:29 +09:00
|
|
|
identity,
|
|
|
|
|
|| Ok(BootId::new([2; 16])),
|
|
|
|
|
|_| {
|
|
|
|
|
opened.set(true);
|
|
|
|
|
unreachable!()
|
|
|
|
|
},
|
2026-07-17 05:38:43 +09:00
|
|
|
|_, _| unreachable!(),
|
|
|
|
|
|_| unreachable!(),
|
2026-07-14 22:08:29 +09:00
|
|
|
);
|
2026-07-17 05:38:43 +09:00
|
|
|
assert!(matches!(result, Ok(PidfsRecoveryMatch::BootMismatch)));
|
2026-07-14 22:08:29 +09:00
|
|
|
assert!(!opened.get());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:38:43 +09:00
|
|
|
#[test]
|
|
|
|
|
fn recovery_refuses_each_post_open_mismatch_without_returning_authority() {
|
|
|
|
|
let authority = PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: BootId::new([1; 16]),
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(7),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(9),
|
|
|
|
|
};
|
|
|
|
|
let identity = PidfsProcessIdentityV1::new(
|
|
|
|
|
authority,
|
|
|
|
|
HostPidLocator::new(41),
|
|
|
|
|
ProcessStartTimeTicks::new(43),
|
|
|
|
|
);
|
|
|
|
|
for (observed, start) in [
|
|
|
|
|
(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(8),
|
|
|
|
|
..authority
|
|
|
|
|
},
|
|
|
|
|
43,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(10),
|
|
|
|
|
..authority
|
|
|
|
|
},
|
|
|
|
|
43,
|
|
|
|
|
),
|
|
|
|
|
(authority, 44),
|
|
|
|
|
] {
|
|
|
|
|
let events = Rc::new(std::cell::RefCell::new(Vec::new()));
|
|
|
|
|
let open_events = Rc::clone(&events);
|
|
|
|
|
let inspect_events = Rc::clone(&events);
|
|
|
|
|
let start_events = Rc::clone(&events);
|
|
|
|
|
let result = match_recovery_process_with(
|
|
|
|
|
identity,
|
|
|
|
|
|| Ok(authority.boot_id),
|
|
|
|
|
move |pid| {
|
|
|
|
|
open_events.borrow_mut().push(("open", u64::from(pid)));
|
|
|
|
|
Ok(File::open("/dev/null").unwrap().into())
|
|
|
|
|
},
|
|
|
|
|
move |fd, _| {
|
|
|
|
|
inspect_events
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.push(("inspect", u64::try_from(fd.as_raw_fd()).unwrap()));
|
|
|
|
|
Ok(observed)
|
|
|
|
|
},
|
|
|
|
|
move |pid| {
|
|
|
|
|
start_events.borrow_mut().push(("start", u64::from(pid)));
|
|
|
|
|
Ok(start)
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert_eq!(events.borrow()[0], ("open", 41));
|
|
|
|
|
assert_eq!(events.borrow()[1].0, "inspect");
|
|
|
|
|
if observed == authority {
|
|
|
|
|
assert_eq!(events.borrow()[2], ("start", 41));
|
|
|
|
|
} else {
|
|
|
|
|
assert_eq!(events.borrow().len(), 2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for failure in ["open", "inspect", "start"] {
|
|
|
|
|
let result = match_recovery_process_with(
|
|
|
|
|
identity,
|
|
|
|
|
|| Ok(authority.boot_id),
|
|
|
|
|
|_| {
|
|
|
|
|
if failure == "open" {
|
|
|
|
|
Err(io::Error::from_raw_os_error(3))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(File::open("/dev/null").unwrap().into())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|_, _| {
|
|
|
|
|
if failure == "inspect" {
|
|
|
|
|
Err(io::Error::other("injected descriptor inspection error"))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(authority)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|_| {
|
|
|
|
|
if failure == "start" {
|
|
|
|
|
Err(io::Error::other("injected start observation error"))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(43)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(result.is_err(), "accepted injected {failure} failure");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 05:47:15 +09:00
|
|
|
#[test]
|
|
|
|
|
fn recovery_normalizes_vanished_proc_evidence_to_esrch() {
|
|
|
|
|
let authority = PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: BootId::new([1; 16]),
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(7),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(9),
|
|
|
|
|
};
|
|
|
|
|
let identity = PidfsProcessIdentityV1::new(
|
|
|
|
|
authority,
|
|
|
|
|
HostPidLocator::new(41),
|
|
|
|
|
ProcessStartTimeTicks::new(43),
|
|
|
|
|
);
|
|
|
|
|
for missing in ["open", "start"] {
|
|
|
|
|
let result = match_recovery_process_with(
|
|
|
|
|
identity,
|
|
|
|
|
|| Ok(authority.boot_id),
|
|
|
|
|
|_| {
|
|
|
|
|
if missing == "open" {
|
|
|
|
|
Err(io::Error::from_raw_os_error(
|
|
|
|
|
rustix::io::Errno::NOENT.raw_os_error(),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(File::open("/dev/null").unwrap().into())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|_, _| Ok(authority),
|
|
|
|
|
|_| {
|
|
|
|
|
if missing == "start" {
|
|
|
|
|
Err(io::Error::from_raw_os_error(
|
|
|
|
|
rustix::io::Errno::NOENT.raw_os_error(),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(43)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap_err();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.raw_os_error(),
|
|
|
|
|
Some(rustix::io::Errno::SRCH.raw_os_error())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 05:38:43 +09:00
|
|
|
#[test]
|
|
|
|
|
fn recovery_action_reconciles_boot_without_process_action() {
|
|
|
|
|
let mut old_boot = *read_boot_id().unwrap().as_bytes();
|
|
|
|
|
old_boot[0] ^= u8::MAX;
|
|
|
|
|
let identity = PidfsProcessIdentityV1::new(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: BootId::new(old_boot),
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(1),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(1),
|
|
|
|
|
},
|
|
|
|
|
HostPidLocator::new(u32::MAX),
|
|
|
|
|
ProcessStartTimeTicks::new(u64::MAX),
|
|
|
|
|
);
|
|
|
|
|
let action_called = Cell::new(false);
|
|
|
|
|
let result = recover_process_action(
|
|
|
|
|
identity,
|
|
|
|
|
|| Ok("reconciled"),
|
|
|
|
|
|_| {
|
|
|
|
|
action_called.set(true);
|
|
|
|
|
Ok("acted")
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(result, RecoveryActionResult::BootReconciled("reconciled"));
|
|
|
|
|
assert!(!action_called.get());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 22:08:29 +09:00
|
|
|
#[test]
|
|
|
|
|
fn pidfs_magic_precedes_same_descriptor_stat_and_errors_refuse() {
|
|
|
|
|
let file = File::open("/dev/null").unwrap();
|
|
|
|
|
let raw = file.as_raw_fd();
|
|
|
|
|
let events = Rc::new(std::cell::RefCell::new(Vec::new()));
|
|
|
|
|
let filesystem_events = Rc::clone(&events);
|
|
|
|
|
let stat_events = Rc::clone(&events);
|
|
|
|
|
let authority = authority_from_pidfd_with(
|
|
|
|
|
file.as_fd(),
|
|
|
|
|
BootId::new([1; 16]),
|
|
|
|
|
move |fd| {
|
|
|
|
|
filesystem_events
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.push(("fstatfs", fd.as_raw_fd()));
|
|
|
|
|
Ok(PID_FS_MAGIC)
|
|
|
|
|
},
|
|
|
|
|
move |fd| {
|
|
|
|
|
stat_events.borrow_mut().push(("fstat", fd.as_raw_fd()));
|
|
|
|
|
Ok((7, 9))
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(&*events.borrow(), &[("fstatfs", raw), ("fstat", raw)]);
|
|
|
|
|
assert_eq!(authority.pidfs_st_dev.get(), 7);
|
|
|
|
|
assert_eq!(authority.pidfs_st_ino.get(), 9);
|
|
|
|
|
|
|
|
|
|
for filesystem_error in [false, true] {
|
|
|
|
|
let stat_called = Cell::new(false);
|
|
|
|
|
let result = authority_from_pidfd_with(
|
|
|
|
|
file.as_fd(),
|
|
|
|
|
BootId::new([1; 16]),
|
|
|
|
|
|_| {
|
|
|
|
|
if filesystem_error {
|
|
|
|
|
Err(io::Error::other("injected fstatfs error"))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(0)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|_| {
|
|
|
|
|
stat_called.set(true);
|
|
|
|
|
Ok((1, 1))
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert!(!stat_called.get());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
authority_from_pidfd_with(
|
|
|
|
|
file.as_fd(),
|
|
|
|
|
BootId::new([1; 16]),
|
|
|
|
|
|_| Ok(PID_FS_MAGIC),
|
|
|
|
|
|_| Err(io::Error::other("injected fstat error")),
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-14 23:50:30 +09:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn sigchld_probe_epipe_terminates_and_reaps_exact_child() {
|
|
|
|
|
let events = Rc::new(std::cell::RefCell::new(Vec::new()));
|
|
|
|
|
let terminate_events = Rc::clone(&events);
|
|
|
|
|
let wait_events = Rc::clone(&events);
|
|
|
|
|
let result = cleanup_sigchld_probe_with(
|
|
|
|
|
io::Error::new(io::ErrorKind::BrokenPipe, "probe died before release"),
|
|
|
|
|
move || {
|
|
|
|
|
terminate_events.borrow_mut().push("terminate-pidfd");
|
|
|
|
|
Err(io::Error::from_raw_os_error(3))
|
|
|
|
|
},
|
|
|
|
|
move || {
|
|
|
|
|
wait_events.borrow_mut().push("wait-pidfd");
|
|
|
|
|
Ok(())
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::BrokenPipe);
|
|
|
|
|
assert_eq!(&*events.borrow(), &["terminate-pidfd", "wait-pidfd"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn sigchld_probe_failed_wait_retries_cleanup_until_not_waitable() {
|
|
|
|
|
let waits = Cell::new(0_u8);
|
|
|
|
|
let result = cleanup_sigchld_probe_with(
|
|
|
|
|
io::Error::other("injected semantic wait failure"),
|
|
|
|
|
|| Ok(()),
|
|
|
|
|
|| {
|
|
|
|
|
waits.set(waits.get() + 1);
|
|
|
|
|
match waits.get() {
|
|
|
|
|
1 => Err(io::Error::from(io::ErrorKind::Interrupted)),
|
|
|
|
|
_ => Err(io::Error::from_raw_os_error(
|
|
|
|
|
rustix::io::Errno::CHILD.raw_os_error(),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Other);
|
|
|
|
|
assert_eq!(waits.get(), 2);
|
|
|
|
|
}
|
2026-07-14 22:08:29 +09:00
|
|
|
}
|