Add confined host escalation transport
This commit is contained in:
parent
903ff837d6
commit
407b75b1ff
2 changed files with 2888 additions and 21 deletions
|
|
@ -1,18 +1,299 @@
|
|||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Seek, SeekFrom, Write};
|
||||
use std::os::fd::AsFd;
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::os::fd::{AsFd, OwnedFd};
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use lazaret_core::identity::{DeviceId, InodeId, MountId, MountIdentity, NamespaceIdentity};
|
||||
use rustix::fs::{
|
||||
AtFlags, FlockOperation, MemfdFlags, SealFlags, StatxFlags, fcntl_add_seals, fcntl_get_seals,
|
||||
flock, fstat, makedev, memfd_create, statx,
|
||||
AtFlags, FlockOperation, MemfdFlags, Mode, OFlags, RawDir, RenameFlags, SealFlags, StatxFlags,
|
||||
fchmod, fcntl_add_seals, fcntl_get_seals, flock, fstat, makedev, memfd_create, openat,
|
||||
renameat_with, statat, statx, unlinkat,
|
||||
};
|
||||
|
||||
static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MailboxEntryIdentity {
|
||||
device: u64,
|
||||
inode: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RegularFileMailbox {
|
||||
directory: File,
|
||||
}
|
||||
|
||||
impl AsFd for RegularFileMailbox {
|
||||
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
|
||||
self.directory.as_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl RegularFileMailbox {
|
||||
/// # Errors
|
||||
/// Refuses entries that are not invoking-user-owned mode-0700 directories opened without links.
|
||||
pub fn openat_private(
|
||||
parent: impl AsFd,
|
||||
name: &std::ffi::OsStr,
|
||||
invoking_uid: u32,
|
||||
) -> io::Result<Self> {
|
||||
validate_mailbox_name(name)?;
|
||||
let directory = File::from(openat(
|
||||
parent.as_fd(),
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?);
|
||||
Self::from_private_directory(directory, invoking_uid)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses paths that are not invoking-user-owned mode-0700 directories opened without links.
|
||||
pub fn open_private(path: &Path, invoking_uid: u32) -> io::Result<Self> {
|
||||
let directory = File::from(openat(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?);
|
||||
Self::from_private_directory(directory, invoking_uid)
|
||||
}
|
||||
|
||||
fn from_private_directory(directory: File, invoking_uid: u32) -> io::Result<Self> {
|
||||
let metadata = directory.metadata()?;
|
||||
if !metadata.file_type().is_dir()
|
||||
|| metadata.uid() != invoking_uid
|
||||
|| metadata.mode() & 0o7777 != 0o700
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"mailbox directory is not invoking-user-owned mode 0700",
|
||||
));
|
||||
}
|
||||
Ok(Self { directory })
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a path that is not a directory opened without following its final component.
|
||||
pub fn open(path: &Path) -> io::Result<Self> {
|
||||
let directory = File::from(openat(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?);
|
||||
Ok(Self { directory })
|
||||
}
|
||||
|
||||
/// Publishes one complete regular-file record using a no-replace atomic rename.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses invalid names, existing destinations, or incomplete durable publication.
|
||||
pub fn publish(
|
||||
&self,
|
||||
name: &std::ffi::OsStr,
|
||||
bytes: &[u8],
|
||||
) -> io::Result<MailboxEntryIdentity> {
|
||||
validate_mailbox_name(name)?;
|
||||
let temporary_name = loop {
|
||||
let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let candidate = std::ffi::OsString::from_vec(
|
||||
format!(".lazaret-{}-{sequence}.tmp", std::process::id()).into_bytes(),
|
||||
);
|
||||
match openat(
|
||||
self.directory.as_fd(),
|
||||
&candidate,
|
||||
OFlags::CREATE | OFlags::EXCL | OFlags::WRONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::RUSR | Mode::WUSR,
|
||||
) {
|
||||
Ok(fd) => break (candidate, fd),
|
||||
Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
};
|
||||
let (temporary_name, fd) = temporary_name;
|
||||
let mut file = File::from(fd);
|
||||
let result = (|| {
|
||||
fchmod(file.as_fd(), Mode::RUSR | Mode::WUSR)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()?;
|
||||
let metadata = file.metadata()?;
|
||||
if !metadata.file_type().is_file()
|
||||
|| metadata.nlink() != 1
|
||||
|| usize::try_from(metadata.len()).ok() != Some(bytes.len())
|
||||
{
|
||||
return Err(io::Error::other("mailbox temporary record changed"));
|
||||
}
|
||||
renameat_with(
|
||||
self.directory.as_fd(),
|
||||
&temporary_name,
|
||||
self.directory.as_fd(),
|
||||
name,
|
||||
RenameFlags::NOREPLACE,
|
||||
)?;
|
||||
self.directory.sync_all()?;
|
||||
Ok(MailboxEntryIdentity {
|
||||
device: metadata.dev(),
|
||||
inode: metadata.ino(),
|
||||
})
|
||||
})();
|
||||
if result.is_err() {
|
||||
let _ = unlinkat(self.directory.as_fd(), &temporary_name, AtFlags::empty());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Reads and removes one exact regular-file entry after bounding it before allocation.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses links, non-regular files, oversize records, replacement, growth, or short reads.
|
||||
pub fn consume(&self, name: &std::ffi::OsStr, maximum: usize) -> io::Result<Vec<u8>> {
|
||||
let (bytes, identity) = self.read(name, maximum)?;
|
||||
self.remove_if_matches(name, identity)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Reads one exact regular-file entry after bounding it before allocation.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses links, non-regular files, oversize records, replacement, growth, or short reads.
|
||||
pub fn read(
|
||||
&self,
|
||||
name: &std::ffi::OsStr,
|
||||
maximum: usize,
|
||||
) -> io::Result<(Vec<u8>, MailboxEntryIdentity)> {
|
||||
validate_mailbox_name(name)?;
|
||||
let mut file = File::from(openat(
|
||||
self.directory.as_fd(),
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?);
|
||||
let before = file.metadata()?;
|
||||
let length = usize::try_from(before.len())
|
||||
.map_err(|_| io::Error::other("mailbox record length is out of range"))?;
|
||||
if !before.file_type().is_file() || before.nlink() != 1 {
|
||||
return Err(io::Error::other(
|
||||
"mailbox entry is linked or is not a regular file",
|
||||
));
|
||||
}
|
||||
if length > maximum {
|
||||
return Err(io::Error::other("mailbox record exceeds the size limit"));
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(length.saturating_add(1));
|
||||
Read::by_ref(&mut file)
|
||||
.take(u64::try_from(maximum).unwrap_or(u64::MAX).saturating_add(1))
|
||||
.read_to_end(&mut bytes)?;
|
||||
let after = file.metadata()?;
|
||||
if bytes.len() != length
|
||||
|| after.dev() != before.dev()
|
||||
|| after.ino() != before.ino()
|
||||
|| after.len() != before.len()
|
||||
|| after.nlink() != 1
|
||||
|| !after.file_type().is_file()
|
||||
{
|
||||
return Err(io::Error::other("mailbox record changed while being read"));
|
||||
}
|
||||
Ok((
|
||||
bytes,
|
||||
MailboxEntryIdentity {
|
||||
device: before.dev(),
|
||||
inode: before.ino(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Removes a host-published record only while the pathname retains its exact identity.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a missing or replaced pathname.
|
||||
pub fn remove_if_matches(
|
||||
&self,
|
||||
name: &std::ffi::OsStr,
|
||||
expected: MailboxEntryIdentity,
|
||||
) -> io::Result<()> {
|
||||
validate_mailbox_name(name)?;
|
||||
let quarantine = loop {
|
||||
let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let candidate = std::ffi::OsString::from_vec(
|
||||
format!(".lazaret-remove-{}-{sequence}.tmp", std::process::id()).into_bytes(),
|
||||
);
|
||||
match renameat_with(
|
||||
self.directory.as_fd(),
|
||||
name,
|
||||
self.directory.as_fd(),
|
||||
&candidate,
|
||||
RenameFlags::NOREPLACE,
|
||||
) {
|
||||
Ok(()) => break candidate,
|
||||
Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
};
|
||||
let current = statat(
|
||||
self.directory.as_fd(),
|
||||
&quarantine,
|
||||
AtFlags::SYMLINK_NOFOLLOW,
|
||||
)?;
|
||||
if current.st_dev != expected.device || current.st_ino != expected.inode {
|
||||
let restore = renameat_with(
|
||||
self.directory.as_fd(),
|
||||
&quarantine,
|
||||
self.directory.as_fd(),
|
||||
name,
|
||||
RenameFlags::NOREPLACE,
|
||||
);
|
||||
return match restore {
|
||||
Ok(()) => Err(io::Error::other("mailbox entry identity mismatch")),
|
||||
Err(error) => Err(io::Error::other(format!(
|
||||
"mailbox entry identity mismatch and restore failed: {error}"
|
||||
))),
|
||||
};
|
||||
}
|
||||
unlinkat(self.directory.as_fd(), &quarantine, AtFlags::empty())?;
|
||||
self.directory.sync_all()
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Returns an error when the retained directory cannot be enumerated.
|
||||
pub fn names(&self) -> io::Result<Vec<std::ffi::OsString>> {
|
||||
let fd: OwnedFd = openat(
|
||||
self.directory.as_fd(),
|
||||
".",
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?;
|
||||
let mut buffer = vec![std::mem::MaybeUninit::<u8>::uninit(); 8192];
|
||||
let mut directory = RawDir::new(fd, &mut buffer);
|
||||
let mut names = Vec::new();
|
||||
while let Some(entry) = directory.next() {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_bytes();
|
||||
if name != b"." && name != b".." {
|
||||
names.push(std::ffi::OsString::from_vec(name.to_vec()));
|
||||
}
|
||||
}
|
||||
Ok(names)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mailbox_name(name: &std::ffi::OsStr) -> io::Result<()> {
|
||||
let bytes = name.as_bytes();
|
||||
if bytes.is_empty()
|
||||
|| bytes.len() > 255
|
||||
|| bytes == b"."
|
||||
|| bytes == b".."
|
||||
|| bytes.contains(&0)
|
||||
|| bytes.contains(&b'/')
|
||||
{
|
||||
Err(io::Error::other("invalid mailbox entry name"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const REQUIRED_SEALS: SealFlags = SealFlags::SHRINK
|
||||
.union(SealFlags::GROW)
|
||||
.union(SealFlags::WRITE)
|
||||
|
|
@ -159,12 +440,13 @@ pub fn require_mount_identity(path: &Path, expected: MountIdentity) -> io::Resul
|
|||
mod tests {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use rustix::fs::{MemfdFlags, memfd_create};
|
||||
|
||||
use super::{
|
||||
atomic_replace, current_mount_identity, namespace_identity, require_mount_identity,
|
||||
require_write_sealed, sealed_memfd, try_exclusive_flock,
|
||||
RegularFileMailbox, atomic_replace, current_mount_identity, namespace_identity,
|
||||
require_mount_identity, require_write_sealed, sealed_memfd, try_exclusive_flock,
|
||||
};
|
||||
|
||||
fn test_directory(name: &str) -> std::path::PathBuf {
|
||||
|
|
@ -265,4 +547,69 @@ mod tests {
|
|||
assert!(require_mount_identity(&old, expected).is_ok());
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_publication_is_atomic_and_no_replace() {
|
||||
let directory = test_directory("mailbox-publish");
|
||||
let mailbox = RegularFileMailbox::open(&directory).unwrap();
|
||||
let name = std::ffi::OsStr::new("request.ready");
|
||||
mailbox.publish(name, b"complete").unwrap();
|
||||
assert!(mailbox.publish(name, b"replacement").is_err());
|
||||
assert_eq!(std::fs::read(directory.join(name)).unwrap(), b"complete");
|
||||
assert!(
|
||||
mailbox
|
||||
.names()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|entry| !entry.as_encoded_bytes().ends_with(b".tmp"))
|
||||
);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_refuses_links_nonregular_and_oversize_records() {
|
||||
let directory = test_directory("mailbox-hostile");
|
||||
let mailbox = RegularFileMailbox::open(&directory).unwrap();
|
||||
std::fs::write(directory.join("target"), b"target").unwrap();
|
||||
symlink("target", directory.join("symlink.ready")).unwrap();
|
||||
std::fs::hard_link(directory.join("target"), directory.join("hardlink.ready")).unwrap();
|
||||
std::fs::create_dir(directory.join("directory.ready")).unwrap();
|
||||
std::fs::write(directory.join("oversize.ready"), [0_u8; 17]).unwrap();
|
||||
|
||||
assert!(
|
||||
mailbox
|
||||
.consume(std::ffi::OsStr::new("symlink.ready"), 16)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
mailbox
|
||||
.consume(std::ffi::OsStr::new("hardlink.ready"), 16)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
mailbox
|
||||
.consume(std::ffi::OsStr::new("directory.ready"), 16)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
mailbox
|
||||
.consume(std::ffi::OsStr::new("oversize.ready"), 16)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_removes_only_the_identity_it_read() {
|
||||
let directory = test_directory("mailbox-replacement");
|
||||
let mailbox = RegularFileMailbox::open(&directory).unwrap();
|
||||
let name = std::ffi::OsStr::new("result.ready");
|
||||
let identity = mailbox.publish(name, b"first").unwrap();
|
||||
std::fs::rename(directory.join(name), directory.join("old")).unwrap();
|
||||
std::fs::write(directory.join(name), b"foreign").unwrap();
|
||||
assert!(mailbox.remove_if_matches(name, identity).is_err());
|
||||
assert_eq!(std::fs::read(directory.join(name)).unwrap(), b"foreign");
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
|
||||
use std::process::Stdio;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ use lazaret_core::records::{
|
|||
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||
};
|
||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||
use rustix::fs::{fstat, fstatfs};
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat, fstatfs};
|
||||
use rustix::io::{FdFlags, fcntl_setfd};
|
||||
use rustix::pipe::{PipeFlags, pipe_with};
|
||||
use rustix::process::{Pid, PidfdFlags, Signal, WaitId, WaitIdOptions};
|
||||
|
|
@ -25,11 +25,12 @@ const WRAPPER_ROLE: &str = "launch-wrapper";
|
|||
const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe";
|
||||
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct TargetCommand {
|
||||
program: OsString,
|
||||
arguments: Vec<OsString>,
|
||||
parent_edge: ParentEdge,
|
||||
executable: Option<OwnedFd>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -45,9 +46,27 @@ impl TargetCommand {
|
|||
program: program.into(),
|
||||
arguments: Vec::new(),
|
||||
parent_edge: ParentEdge::ParentDeathKill,
|
||||
executable: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn argument(mut self, argument: impl Into<OsString>) -> Self {
|
||||
self.arguments.push(argument.into());
|
||||
|
|
@ -73,6 +92,7 @@ impl TargetCommand {
|
|||
|
||||
#[allow(clippy::disallowed_types)]
|
||||
fn append_to(&self, command: &mut std::process::Command) {
|
||||
let _ = &self.executable;
|
||||
command
|
||||
.arg("--parent-action")
|
||||
.arg(match self.parent_edge {
|
||||
|
|
@ -149,6 +169,35 @@ pub struct RetainedProcess {
|
|||
identity: PidfsProcessIdentityV1,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CapturedProcess {
|
||||
process: RetainedProcess,
|
||||
stdout: File,
|
||||
stderr: File,
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
impl RetainedProcess {
|
||||
#[must_use]
|
||||
pub const fn identity(&self) -> PidfsProcessIdentityV1 {
|
||||
|
|
@ -212,6 +261,16 @@ fn require_parent_edge_standard_io(
|
|||
}
|
||||
}
|
||||
|
||||
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",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildBroker {
|
||||
/// Spawns the repository wrapper, stages exact ownership, accepts its acknowledgment,
|
||||
/// and only then releases target execution.
|
||||
|
|
@ -312,18 +371,144 @@ impl ChildBroker {
|
|||
Ok(process)
|
||||
}
|
||||
|
||||
/// Acquires an exact child with bounded-output pipe ownership retained by the caller.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error when serialization or exact pidfd reaping fails.
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
pub fn reap(&self, process: RetainedProcess) -> io::Result<()> {
|
||||
/// 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>,
|
||||
{
|
||||
let _guard = self
|
||||
.exclusion
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
||||
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
|
||||
drop(process);
|
||||
Ok(())
|
||||
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);
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// # 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_types)]
|
||||
|
|
@ -429,30 +614,27 @@ pub fn verify_direct_child_pidfd(pidfd: impl AsFd) -> io::Result<()> {
|
|||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
fn fail_before_pidfd(
|
||||
fn fail_before_pidfd<T>(
|
||||
release: BarrierRelease,
|
||||
child: &mut std::process::Child,
|
||||
error: io::Error,
|
||||
) -> io::Result<RetainedProcess> {
|
||||
) -> io::Result<T> {
|
||||
drop(release);
|
||||
child.wait()?;
|
||||
Err(error)
|
||||
}
|
||||
|
||||
fn fail_after_pidfd(
|
||||
fn fail_after_pidfd<T>(
|
||||
release: BarrierRelease,
|
||||
process: RetainedProcess,
|
||||
error: io::Error,
|
||||
) -> io::Result<RetainedProcess> {
|
||||
) -> io::Result<T> {
|
||||
drop(release);
|
||||
fail_after_pidfd_closed(process, error)
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
fn fail_after_pidfd_closed(
|
||||
process: RetainedProcess,
|
||||
error: io::Error,
|
||||
) -> io::Result<RetainedProcess> {
|
||||
fn fail_after_pidfd_closed<T>(process: RetainedProcess, error: io::Error) -> io::Result<T> {
|
||||
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
|
||||
drop(process);
|
||||
Err(error)
|
||||
|
|
@ -848,6 +1030,7 @@ pub fn parse_wrapper_arguments(
|
|||
program,
|
||||
arguments: arguments.collect(),
|
||||
parent_edge,
|
||||
executable: None,
|
||||
};
|
||||
let boot = parse_boot_hex(&boot)?;
|
||||
Ok((
|
||||
|
|
|
|||
Loading…
Reference in a new issue