feat(sandbox): reap namespace descendants and cancel via init lifecycle

This commit is contained in:
2026-10-07 22:28:13 +08:00
parent 03eaab719e
commit 89da9ffd9f
8 changed files with 673 additions and 2 deletions
+20
View File
@@ -396,3 +396,23 @@ socketpair、ptrace 和 dumpability 可用,再安装真实生产策略验证
TMPDIR 指向私有 `/tmp`,native 不带 PATH。请求仍不能指定 PATH、TMPDIR、HOME
或 loader 覆盖;最终 exec 必须先清空环境、进入私有根,再应用这份环境。VM 语言
探针使用相同固定编译 PATH,仅将 TMPDIR 显式指向其有界测试工作区。
### Namespace 进程树回收原语
私有 `isolation/process_tree.rs` 为单线程、已降权/过滤的 namespace PID 1 定义回收
角色。PID 1 保留运行,主程序在独立子进程 exec;主进程 pidfd + waitid 与 wait-any
配合,处理主进程退出和孤儿后代收养的竞态,只有 ECHILD 才确认树为空。取消、控制
通道失联或监控异常都使 PID 1 退出,由内核终止该 namespace 的所有剩余进程;外层
监督器仍必须检查 cgroup 为空并回收 bootstrap,不能把主进程退出当作整个任务结束。
exec 参数与固定环境在 fork 前分配,fork 前重复验证单线程/PID 1/凭据/过滤器与
SIGCHLD wait 所有权。受审计的 unsafe 仅在沙箱内部:子进程用 async-signal-safe
系统操作绑定 FD 0/1/2、关闭其他 FD、设置 cwd/SIGPIPE,随后 execve 或 _exit。
FD 3 仅为 CLOEXEC 的 exec errno 管道;错误管道 EOF 只表示没有收到 exec 错误,
单凭 EOF 不能证明程序已经 exec(例如放行前被终止)。普通信号/pidfd_send_signal/
pidfd_getfd 仍拒绝,仅增加可见进程的只读 pidfd_open 以支持等待。
`tree_vm.rs`/固定 `tree_probe` 在受限独立 VM 实际验证正常主进程、孤儿收养/回收、
主进程退出仍有后代时取消、控制端断开及真实 exec ENOENT。探针使用与生产相同的
私有回收/凭据/seccomp 原语,在新 PID namespace 的 PID 1 运行;它不是完整任务
执行 API。CLI run 继续拒绝,stdio/限额/计量与完整监督全部验收前不暴露执行放行。
+3 -2
View File
@@ -10,10 +10,11 @@ kf-contracts.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
rustix = { version = "1.1.5", features = ["net", "process", "fs", "thread", "mount", "system", "stdio"] }
rustix = { version = "1.1.5", features = ["net", "process", "fs", "thread", "mount", "system", "stdio", "pipe", "event"] }
# Fixed BPF compilation/installation through safe APIs; libc supplies syscall
# numbers only. Both dependencies stay inside the OS sandbox boundary.
# numbers and the audited fork/exec primitive. Both dependencies stay inside
# the OS sandbox boundary.
seccompiler = { version = "=0.5.0", default-features = false }
libc = "=0.2.190"
+266
View File
@@ -0,0 +1,266 @@
//! Fixed guarded VM role using the exact private production reaper/reducer.
//! No production CLI user-exec gate or arbitrary fixture argv is added.
mod launcher {
pub mod entry {
pub use kf_sandbox::launcher::entry::ensure_single_threaded;
}
}
mod outcome {
pub use kf_sandbox::outcome::Termination;
}
mod spec {
pub use kf_sandbox::spec::{ExecutionProfile, ValidatedSpec};
}
#[allow(dead_code)]
#[path = "../src/isolation/identity.rs"]
pub(crate) mod identity;
#[allow(dead_code)]
#[path = "../src/isolation/namespaces.rs"]
mod namespaces;
#[allow(dead_code)]
#[path = "../src/isolation/process_tree.rs"]
mod process_tree;
#[allow(dead_code)]
#[path = "../src/isolation/seccomp.rs"]
pub(crate) mod seccomp;
mod isolation {
pub(crate) use crate::{identity, seccomp};
}
#[allow(dead_code)]
#[path = "../tests/support/mod.rs"]
mod support;
use rustix::fd::{AsRawFd, OwnedFd};
use std::{
io::{Read, Write},
os::unix::net::UnixStream,
process::{Command, Stdio},
time::{Duration, Instant},
};
fn init(mode: &str) {
assert_eq!(rustix::process::getpid(), rustix::process::Pid::INIT);
rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::KILL)).unwrap();
let mut control =
UnixStream::from(rustix::io::fcntl_dupfd_cloexec(rustix::stdio::stdin(), 3).unwrap());
assert_eq!(control.as_raw_fd(), 3);
control
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
control
.set_write_timeout(Some(Duration::from_secs(1)))
.unwrap();
// Original proc still shows outer PIDs. The fixed fresh PID 1 must replace
// it before single-threaded/proc-based preconditions and credential checks.
rustix::mount::mount(
"proc",
"/proc",
"proc",
rustix::mount::MountFlags::NOSUID
| rustix::mount::MountFlags::NODEV
| rustix::mount::MountFlags::NOEXEC,
Some(c"hidepid=2,subset=pid"),
)
.unwrap();
let (input, in_write) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC).unwrap();
let (out_read, output) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC).unwrap();
let (err_read, error) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC).unwrap();
drop(in_write); // fixed empty stdin / EOF
let mut request = support::spec("tree-fixture", 64 << 20, 8).spec().clone();
request.argv = vec!["/work/main".into(), mode.into()];
if mode == "exec-fail" {
request.argv[0] = "/work/missing".into();
request.inputs[0].path = "missing".into();
}
let plan = process_tree::ExecPlan::new(
&request
.validate(&kf_sandbox::spec::SpecPolicy::default())
.unwrap(),
)
.unwrap();
identity::reduce_current(100000, 100000).unwrap();
seccomp::install(spec::ExecutionProfile::Native).unwrap();
let mut reaper = process_tree::Reaper::spawn(
plan,
process_tree::ChildStdio {
input,
output,
error,
},
)
.unwrap();
control.write_all(b"READY\n").unwrap();
let mut main_reported = false;
let deadline = Instant::now() + Duration::from_secs(3);
loop {
let observed = reaper.poll().unwrap();
if let Some(status) = observed.main.as_ref().filter(|_| !main_reported) {
assert_eq!(
*status,
outcome::Termination::Exited {
code: if mode == "exec-fail" { 126 } else { 7 }
}
);
if mode == "exec-fail" {
assert_eq!(
observed.exec,
process_tree::ExecObservation::Failed { errno: 2 }
);
}
control.write_all(b"MAIN\n").unwrap();
main_reported = true;
}
if observed.empty {
assert!(main_reported);
if mode == "orphan-short" {
assert_eq!(observed.reaped_descendants, 1);
}
let mut out = std::fs::File::from(out_read);
let mut err = std::fs::File::from(err_read);
let mut output = Vec::new();
let mut error = Vec::new();
out.read_to_end(&mut output).unwrap();
err.read_to_end(&mut error).unwrap();
if mode != "exec-fail" {
assert!(String::from_utf8_lossy(&output).contains("KF_TREE_USER_PROOF"));
}
if mode == "orphan-short" {
assert!(String::from_utf8_lossy(&error).contains("KF_ORPHAN_ADOPTED_PROOF"));
}
control.write_all(b"DRAINED\n").unwrap();
reaper.shutdown(process_tree::NamespaceEnd::Drained);
}
let mut poll = [rustix::event::PollFd::new(
&control,
rustix::event::PollFlags::IN,
)];
rustix::event::poll(
&mut poll,
Some(&rustix::event::Timespec {
tv_sec: 0,
tv_nsec: 2_000_000,
}),
)
.unwrap();
if !poll[0].revents().is_empty() {
let mut byte = [0];
// The fixed init uses the admitted read syscall; std UnixStream::read
// uses recvfrom, which is deliberately outside this policy.
match rustix::io::read(&control, &mut byte) {
Ok(0) => reaper.shutdown(process_tree::NamespaceEnd::ControlLost),
Ok(1) if byte[0] == b'C' => {
control.write_all(b"CANCELLED\n").unwrap();
reaper.shutdown(process_tree::NamespaceEnd::Cancelled);
}
_ => reaper.shutdown(process_tree::NamespaceEnd::MonitorFailed),
}
}
if Instant::now() >= deadline {
reaper.shutdown(process_tree::NamespaceEnd::MonitorFailed);
}
}
}
fn line(control: &mut UnixStream) -> String {
let mut bytes = Vec::new();
loop {
let mut b = [0];
control.read_exact(&mut b).unwrap();
if b[0] == b'\n' {
break;
}
bytes.push(b[0]);
assert!(bytes.len() < 32);
}
String::from_utf8(bytes).unwrap()
}
fn main() {
support::require_vm();
let args: Vec<_> = std::env::args().collect();
assert_eq!(args.len(), 3);
assert!(
[
"normal",
"orphan-short",
"orphan-cancel",
"orphan-disconnect",
"exec-fail"
]
.contains(&args[2].as_str())
);
if args[1] == "init" {
init(&args[2]);
return;
}
assert_eq!(args[1], "fixed");
launcher::entry::ensure_single_threaded().unwrap();
let (mut parent, child) = UnixStream::pair().unwrap();
parent
.set_read_timeout(Some(Duration::from_secs(4)))
.unwrap();
parent
.set_write_timeout(Some(Duration::from_secs(1)))
.unwrap();
let own = std::env::current_exe().unwrap();
namespaces::create().unwrap();
let mut process = Command::new(own)
.args(["init", &args[2]])
.env_clear()
.env("KF_SANDBOX_INDEPENDENT_VM", "1")
.stdin(Stdio::from(OwnedFd::from(child)))
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let pidfd = rustix::process::pidfd_open(
rustix::process::Pid::from_raw(process.id() as i32).unwrap(),
rustix::process::PidfdFlags::empty(),
)
.unwrap();
assert_eq!(line(&mut parent), "READY");
assert_eq!(line(&mut parent), "MAIN");
if args[2] == "orphan-cancel" || args[2] == "orphan-disconnect" {
assert!(
process.try_wait().unwrap().is_none(),
"main exit abandoned descendants"
);
if args[2] == "orphan-cancel" {
parent.write_all(b"C").unwrap();
assert_eq!(line(&mut parent), "CANCELLED");
}
drop(parent);
} else {
assert_eq!(line(&mut parent), "DRAINED");
drop(parent);
}
let deadline = Instant::now() + Duration::from_secs(4);
let status = loop {
if let Some(s) = process.try_wait().unwrap() {
break s;
}
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(2));
};
if args[2] == "orphan-disconnect" {
assert_eq!(status.code(), Some(125));
} else {
assert!(status.success(), "{status}");
}
let mut poll = [rustix::event::PollFd::new(
&pidfd,
rustix::event::PollFlags::IN,
)];
assert_eq!(
rustix::event::poll(
&mut poll,
Some(&rustix::event::Timespec {
tv_sec: 0,
tv_nsec: 0
})
)
.unwrap(),
1
);
println!(
"KF_TREE_PROOF mode={} pid1_reaper=1 main_pidfd=1 main_status_verified=1 init_pidfd_ready=1 namespace_cleanup=1",
args[2]
);
}
@@ -6,3 +6,6 @@ pub mod namespaces;
pub(crate) mod proc_dev;
pub(crate) mod root;
pub(crate) mod seccomp;
// No public exec gate until resource supervision and lifecycle acceptance.
#[allow(dead_code)]
pub(crate) mod process_tree;
@@ -0,0 +1,321 @@
//! Single-threaded namespace PID 1. Private until all execution gates are ready.
//! The init never execs the user program: it owns a pidfd and reaps orphaned
//! descendants. Exiting init is the namespace-wide cancellation mechanism.
use crate::{outcome::Termination, spec::ValidatedSpec};
use rustix::{
fd::{AsFd, AsRawFd, OwnedFd},
process::{Pid, PidfdFlags, WaitId, WaitIdOptions, WaitOptions, WaitStatus},
};
use std::{ffi::CString, io};
fn denied(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::PermissionDenied, message)
}
pub(crate) struct ChildStdio {
pub input: OwnedFd,
pub output: OwnedFd,
pub error: OwnedFd,
}
impl ChildStdio {
pub(crate) fn validate(&self) -> io::Result<()> {
for (fd, access) in [
(&self.input, rustix::fs::OFlags::RDONLY),
(&self.output, rustix::fs::OFlags::WRONLY),
(&self.error, rustix::fs::OFlags::WRONLY),
] {
let s = rustix::fs::fstat(fd)?;
if fd.as_raw_fd() < 4
|| s.st_mode & libc::S_IFMT != libc::S_IFIFO
|| rustix::fs::fcntl_getfl(fd)?.intersection(rustix::fs::OFlags::ACCMODE) != access
|| !rustix::io::fcntl_getfd(fd)?.contains(rustix::io::FdFlags::CLOEXEC)
{
return Err(denied("stdio requires owned CLOEXEC pipe ends"));
}
}
Ok(())
}
}
/// Allocate/validate strings before fork. No request shell or host environment.
pub(crate) struct ExecPlan {
argv: Vec<CString>,
env: Vec<CString>,
cwd: CString,
}
impl ExecPlan {
pub(crate) fn new(spec: &ValidatedSpec) -> io::Result<Self> {
let string = |s: String| CString::new(s).map_err(|_| denied("exec string"));
Ok(Self {
argv: spec
.spec()
.argv
.iter()
.cloned()
.map(string)
.collect::<io::Result<_>>()?,
env: spec
.execution_environment()
.into_iter()
.map(|(k, v)| string(format!("{k}={v}")))
.collect::<io::Result<_>>()?,
cwd: string(if spec.spec().cwd == "." {
"/work".into()
} else {
format!("/work/{}", spec.spec().cwd)
})?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ExecObservation {
Pending,
NoErrorObserved,
Failed { errno: u16 },
}
#[derive(Clone, Debug)]
pub(crate) struct TreeObservation {
pub main: Option<Termination>,
pub exec: ExecObservation,
pub reaped_descendants: u32,
pub empty: bool,
}
pub(crate) enum NamespaceEnd {
Drained,
Cancelled,
ControlLost,
MonitorFailed,
}
pub(crate) struct Reaper {
pid: Pid,
pidfd: OwnedFd,
exec_error: OwnedFd,
errno_bytes: [u8; 4],
errno_len: usize,
observation: TreeObservation,
}
impl Reaper {
pub(crate) fn spawn(plan: ExecPlan, stdio: ChildStdio) -> io::Result<Self> {
crate::launcher::entry::ensure_single_threaded()?;
if rustix::process::getpid() != Pid::INIT {
return Err(denied("reaper requires namespace PID 1"));
}
let uid = rustix::process::getuid().as_raw();
let gid = rustix::process::getgid().as_raw();
let status = std::fs::read_to_string("/proc/self/status")?;
if !crate::isolation::identity::valid(uid, gid) {
return Err(denied("reaper identity"));
}
crate::isolation::identity::verify_status(&status, uid, gid)?;
crate::isolation::seccomp::filter_count(&status)?;
// PID ownership requires no inherited SIGCHLD auto-reap disposition.
check_sigchld()?;
stdio.validate()?;
let (read, write) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)?;
rustix::fs::fcntl_setfl(&read, rustix::fs::OFlags::NONBLOCK)?;
let pid = fork_exec(&plan, &stdio, &write)?;
drop(write);
drop(stdio); // Init must not keep users' pipe ends open.
let pidfd = match rustix::process::pidfd_open(pid, PidfdFlags::empty()) {
Ok(fd) => fd,
Err(_) => std::process::exit(125), // PID 1 exit kills the spawned tree.
};
Ok(Self {
pid,
pidfd,
exec_error: read,
errno_bytes: [0; 4],
errno_len: 0,
observation: TreeObservation {
main: None,
exec: ExecObservation::Pending,
reaped_descendants: 0,
empty: false,
},
})
}
pub(crate) fn poll(&mut self) -> io::Result<TreeObservation> {
self.poll_exec()?;
if self.observation.main.is_none() {
match rustix::process::waitid(
WaitId::PidFd(self.pidfd.as_fd()),
WaitIdOptions::EXITED | WaitIdOptions::NOHANG,
) {
Ok(Some(s)) => {
self.observation.main = Some(if let Some(code) = s.exit_status() {
Termination::Exited { code: code as u8 }
} else if let Some(signal) = s.terminating_signal() {
Termination::Signaled {
signal: signal as u8,
core_dumped: s.dumped(),
}
} else {
return Err(denied("unexpected main waitid state"));
});
}
Ok(None) | Err(rustix::io::Errno::INTR) => {}
Err(e) => return Err(e.into()),
}
}
// At most the validated pids ceiling per tick; don't starve control/I/O.
for _ in 0..128 {
match rustix::process::wait(WaitOptions::NOHANG) {
Ok(Some((pid, status))) if pid == self.pid => {
// Main exited between pidfd polling and wait-any. Record
// its actual status, never mistake it for a descendant.
if self.observation.main.is_some() {
return Err(denied("main reaped twice"));
}
self.observation.main = Some(termination(status)?);
}
Ok(Some(_)) => {
self.observation.reaped_descendants = self
.observation
.reaped_descendants
.checked_add(1)
.ok_or_else(|| denied("reap counter overflow"))?
}
Ok(None) => break,
Err(rustix::io::Errno::INTR) => continue,
Err(rustix::io::Errno::CHILD) => {
if self.observation.main.is_none() {
return Err(denied("main wait ownership lost"));
}
self.observation.empty = true;
break;
}
Err(e) => return Err(e.into()),
}
}
if self.observation.main.is_some() {
self.poll_exec()?;
}
Ok(self.observation.clone())
}
fn poll_exec(&mut self) -> io::Result<()> {
if self.observation.exec != ExecObservation::Pending {
return Ok(());
}
loop {
match rustix::io::read(&self.exec_error, &mut self.errno_bytes[self.errno_len..]) {
Ok(0) if self.errno_len == 0 => {
self.observation.exec = ExecObservation::NoErrorObserved;
return Ok(());
}
Ok(0) => return Err(denied("partial exec error")),
Ok(n) => {
self.errno_len += n;
if self.errno_len == 4 {
let errno = i32::from_ne_bytes(self.errno_bytes);
let errno = u16::try_from(errno)
.ok()
.filter(|n| *n > 0)
.ok_or_else(|| denied("exec errno"))?;
self.observation.exec = ExecObservation::Failed { errno };
return Ok(());
}
}
Err(rustix::io::Errno::AGAIN) => return Ok(()),
Err(rustix::io::Errno::INTR) => continue,
Err(e) => return Err(e.into()),
}
}
}
pub(crate) fn shutdown(self, reason: NamespaceEnd) -> ! {
let code = match reason {
NamespaceEnd::Drained | NamespaceEnd::Cancelled => 0,
NamespaceEnd::ControlLost | NamespaceEnd::MonitorFailed => 125,
};
// Constructor proved PID 1. Kernel SIGKILLs and waits for the entire
// namespace; the outside supervisor must still verify cgroup empty.
std::process::exit(code)
}
}
impl Drop for Reaper {
fn drop(&mut self) {
// Monitor panic/error cannot abandon a live descendant namespace.
std::process::exit(125);
}
}
fn termination(s: WaitStatus) -> io::Result<Termination> {
if let Some(code) = s.exit_status() {
Ok(Termination::Exited { code: code as u8 })
} else if let Some(signal) = s.terminating_signal() {
Ok(Termination::Signaled {
signal: signal as u8,
core_dumped: s.as_raw() & 0x80 != 0,
})
} else {
Err(denied("unexpected descendant wait state"))
}
}
#[allow(unsafe_code)]
fn check_sigchld() -> io::Result<()> {
// SAFETY: sigaction query initializes the supplied C struct, no handler or
// global disposition is changed. Only the checked single-threaded init.
unsafe {
let mut s: libc::sigaction = std::mem::zeroed();
if libc::sigaction(libc::SIGCHLD, std::ptr::null(), &mut s) != 0 {
return Err(io::Error::last_os_error());
}
if s.sa_sigaction != libc::SIG_DFL || s.sa_flags & libc::SA_NOCLDWAIT != 0 {
return Err(denied("SIGCHLD must preserve wait ownership"));
}
}
Ok(())
}
#[allow(unsafe_code)]
fn fork_exec(plan: &ExecPlan, stdio: &ChildStdio, error: &OwnedFd) -> io::Result<Pid> {
let mut argv: Vec<_> = plan.argv.iter().map(|s| s.as_ptr()).collect();
argv.push(std::ptr::null());
let mut env: Vec<_> = plan.env.iter().map(|s| s.as_ptr()).collect();
env.push(std::ptr::null());
let sources = [
stdio.input.as_raw_fd(),
stdio.output.as_raw_fd(),
stdio.error.as_raw_fd(),
];
let err = error.as_raw_fd();
let program = plan.argv[0].as_ptr();
let cwd = plan.cwd.as_ptr();
if err < 4 {
return Err(denied("exec-error descriptor alias"));
}
// SAFETY: fresh, checked single-threaded PID 1, trusted image only, no user
// code/threads/atfork hooks loaded. All allocations/C strings/raw FD values
// prepared above and borrowed alive in both processes. Child uses only
// async-signal-safe libc syscalls and exec/_exit, never Rust allocation,
// panic, unwinding or descriptor destructors. fork copies, not shares, the
// FD table; only its copy is rewritten/closed. All source FDs are >=4.
unsafe {
let raw = libc::fork();
if raw < 0 {
return Err(io::Error::last_os_error());
}
if raw == 0 {
if libc::dup3(err, 3, libc::O_CLOEXEC) < 0 {
let errno = *libc::__errno_location();
let _ = libc::write(err, (&errno as *const i32).cast(), 4);
libc::_exit(126);
}
let setup = sources
.iter()
.enumerate()
.all(|(target, source)| libc::dup2(*source, target as i32) >= 0)
&& libc::syscall(libc::SYS_close_range, 4_u32, u32::MAX, 0_u32) == 0
&& libc::chdir(cwd) == 0
&& libc::signal(libc::SIGPIPE, libc::SIG_DFL) != libc::SIG_ERR;
if setup {
libc::execve(program, argv.as_ptr(), env.as_ptr());
}
let errno = *libc::__errno_location();
let _ = libc::write(3, (&errno as *const i32).cast(), 4);
libc::_exit(126);
}
Ok(Pid::from_raw(raw).expect("positive fork PID"))
}
}
@@ -137,6 +137,9 @@ pub(crate) fn compile(profile: ExecutionProfile) -> io::Result<BpfProgram> {
libc::SYS_fork,
libc::SYS_wait4,
libc::SYS_waitid,
// Holding/waiting on visible children is read-only; signalling via
// pidfds remains forbidden, as does pidfd_getfd.
libc::SYS_pidfd_open,
libc::SYS_execve,
libc::SYS_exit,
libc::SYS_exit_group,
+25
View File
@@ -0,0 +1,25 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>
static void require(int yes){if(!yes){fprintf(stderr,"tree user proof failure errno=%d\n",errno);_exit(2);}}
int main(int argc,char **argv){
require(argc==2&&getpid()>1&&getppid()==1&&getuid()==100000&&prctl(PR_GET_SECCOMP)==2);
DIR *dir=opendir("/proc/self/fd");require(dir!=NULL);int scanner=dirfd(dir);struct dirent *e;
while((e=readdir(dir)))if(e->d_name[0]!='.'){int fd=atoi(e->d_name);require(fd<=2||fd==scanner);}
closedir(dir);char data;require(read(0,&data,1)==0);
puts("KF_TREE_USER_PROOF pid_gt1=1 parent_init=1 reduced=1 filtered=1 fd012_only=1 eof=1");fflush(stdout);
if(!strcmp(argv[1],"normal"))return 7;
pid_t child=fork();require(child>=0);
if(child)return 7;
if(!strcmp(argv[1],"orphan-short")){
for(unsigned i=0;i<100&&getppid()!=1;i++)usleep(1000);
require(getppid()==1);fputs("KF_ORPHAN_ADOPTED_PROOF\n",stderr);return 3;
}
for(;;)sleep(10);
}
+32
View File
@@ -0,0 +1,32 @@
#![cfg(target_os = "linux")]
#[allow(dead_code)]
mod support;
use std::process::Command;
#[test]
#[ignore = "requires independent capped VM with fixed namespace role fixture"]
fn namespace_init_reaps_main_and_orphans_and_cancels_surviving_tree() {
support::require_vm();
for mode in [
"normal",
"orphan-short",
"orphan-cancel",
"orphan-disconnect",
"exec-fail",
] {
let output = Command::new("/kf/bin/tree-probe")
.args(["fixed", mode])
.env_clear()
.env("KF_SANDBOX_INDEPENDENT_VM", "1")
.output()
.unwrap();
assert!(
output.status.success(),
"{mode}: {:?} {:?}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let proof = String::from_utf8(output.stdout).unwrap();
assert!(proof.contains(&format!("KF_TREE_PROOF mode={mode}")));
print!("{proof}");
}
}