Enforce causal child acquisition boundaries
This commit is contained in:
parent
1468f6b841
commit
6f722ce079
9 changed files with 603 additions and 146 deletions
|
|
@ -18,5 +18,10 @@ name = "capability-drop-helper"
|
||||||
path = "tests/capability-drop-helper.rs"
|
path = "tests/capability-drop-helper.rs"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "acquisition"
|
||||||
|
path = "tests/acquisition.rs"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{dispatch_private_process_role, exec_target};
|
||||||
BarrierWait, arm_parent_death_and_recheck, exec_target, parse_wrapper_arguments,
|
|
||||||
};
|
|
||||||
use rustix::process::Signal;
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
match run() {
|
match run() {
|
||||||
|
|
@ -18,6 +15,9 @@ fn main() -> ExitCode {
|
||||||
|
|
||||||
fn run() -> std::io::Result<()> {
|
fn run() -> std::io::Result<()> {
|
||||||
let arguments = std::env::args_os().skip(1).collect::<Vec<OsString>>();
|
let arguments = std::env::args_os().skip(1).collect::<Vec<OsString>>();
|
||||||
|
if let Some(result) = dispatch_private_process_role(&arguments) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
if arguments == [OsString::from("--trace-self-test")] {
|
if arguments == [OsString::from("--trace-self-test")] {
|
||||||
let (release, wait) = lazaret_linux::process::launch_barrier()?;
|
let (release, wait) = lazaret_linux::process::launch_barrier()?;
|
||||||
release.release()?;
|
release.release()?;
|
||||||
|
|
@ -26,9 +26,7 @@ fn run() -> std::io::Result<()> {
|
||||||
"/bin/true",
|
"/bin/true",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
|
Err(std::io::Error::other(
|
||||||
BarrierWait::from_stdin()?.wait()?;
|
"no public wrapper command was requested",
|
||||||
rustix::io::fcntl_setfd(std::io::stdin(), rustix::io::FdFlags::CLOEXEC)?;
|
))
|
||||||
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
|
|
||||||
Err(exec_target(target))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ use std::ffi::{OsStr, OsString};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::os::fd::{AsFd, OwnedFd};
|
use std::os::fd::{AsFd, OwnedFd};
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use lazaret_core::identity::{
|
use lazaret_core::identity::{
|
||||||
BootId, HostPidLocator, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1,
|
BootId, CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsDeviceId, PidfsInodeId,
|
||||||
PidfsProcessIdentityV1, ProcessStartTimeTicks,
|
PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks,
|
||||||
|
};
|
||||||
|
use lazaret_core::records::{
|
||||||
|
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||||
};
|
};
|
||||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||||
use rustix::fs::{fstat, fstatfs};
|
use rustix::fs::{fstat, fstatfs};
|
||||||
|
|
@ -18,6 +20,9 @@ use rustix::process::{Pid, PidfdFlags, Signal, WaitId, WaitIdOptions};
|
||||||
|
|
||||||
pub const PID_FS_MAGIC: i64 = 0x5049_4446;
|
pub const PID_FS_MAGIC: i64 = 0x5049_4446;
|
||||||
const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!";
|
const BARRIER_RELEASE: &[u8; 32] = b"LAZARET-CAUSAL-BARRIER-RELEASE!!";
|
||||||
|
const PRIVATE_ROLE_ARGUMENT: &str = "--lazaret-private-process-role";
|
||||||
|
const WRAPPER_ROLE: &str = "launch-wrapper";
|
||||||
|
const SIGCHLD_PROBE_ROLE: &str = "sigchld-probe";
|
||||||
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
static CHILD_BROKER: OnceLock<ChildBroker> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -47,6 +52,14 @@ impl TargetCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ProcessOwnershipContext {
|
||||||
|
pub domain_id: DomainId,
|
||||||
|
pub generation_id: GenerationId,
|
||||||
|
pub coordinator_id: CoordinatorId,
|
||||||
|
pub slot: HostProcessOwnershipSlot,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RetainedProcess {
|
pub struct RetainedProcess {
|
||||||
pidfd: OwnedFd,
|
pidfd: OwnedFd,
|
||||||
|
|
@ -96,25 +109,26 @@ impl ChildBroker {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses and reaps the blocked wrapper on every failed acquisition or callback.
|
/// Refuses and reaps the blocked wrapper on every failed acquisition or callback.
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
pub fn acquire<S, Stage, Acknowledge>(
|
pub fn acquire<Stage, Acknowledge>(
|
||||||
&self,
|
&self,
|
||||||
wrapper: &Path,
|
|
||||||
expected_parent: PidfsProcessIdentityV1,
|
expected_parent: PidfsProcessIdentityV1,
|
||||||
|
ownership: ProcessOwnershipContext,
|
||||||
target: &TargetCommand,
|
target: &TargetCommand,
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
acknowledge: Acknowledge,
|
acknowledge: Acknowledge,
|
||||||
) -> io::Result<RetainedProcess>
|
) -> io::Result<RetainedProcess>
|
||||||
where
|
where
|
||||||
Stage: FnOnce(PidfsProcessIdentityV1) -> io::Result<S>,
|
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
||||||
Acknowledge: FnOnce(&S, PidfsProcessIdentityV1) -> io::Result<()>,
|
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
||||||
{
|
{
|
||||||
let _guard = self
|
let _guard = self
|
||||||
.exclusion
|
.exclusion
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
|
||||||
require_default_sigchld()?;
|
require_default_sigchld()?;
|
||||||
|
require_reapable_sigchld()?;
|
||||||
let (release, wait) = launch_barrier()?;
|
let (release, wait) = launch_barrier()?;
|
||||||
let mut command = std::process::Command::new(wrapper);
|
let mut command = private_role_command(WRAPPER_ROLE);
|
||||||
append_parent_identity(&mut command, expected_parent);
|
append_parent_identity(&mut command, expected_parent);
|
||||||
target.append_to(&mut command);
|
target.append_to(&mut command);
|
||||||
command.stdin(wait.into_stdio());
|
command.stdin(wait.into_stdio());
|
||||||
|
|
@ -125,12 +139,28 @@ impl ChildBroker {
|
||||||
Ok(process) => process,
|
Ok(process) => process,
|
||||||
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
Err(error) => return fail_before_pidfd(release, &mut child, error),
|
||||||
};
|
};
|
||||||
let staged = match stage(process.identity) {
|
let staged = StagedProcessOwnership::v1(
|
||||||
Ok(staged) => staged,
|
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),
|
Err(error) => return fail_after_pidfd(release, process, error),
|
||||||
};
|
};
|
||||||
if let Err(error) = acknowledge(&staged, process.identity) {
|
if !acknowledgment.acknowledges(&staged) {
|
||||||
return fail_after_pidfd(release, process, error);
|
return fail_after_pidfd(
|
||||||
|
release,
|
||||||
|
process,
|
||||||
|
io::Error::other(
|
||||||
|
"process ownership acknowledgment does not match staged ownership",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Err(error) = release.release() {
|
if let Err(error) = release.release() {
|
||||||
return fail_after_pidfd_closed(process, error);
|
return fail_after_pidfd_closed(process, error);
|
||||||
|
|
@ -153,6 +183,44 @@ impl ChildBroker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)?;
|
||||||
|
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
|
||||||
|
Err(exec_target(target))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
fn acquire_spawned_child(host_pid: u32) -> io::Result<RetainedProcess> {
|
fn acquire_spawned_child(host_pid: u32) -> io::Result<RetainedProcess> {
|
||||||
let raw_pid = i32::try_from(host_pid)
|
let raw_pid = i32::try_from(host_pid)
|
||||||
|
|
@ -406,6 +474,101 @@ fn require_default_sigchld() -> io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
|
||||||
fn signal_mask(status: &str, name: &str) -> io::Result<u128> {
|
fn signal_mask(status: &str, name: &str) -> io::Result<u128> {
|
||||||
status
|
status
|
||||||
.lines()
|
.lines()
|
||||||
|
|
@ -695,4 +858,44 @@ mod tests {
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,56 @@
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Seek, Write};
|
use std::io::{Seek, Write};
|
||||||
use std::os::fd::AsFd;
|
use std::os::fd::AsFd;
|
||||||
use std::process::Stdio;
|
use std::process::{ExitCode, Stdio};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Barrier};
|
use std::sync::{Arc, Barrier};
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
use lazaret_core::identity::{
|
||||||
|
CoordinatorId, DomainId, GenerationId, HostPidLocator, PidfsProcessIdentityV1,
|
||||||
|
};
|
||||||
|
use lazaret_core::records::{
|
||||||
|
HostProcessOwnershipSlot, ProcessOwnershipCommitAck, StagedProcessOwnership,
|
||||||
|
};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
TargetCommand, append_parent_identity, child_broker, current_process_identity,
|
ProcessOwnershipContext, TargetCommand, append_parent_identity, child_broker,
|
||||||
verify_direct_child_pidfd,
|
current_process_identity, dispatch_private_process_role, verify_direct_child_pidfd,
|
||||||
};
|
};
|
||||||
use rustix::process::{PidfdFlags, pidfd_open};
|
use rustix::process::{PidfdFlags, pidfd_open};
|
||||||
|
|
||||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
|
||||||
|
if let Some(result) = dispatch_private_process_role(&arguments) {
|
||||||
|
return exit(result);
|
||||||
|
}
|
||||||
|
if arguments.first() == Some(&OsString::from("--run-acquisition")) {
|
||||||
|
return exit(run_acquisition(&arguments[1..]));
|
||||||
|
}
|
||||||
|
exit(run_all())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(result: std::io::Result<()>) -> ExitCode {
|
||||||
|
match result {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("acquisition fixture failed: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_all() -> std::io::Result<()> {
|
||||||
|
successful_acquisition()?;
|
||||||
|
failed_stages_and_acknowledgments()?;
|
||||||
|
production_wrapper_refuses_former_harness_path()?;
|
||||||
|
malformed_barriers_refuse()?;
|
||||||
|
sa_nocldwait_refuses()?;
|
||||||
|
foreign_non_child_refuses()?;
|
||||||
|
concurrent_acquisitions_serialize()
|
||||||
|
}
|
||||||
|
|
||||||
fn marker(name: &str) -> std::path::PathBuf {
|
fn marker(name: &str) -> std::path::PathBuf {
|
||||||
std::env::temp_dir().join(format!(
|
std::env::temp_dir().join(format!(
|
||||||
"lazaret-acquisition-{name}-{}-{}",
|
"lazaret-acquisition-{name}-{}-{}",
|
||||||
|
|
@ -23,80 +59,177 @@ fn marker(name: &str) -> std::path::PathBuf {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn target(path: &std::path::Path) -> TargetCommand {
|
fn production_wrapper() -> &'static std::path::Path {
|
||||||
TargetCommand::new("/bin/sh")
|
|
||||||
.argument("-c")
|
|
||||||
.argument(format!("printf target > '{}'", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrapper() -> &'static std::path::Path {
|
|
||||||
std::path::Path::new(env!("CARGO_BIN_EXE_lazaret-launch-wrapper"))
|
std::path::Path::new(env!("CARGO_BIN_EXE_lazaret-launch-wrapper"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn context() -> ProcessOwnershipContext {
|
||||||
fn target_runs_only_after_exact_stage_ack_and_release() {
|
ProcessOwnershipContext {
|
||||||
let marker = marker("released");
|
domain_id: DomainId::new([1; 32]),
|
||||||
let parent = current_process_identity().unwrap();
|
generation_id: GenerationId::new(2),
|
||||||
let process = child_broker()
|
coordinator_id: CoordinatorId::new([3; 32]),
|
||||||
.acquire(
|
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||||
wrapper(),
|
|
||||||
parent,
|
|
||||||
&target(&marker),
|
|
||||||
|identity| {
|
|
||||||
assert!(!marker.exists());
|
|
||||||
Ok(identity)
|
|
||||||
},
|
|
||||||
|staged, identity| {
|
|
||||||
assert_eq!(*staged, identity);
|
|
||||||
assert!(!marker.exists());
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
for _ in 0..100 {
|
|
||||||
if marker.exists() {
|
|
||||||
child_broker().reap(process).unwrap();
|
|
||||||
assert_eq!(std::fs::read(&marker).unwrap(), b"target");
|
|
||||||
std::fs::remove_file(marker).unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
panic!("released target did not run");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stage_and_ack_failures_leave_target_absent() {
|
|
||||||
for failure in ["stage", "ack"] {
|
|
||||||
let marker = marker(failure);
|
|
||||||
let parent = current_process_identity().unwrap();
|
|
||||||
let result = child_broker().acquire(
|
|
||||||
wrapper(),
|
|
||||||
parent,
|
|
||||||
&target(&marker),
|
|
||||||
|_| {
|
|
||||||
if failure == "stage" {
|
|
||||||
Err(std::io::Error::other("injected stage failure"))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|(), _| {
|
|
||||||
if failure == "ack" {
|
|
||||||
Err(std::io::Error::other("injected acknowledgment failure"))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(!marker.exists());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
fn inherited_barrier_refuses_eof_partial_and_malformed_release() {
|
fn run_acquisition(arguments: &[OsString]) -> std::io::Result<()> {
|
||||||
|
let scenario = arguments
|
||||||
|
.first()
|
||||||
|
.and_then(|argument| argument.to_str())
|
||||||
|
.ok_or_else(|| std::io::Error::other("scenario is missing"))?;
|
||||||
|
let marker = arguments
|
||||||
|
.get(1)
|
||||||
|
.ok_or_else(|| std::io::Error::other("marker is missing"))?;
|
||||||
|
let parent = current_process_identity()?;
|
||||||
|
let target = TargetCommand::new("/usr/bin/touch").argument(marker);
|
||||||
|
let expected = std::cell::Cell::new(None);
|
||||||
|
let stage_count = std::cell::Cell::new(0_u8);
|
||||||
|
let acknowledgment_count = std::cell::Cell::new(0_u8);
|
||||||
|
let process = child_broker().acquire(
|
||||||
|
parent,
|
||||||
|
context(),
|
||||||
|
&target,
|
||||||
|
|staged| {
|
||||||
|
stage_count.set(stage_count.get() + 1);
|
||||||
|
if scenario == "stage-fail" {
|
||||||
|
return Err(std::io::Error::other("injected stage failure"));
|
||||||
|
}
|
||||||
|
expected.set(Some(staged));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|staged| {
|
||||||
|
acknowledgment_count.set(acknowledgment_count.get() + 1);
|
||||||
|
if scenario == "ack-loss" {
|
||||||
|
return Err(std::io::Error::other("injected acknowledgment loss"));
|
||||||
|
}
|
||||||
|
let committed = mismatched_acknowledgment(scenario, staged);
|
||||||
|
if expected.get() != Some(staged) {
|
||||||
|
return Err(std::io::Error::other("staged ownership changed"));
|
||||||
|
}
|
||||||
|
Ok(ProcessOwnershipCommitAck::v1(committed))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
if stage_count.get() != 1 || acknowledgment_count.get() != 1 {
|
||||||
|
return Err(std::io::Error::other(
|
||||||
|
"stage and acknowledgment were not consumed exactly once",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
child_broker().reap(process)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mismatched_acknowledgment(
|
||||||
|
scenario: &str,
|
||||||
|
staged: StagedProcessOwnership,
|
||||||
|
) -> StagedProcessOwnership {
|
||||||
|
match scenario {
|
||||||
|
"ack-domain-mismatch" => StagedProcessOwnership::v1(
|
||||||
|
DomainId::new([9; 32]),
|
||||||
|
staged.generation_id(),
|
||||||
|
staged.coordinator_id(),
|
||||||
|
staged.slot(),
|
||||||
|
staged.identity(),
|
||||||
|
),
|
||||||
|
"ack-generation-mismatch" => StagedProcessOwnership::v1(
|
||||||
|
staged.domain_id(),
|
||||||
|
GenerationId::new(staged.generation_id().get() + 1),
|
||||||
|
staged.coordinator_id(),
|
||||||
|
staged.slot(),
|
||||||
|
staged.identity(),
|
||||||
|
),
|
||||||
|
"ack-coordinator-mismatch" => StagedProcessOwnership::v1(
|
||||||
|
staged.domain_id(),
|
||||||
|
staged.generation_id(),
|
||||||
|
CoordinatorId::new([9; 32]),
|
||||||
|
staged.slot(),
|
||||||
|
staged.identity(),
|
||||||
|
),
|
||||||
|
"ack-slot-mismatch" => StagedProcessOwnership::v1(
|
||||||
|
staged.domain_id(),
|
||||||
|
staged.generation_id(),
|
||||||
|
staged.coordinator_id(),
|
||||||
|
HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||||
|
staged.identity(),
|
||||||
|
),
|
||||||
|
"ack-identity-mismatch" => {
|
||||||
|
let identity = staged.identity();
|
||||||
|
StagedProcessOwnership::v1(
|
||||||
|
staged.domain_id(),
|
||||||
|
staged.generation_id(),
|
||||||
|
staged.coordinator_id(),
|
||||||
|
staged.slot(),
|
||||||
|
PidfsProcessIdentityV1::new(
|
||||||
|
identity.authority(),
|
||||||
|
HostPidLocator::new(identity.host_pid().get() + 1),
|
||||||
|
identity.start_time_ticks(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => staged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn successful_acquisition() -> std::io::Result<()> {
|
||||||
|
let marker = marker("success");
|
||||||
|
let status = std::process::Command::new(std::env::current_exe()?)
|
||||||
|
.arg("--run-acquisition")
|
||||||
|
.arg("success")
|
||||||
|
.arg(&marker)
|
||||||
|
.status()?;
|
||||||
|
if !status.success() || !std::fs::read(&marker)?.is_empty() {
|
||||||
|
return Err(std::io::Error::other(
|
||||||
|
"successful target did not run exactly once",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
std::fs::remove_file(marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn failed_stages_and_acknowledgments() -> std::io::Result<()> {
|
||||||
|
for scenario in [
|
||||||
|
"stage-fail",
|
||||||
|
"ack-loss",
|
||||||
|
"ack-domain-mismatch",
|
||||||
|
"ack-generation-mismatch",
|
||||||
|
"ack-coordinator-mismatch",
|
||||||
|
"ack-slot-mismatch",
|
||||||
|
"ack-identity-mismatch",
|
||||||
|
] {
|
||||||
|
let marker = marker(scenario);
|
||||||
|
let status = std::process::Command::new(std::env::current_exe()?)
|
||||||
|
.arg("--run-acquisition")
|
||||||
|
.arg(scenario)
|
||||||
|
.arg(&marker)
|
||||||
|
.status()?;
|
||||||
|
if status.success() || marker.exists() {
|
||||||
|
return Err(std::io::Error::other(format!(
|
||||||
|
"{scenario} released target execution"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn production_wrapper_refuses_former_harness_path() -> std::io::Result<()> {
|
||||||
|
let marker = marker("former-harness");
|
||||||
|
let status = std::process::Command::new(production_wrapper())
|
||||||
|
.arg("--acquisition-harness")
|
||||||
|
.arg("success")
|
||||||
|
.arg(&marker)
|
||||||
|
.status()?;
|
||||||
|
if status.success() || marker.exists() {
|
||||||
|
Err(std::io::Error::other(
|
||||||
|
"production wrapper accepted former test harness path",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn malformed_barriers_refuse() -> std::io::Result<()> {
|
||||||
for bytes in [
|
for bytes in [
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
b"partial".to_vec(),
|
b"partial".to_vec(),
|
||||||
|
|
@ -104,78 +237,115 @@ fn inherited_barrier_refuses_eof_partial_and_malformed_release() {
|
||||||
b"LAZARET-CAUSAL-BARRIER-RELEASE!!x".to_vec(),
|
b"LAZARET-CAUSAL-BARRIER-RELEASE!!x".to_vec(),
|
||||||
] {
|
] {
|
||||||
let marker = marker("barrier-refusal");
|
let marker = marker("barrier-refusal");
|
||||||
let parent = current_process_identity().unwrap();
|
let parent = current_process_identity()?;
|
||||||
let mut command = std::process::Command::new(wrapper());
|
let mut command = std::process::Command::new(production_wrapper());
|
||||||
append_parent_identity(&mut command, parent);
|
|
||||||
command
|
command
|
||||||
.arg("--")
|
.arg("--lazaret-private-process-role")
|
||||||
.arg("/bin/sh")
|
.arg("launch-wrapper");
|
||||||
.arg("-c")
|
append_parent_identity(&mut command, parent);
|
||||||
.arg(format!("printf target > '{}'", marker.display()));
|
command.arg("--").arg("/usr/bin/touch").arg(&marker);
|
||||||
let mut input = tempfile_file(&bytes);
|
command.stdin(Stdio::from(temporary_file(&bytes)?));
|
||||||
input.flush().unwrap();
|
if command.status()?.success() || marker.exists() {
|
||||||
command.stdin(Stdio::from(input));
|
return Err(std::io::Error::other("malformed barrier released target"));
|
||||||
assert!(!command.status().unwrap().success());
|
}
|
||||||
assert!(!marker.exists());
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tempfile_file(bytes: &[u8]) -> File {
|
fn temporary_file(bytes: &[u8]) -> std::io::Result<File> {
|
||||||
let path = marker("barrier-input");
|
let path = marker("barrier-input");
|
||||||
let mut file = File::options()
|
let mut file = File::options()
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&path)
|
.open(&path)?;
|
||||||
.unwrap();
|
file.write_all(bytes)?;
|
||||||
file.write_all(bytes).unwrap();
|
file.rewind()?;
|
||||||
file.rewind().unwrap();
|
std::fs::remove_file(path)?;
|
||||||
std::fs::remove_file(path).unwrap();
|
Ok(file)
|
||||||
file
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
fn sa_nocldwait_refuses() -> std::io::Result<()> {
|
||||||
|
let marker = marker("sa-nocldwait");
|
||||||
|
let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests")
|
||||||
|
.join("sa_nocldwait.py");
|
||||||
|
let output = std::process::Command::new("uv")
|
||||||
|
.args([
|
||||||
|
"--cache-dir",
|
||||||
|
"/tmp/lazaret-uv-cache",
|
||||||
|
"run",
|
||||||
|
"--no-project",
|
||||||
|
"python",
|
||||||
|
])
|
||||||
|
.arg(script)
|
||||||
|
.arg(std::env::current_exe()?)
|
||||||
|
.arg(&marker)
|
||||||
|
.output()?;
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
if output.status.success()
|
||||||
|
|| marker.exists()
|
||||||
|
|| !stderr.contains("SIGCHLD semantic probe was not reapable")
|
||||||
|
{
|
||||||
|
Err(std::io::Error::other("SA_NOCLDWAIT was accepted"))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
fn foreign_non_child_pidfd_is_refused_without_action() {
|
fn foreign_non_child_refuses() -> std::io::Result<()> {
|
||||||
let pidfd = pidfd_open(rustix::process::getpid(), PidfdFlags::empty()).unwrap();
|
let pidfd = pidfd_open(rustix::process::getpid(), PidfdFlags::empty())?;
|
||||||
assert!(verify_direct_child_pidfd(pidfd.as_fd()).is_err());
|
if verify_direct_child_pidfd(pidfd.as_fd()).is_err() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(std::io::Error::other(
|
||||||
|
"foreign non-child pidfd was accepted",
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[allow(clippy::disallowed_methods)]
|
||||||
fn process_global_broker_serializes_concurrent_acquisitions() {
|
fn concurrent_acquisitions_serialize() -> std::io::Result<()> {
|
||||||
|
let parent = current_process_identity()?;
|
||||||
let start = Arc::new(Barrier::new(9));
|
let start = Arc::new(Barrier::new(9));
|
||||||
let inside = Arc::new(AtomicU64::new(0));
|
let inside = Arc::new(AtomicUsize::new(0));
|
||||||
let maximum = Arc::new(AtomicU64::new(0));
|
let maximum = Arc::new(AtomicUsize::new(0));
|
||||||
let workers = (0..8)
|
let workers = (0..8)
|
||||||
.map(|_| {
|
.map(|_| {
|
||||||
let start = Arc::clone(&start);
|
let start = Arc::clone(&start);
|
||||||
let inside = Arc::clone(&inside);
|
let inside = Arc::clone(&inside);
|
||||||
let maximum = Arc::clone(&maximum);
|
let maximum = Arc::clone(&maximum);
|
||||||
thread::spawn(move || {
|
std::thread::spawn(move || -> std::io::Result<()> {
|
||||||
start.wait();
|
start.wait();
|
||||||
let parent = current_process_identity().unwrap();
|
let process = child_broker().acquire(
|
||||||
let process = child_broker()
|
parent,
|
||||||
.acquire(
|
context(),
|
||||||
wrapper(),
|
&TargetCommand::new("/bin/true"),
|
||||||
parent,
|
|staged| {
|
||||||
&TargetCommand::new("/bin/true"),
|
let active = inside.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|_| {
|
maximum.fetch_max(active, Ordering::SeqCst);
|
||||||
let active = inside.fetch_add(1, Ordering::SeqCst) + 1;
|
std::thread::yield_now();
|
||||||
maximum.fetch_max(active, Ordering::SeqCst);
|
inside.fetch_sub(1, Ordering::SeqCst);
|
||||||
thread::sleep(Duration::from_millis(5));
|
let _ = staged;
|
||||||
inside.fetch_sub(1, Ordering::SeqCst);
|
Ok(())
|
||||||
Ok(())
|
},
|
||||||
},
|
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||||
|(), _| Ok(()),
|
)?;
|
||||||
)
|
child_broker().reap(process)
|
||||||
.unwrap();
|
|
||||||
child_broker().reap(process).unwrap();
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
start.wait();
|
start.wait();
|
||||||
for worker in workers {
|
for worker in workers {
|
||||||
worker.join().unwrap();
|
worker
|
||||||
|
.join()
|
||||||
|
.map_err(|_| std::io::Error::other("worker panicked"))??;
|
||||||
|
}
|
||||||
|
if maximum.load(Ordering::SeqCst) == 1 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(std::io::Error::other("broker acquisition overlapped"))
|
||||||
}
|
}
|
||||||
assert_eq!(maximum.load(Ordering::SeqCst), 1);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ fn main() -> ExitCode {
|
||||||
if std::env::var_os("LAZARET_CAPABILITY_DROP_ISOLATED").is_none() {
|
if std::env::var_os("LAZARET_CAPABILITY_DROP_ISOLATED").is_none() {
|
||||||
let before = lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN)
|
let before = lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN)
|
||||||
.expect("inspect unprivileged capability state");
|
.expect("inspect unprivileged capability state");
|
||||||
if lazaret_linux::capability::irreversibly_drop(CapabilitySet::NET_ADMIN).is_ok()
|
if lazaret_linux::capability::irreversibly_drop(CapabilitySet::NET_ADMIN).is_err()
|
||||||
|| lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN).ok() != Some(before)
|
|| lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN).ok() != Some(before)
|
||||||
{
|
{
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
|
|
@ -19,10 +19,30 @@ fn main() -> ExitCode {
|
||||||
.args(["--user", "--map-root-user"])
|
.args(["--user", "--map-root-user"])
|
||||||
.arg(std::env::current_exe().expect("test executable path"))
|
.arg(std::env::current_exe().expect("test executable path"))
|
||||||
.env("LAZARET_CAPABILITY_DROP_ISOLATED", "1")
|
.env("LAZARET_CAPABILITY_DROP_ISOLATED", "1")
|
||||||
.status()
|
.output()
|
||||||
{
|
{
|
||||||
Ok(status) if status.success() => ExitCode::SUCCESS,
|
Ok(output) if output.status.success() => ExitCode::SUCCESS,
|
||||||
Ok(_) | Err(_) => ExitCode::FAILURE,
|
Ok(output)
|
||||||
|
if String::from_utf8_lossy(&output.stderr).contains("Operation not permitted") =>
|
||||||
|
{
|
||||||
|
eprintln!("SKIP isolated capability-positive branch: user namespaces unavailable");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
eprintln!(
|
||||||
|
"isolated capability-positive branch failed: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
eprintln!("SKIP isolated capability-positive branch: unshare is unavailable");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("failed to run isolated capability-positive branch: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
9
crates/lazaret-linux/tests/sa_nocldwait.c
Normal file
9
crates/lazaret-linux/tests/sa_nocldwait.c
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
__attribute__((constructor)) static void set_sa_nocldwait(void) {
|
||||||
|
struct sigaction action = {0};
|
||||||
|
action.sa_handler = SIG_DFL;
|
||||||
|
action.sa_flags = SA_NOCLDWAIT;
|
||||||
|
sigemptyset(&action.sa_mask);
|
||||||
|
sigaction(SIGCHLD, &action, 0);
|
||||||
|
}
|
||||||
21
crates/lazaret-linux/tests/sa_nocldwait.py
Normal file
21
crates/lazaret-linux/tests/sa_nocldwait.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
source = pathlib.Path(__file__).with_suffix(".c")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="lazaret-sa-nocldwait-") as directory:
|
||||||
|
preload = pathlib.Path(directory) / "sa_nocldwait.so"
|
||||||
|
subprocess.run(
|
||||||
|
["cc", "-shared", "-fPIC", "-o", preload, source],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["LD_PRELOAD"] = str(preload)
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.argv[1], "--run-acquisition", "success", sys.argv[2]],
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
raise SystemExit(result.returncode)
|
||||||
|
|
@ -269,7 +269,17 @@ class VerificationTests(unittest.TestCase):
|
||||||
"--all-features",
|
"--all-features",
|
||||||
"--locked",
|
"--locked",
|
||||||
"--filterset",
|
"--filterset",
|
||||||
"not binary(capability-drop-helper)",
|
"not binary(capability-drop-helper) and not binary(acquisition)",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"lazaret-linux",
|
||||||
|
"--test",
|
||||||
|
"acquisition",
|
||||||
|
"--locked",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"cargo",
|
"cargo",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ NEXTEST_COMMAND = (
|
||||||
"--all-features",
|
"--all-features",
|
||||||
"--locked",
|
"--locked",
|
||||||
"--filterset",
|
"--filterset",
|
||||||
"not binary(capability-drop-helper)",
|
"not binary(capability-drop-helper) and not binary(acquisition)",
|
||||||
)
|
)
|
||||||
DOCTEST_COMMAND = (
|
DOCTEST_COMMAND = (
|
||||||
"cargo",
|
"cargo",
|
||||||
|
|
@ -48,6 +48,16 @@ CAPABILITY_TEST_COMMAND = (
|
||||||
"capability-drop-helper",
|
"capability-drop-helper",
|
||||||
"--locked",
|
"--locked",
|
||||||
)
|
)
|
||||||
|
ACQUISITION_TEST_COMMAND = (
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"lazaret-linux",
|
||||||
|
"--test",
|
||||||
|
"acquisition",
|
||||||
|
"--locked",
|
||||||
|
)
|
||||||
REQUIRED_TOOL_VERSIONS = (
|
REQUIRED_TOOL_VERSIONS = (
|
||||||
("rustc", ("--version",), (0,), re.compile(r"^rustc 1\.90\.0 ")),
|
("rustc", ("--version",), (0,), re.compile(r"^rustc 1\.90\.0 ")),
|
||||||
("cargo", ("--version",), (0,), re.compile(r"^cargo 1\.90\.0 ")),
|
("cargo", ("--version",), (0,), re.compile(r"^cargo 1\.90\.0 ")),
|
||||||
|
|
@ -107,6 +117,10 @@ PROCESS_LINT_ALLOWANCES = {
|
||||||
"append_to",
|
"append_to",
|
||||||
"send_signal",
|
"send_signal",
|
||||||
"acquire",
|
"acquire",
|
||||||
|
"private_role_command",
|
||||||
|
"require_reapable_sigchld",
|
||||||
|
"cleanup_sigchld_probe",
|
||||||
|
"fail_sigchld_probe",
|
||||||
"reap",
|
"reap",
|
||||||
"acquire_spawned_child",
|
"acquire_spawned_child",
|
||||||
"verify_direct_child_pidfd",
|
"verify_direct_child_pidfd",
|
||||||
|
|
@ -118,8 +132,14 @@ PROCESS_LINT_ALLOWANCES = {
|
||||||
"exec_target",
|
"exec_target",
|
||||||
},
|
},
|
||||||
"crates/lazaret-linux/tests/acquisition.rs": {
|
"crates/lazaret-linux/tests/acquisition.rs": {
|
||||||
"inherited_barrier_refuses_eof_partial_and_malformed_release",
|
"run_acquisition",
|
||||||
"foreign_non_child_pidfd_is_refused_without_action",
|
"successful_acquisition",
|
||||||
|
"failed_stages_and_acknowledgments",
|
||||||
|
"production_wrapper_refuses_former_harness_path",
|
||||||
|
"malformed_barriers_refuse",
|
||||||
|
"sa_nocldwait_refuses",
|
||||||
|
"foreign_non_child_refuses",
|
||||||
|
"concurrent_acquisitions_serialize",
|
||||||
},
|
},
|
||||||
"crates/lazaret-linux/tests/capability-drop-helper.rs": {"main"},
|
"crates/lazaret-linux/tests/capability-drop-helper.rs": {"main"},
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +500,7 @@ def refresh_advisories() -> None:
|
||||||
|
|
||||||
def run_rust_tests() -> None:
|
def run_rust_tests() -> None:
|
||||||
run(list(NEXTEST_COMMAND))
|
run(list(NEXTEST_COMMAND))
|
||||||
|
run(list(ACQUISITION_TEST_COMMAND))
|
||||||
run(list(CAPABILITY_TEST_COMMAND))
|
run(list(CAPABILITY_TEST_COMMAND))
|
||||||
run(list(DOCTEST_COMMAND))
|
run(list(DOCTEST_COMMAND))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue