Add fail-closed Linux process and descriptor primitives
This commit is contained in:
parent
b34421efa1
commit
1468f6b841
2 changed files with 1725 additions and 15 deletions
181
crates/lazaret-linux/tests/acquisition.rs
Normal file
181
crates/lazaret-linux/tests/acquisition.rs
Normal 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);
|
||||
}
|
||||
54
crates/lazaret-linux/tests/capability-drop-helper.rs
Normal file
54
crates/lazaret-linux/tests/capability-drop-helper.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::process::ExitCode;
|
||||
|
||||
use rustix::thread::{
|
||||
CapabilitySet, capabilities, capability_is_in_ambient_set, configure_capability_in_ambient_set,
|
||||
set_capabilities,
|
||||
};
|
||||
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
fn main() -> ExitCode {
|
||||
if std::env::var_os("LAZARET_CAPABILITY_DROP_ISOLATED").is_none() {
|
||||
let before = lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN)
|
||||
.expect("inspect unprivileged capability state");
|
||||
if lazaret_linux::capability::irreversibly_drop(CapabilitySet::NET_ADMIN).is_ok()
|
||||
|| lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN).ok() != Some(before)
|
||||
{
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
return match std::process::Command::new("unshare")
|
||||
.args(["--user", "--map-root-user"])
|
||||
.arg(std::env::current_exe().expect("test executable path"))
|
||||
.env("LAZARET_CAPABILITY_DROP_ISOLATED", "1")
|
||||
.status()
|
||||
{
|
||||
Ok(status) if status.success() => ExitCode::SUCCESS,
|
||||
Ok(_) | Err(_) => ExitCode::FAILURE,
|
||||
};
|
||||
}
|
||||
|
||||
let capability = CapabilitySet::NET_ADMIN;
|
||||
let retained = CapabilitySet::CHOWN;
|
||||
let mut sets = capabilities(None).expect("inspect isolated capability sets");
|
||||
sets.inheritable.insert(retained);
|
||||
set_capabilities(None, sets).expect("prepare unrelated ambient capability");
|
||||
configure_capability_in_ambient_set(retained, true)
|
||||
.expect("raise unrelated ambient capability");
|
||||
if lazaret_linux::capability::irreversibly_drop(capability).is_err() {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let Ok(observed) = lazaret_linux::capability::inspect(capability) else {
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
if observed.sets.effective.intersects(capability)
|
||||
|| observed.sets.permitted.intersects(capability)
|
||||
|| observed.sets.inheritable.intersects(capability)
|
||||
|| observed.ambient
|
||||
|| observed.bounding
|
||||
|| !capability_is_in_ambient_set(retained).unwrap_or(false)
|
||||
|| configure_capability_in_ambient_set(capability, true).is_ok()
|
||||
{
|
||||
ExitCode::FAILURE
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue