Add fail-closed Linux process and descriptor primitives

This commit is contained in:
ms2k 2026-07-14 22:08:29 +09:00
commit 1468f6b841

View file

@ -0,0 +1,181 @@
use std::fs::File;
use std::io::{Seek, Write};
use std::os::fd::AsFd;
use std::process::Stdio;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
use lazaret_linux::process::{
TargetCommand, append_parent_identity, child_broker, current_process_identity,
verify_direct_child_pidfd,
};
use rustix::process::{PidfdFlags, pidfd_open};
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
fn marker(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"lazaret-acquisition-{name}-{}-{}",
std::process::id(),
SEQUENCE.fetch_add(1, Ordering::Relaxed)
))
}
fn target(path: &std::path::Path) -> TargetCommand {
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"))
}
#[test]
fn target_runs_only_after_exact_stage_ack_and_release() {
let marker = marker("released");
let parent = current_process_identity().unwrap();
let process = child_broker()
.acquire(
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)]
fn inherited_barrier_refuses_eof_partial_and_malformed_release() {
for bytes in [
Vec::new(),
b"partial".to_vec(),
vec![b'x'; 32],
b"LAZARET-CAUSAL-BARRIER-RELEASE!!x".to_vec(),
] {
let marker = marker("barrier-refusal");
let parent = current_process_identity().unwrap();
let mut command = std::process::Command::new(wrapper());
append_parent_identity(&mut command, parent);
command
.arg("--")
.arg("/bin/sh")
.arg("-c")
.arg(format!("printf target > '{}'", marker.display()));
let mut input = tempfile_file(&bytes);
input.flush().unwrap();
command.stdin(Stdio::from(input));
assert!(!command.status().unwrap().success());
assert!(!marker.exists());
}
}
fn tempfile_file(bytes: &[u8]) -> File {
let path = marker("barrier-input");
let mut file = File::options()
.create_new(true)
.read(true)
.write(true)
.open(&path)
.unwrap();
file.write_all(bytes).unwrap();
file.rewind().unwrap();
std::fs::remove_file(path).unwrap();
file
}
#[test]
#[allow(clippy::disallowed_methods)]
fn foreign_non_child_pidfd_is_refused_without_action() {
let pidfd = pidfd_open(rustix::process::getpid(), PidfdFlags::empty()).unwrap();
assert!(verify_direct_child_pidfd(pidfd.as_fd()).is_err());
}
#[test]
fn process_global_broker_serializes_concurrent_acquisitions() {
let start = Arc::new(Barrier::new(9));
let inside = Arc::new(AtomicU64::new(0));
let maximum = Arc::new(AtomicU64::new(0));
let workers = (0..8)
.map(|_| {
let start = Arc::clone(&start);
let inside = Arc::clone(&inside);
let maximum = Arc::clone(&maximum);
thread::spawn(move || {
start.wait();
let parent = current_process_identity().unwrap();
let process = child_broker()
.acquire(
wrapper(),
parent,
&TargetCommand::new("/bin/true"),
|_| {
let active = inside.fetch_add(1, Ordering::SeqCst) + 1;
maximum.fetch_max(active, Ordering::SeqCst);
thread::sleep(Duration::from_millis(5));
inside.fetch_sub(1, Ordering::SeqCst);
Ok(())
},
|(), _| Ok(()),
)
.unwrap();
child_broker().reap(process).unwrap();
})
})
.collect::<Vec<_>>();
start.wait();
for worker in workers {
worker.join().unwrap();
}
assert_eq!(maximum.load(Ordering::SeqCst), 1);
}