use std::io::Write;
use std::os::unix::io::RawFd;
use std::time::{Duration, Instant};
const LOG: &str = "/data/local/tmp/newpid_probe.log";
const SYS_CLONE3: libc::c_long = 435; const SYS_CLONE: libc::c_long = 220; const CLONE_NEWPID: u64 = 0x2000_0000;
const CLONE_NEWUSER: u64 = 0x1000_0000;
const M_BASELINE: i32 = 0;
const M_NS_KILL: i32 = 1;
const M_PTY: i32 = 2;
const M_DRAIN: i32 = 3;
const M_SECCOMP: i32 = 4;
const M_REAP: i32 = 5;
const M_PG3A: i32 = 6;
const M_PG3B: i32 = 7;
const M_NSPROBE: i32 = 8;
#[repr(C)]
struct CloneArgs {
flags: u64,
pidfd: u64,
child_tid: u64,
parent_tid: u64,
exit_signal: u64,
stack: u64,
stack_size: u64,
tls: u64,
set_tid: u64,
set_tid_size: u64,
cgroup: u64,
}
static mut CHILD_REPORT_FD: RawFd = -1;
static mut CHILD_PARENT_PID: i32 = 0;
static mut CHILD_ARG1: i32 = 0;
static mut CHILD_MODE: i32 = M_BASELINE;
fn say(s: impl AsRef<str>) {
let s = s.as_ref();
println!("{s}");
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(LOG)
{
let _ = writeln!(f, "{s}");
}
}
fn errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1)
}
fn errstr() -> String {
format!(" errno={}", errno())
}
fn clone3_newpid() -> Result<libc::pid_t, i32> {
let args = CloneArgs {
flags: CLONE_NEWPID,
pidfd: 0,
child_tid: 0,
parent_tid: 0,
exit_signal: libc::SIGCHLD as u64,
stack: 0,
stack_size: 0,
tls: 0,
set_tid: 0,
set_tid_size: 0,
cgroup: 0,
};
let pid = unsafe {
libc::syscall(
SYS_CLONE3,
&args as *const CloneArgs as usize,
std::mem::size_of::<CloneArgs>() as usize,
)
};
if pid < 0 {
Err(errno())
} else {
Ok(pid as libc::pid_t)
}
}
fn clone_plain_newpid() -> Result<libc::pid_t, i32> {
let flags = (CLONE_NEWPID | libc::SIGCHLD as u64) as usize;
let pid = unsafe { libc::syscall(SYS_CLONE, flags, 0usize, 0usize, 0usize, 0usize) };
if pid < 0 {
Err(errno())
} else {
Ok(pid as libc::pid_t)
}
}
fn clone_plain() -> Result<libc::pid_t, i32> {
let pid = unsafe {
libc::syscall(
SYS_CLONE,
libc::SIGCHLD as usize,
0usize,
0usize,
0usize,
0usize,
)
};
if pid < 0 {
Err(errno())
} else {
Ok(pid as libc::pid_t)
}
}
fn spawn_child(
mode: i32,
parent_pid: i32,
arg1: i32,
newpid: bool,
) -> Result<(libc::pid_t, RawFd), String> {
let mut fds = [0 as RawFd; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return Err(format!("report pipe: errno={}", errno()));
}
let (report_r, report_w) = (fds[0], fds[1]);
unsafe {
CHILD_MODE = mode;
CHILD_PARENT_PID = parent_pid;
CHILD_ARG1 = arg1;
CHILD_REPORT_FD = report_w;
}
let pid = if newpid {
match clone3_newpid() {
Ok(p) => p,
Err(e) if e == libc::ENOSYS => match clone_plain_newpid() {
Ok(p) => p,
Err(e2) => {
unsafe {
libc::close(report_r);
libc::close(report_w);
}
return Err(format!(
"clone3(CLONE_NEWPID) errno={e}, fallback clone errno={e2}"
));
}
},
Err(e) => {
unsafe {
libc::close(report_r);
libc::close(report_w);
}
return Err(format!("clone3(CLONE_NEWPID) errno={e}"));
}
}
} else {
match clone_plain() {
Ok(p) => p,
Err(e) => {
unsafe {
libc::close(report_r);
libc::close(report_w);
}
return Err(format!("clone errno={e}"));
}
}
};
if pid == 0 {
unsafe { child_main() }
}
unsafe {
libc::close(report_w);
}
Ok((pid, report_r))
}
unsafe fn cwl(fd: RawFd, s: &str) {
let mut b = Vec::with_capacity(s.len() + 1);
b.extend_from_slice(s.as_bytes());
b.push(b'\n');
let _ = libc::write(fd, b.as_ptr() as *const libc::c_void, b.len());
}
unsafe fn cwi(fd: RawFd, label: &str, v: i64, extra: &str) {
let s = format!("{label}={v}{extra}");
cwl(fd, &s);
}
unsafe fn child_main() -> ! {
let fd = CHILD_REPORT_FD;
let pp = CHILD_PARENT_PID;
let a1 = CHILD_ARG1;
match CHILD_MODE {
M_BASELINE => child_baseline(fd, pp),
M_NS_KILL => child_ns_kill(fd, pp),
M_PTY => child_pty(fd, a1),
M_DRAIN => child_drain(fd, a1),
M_SECCOMP => child_seccomp(fd),
M_REAP => child_reap(fd),
M_PG3A => child_pg3(fd, "pg3a"),
M_PG3B => child_pg3(fd, "pg3b"),
M_NSPROBE => child_nsprobe(fd),
_ => libc::_exit(2),
}
}
unsafe fn child_baseline(fd: RawFd, pp: i32) -> ! {
cwl(fd, "baseline-child-entered");
let gp = libc::getppid();
cwi(fd, "getppid", gp as i64, "");
let r = libc::kill(pp, libc::SIGSTOP);
cwi(fd, "kill_stop_ret", r as i64, &errstr());
libc::usleep(800_000);
let r2 = libc::kill(pp, libc::SIGCONT);
cwi(fd, "kill_cont_ret", r2 as i64, &errstr());
cwl(fd, "baseline-child-done");
libc::_exit(0);
}
unsafe fn child_ns_kill(fd: RawFd, pp: i32) -> ! {
cwl(fd, "nschild-entered");
let me = libc::syscall(libc::SYS_getpid);
cwi(fd, "getpid", me as i64, "");
let gp = libc::syscall(libc::SYS_getppid);
cwi(fd, "getppid", gp as i64, "");
let parent_visible = proc_has_pid(pp);
cwi(fd, "parent_pid_visible_in_proc", parent_visible as i64, "");
let st = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
let nspid = st
.lines()
.find_map(|l| l.strip_prefix("NSpid:").map(|v| v.trim().to_string()))
.unwrap_or_default();
cwl(fd, &format!("nschild_NSpid={nspid}"));
let pid_inode = std::fs::read_link("/proc/self/ns/pid")
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
cwl(fd, &format!("nschild_pid_ns_inode={pid_inode}"));
let ns_is_real = nspid.split_whitespace().count() >= 2;
cwi(fd, "nschild_ns_is_real", ns_is_real as i64, "");
cwl(fd, "nschild-before-kill-attempt");
libc::usleep(2_000_000);
if ns_is_real {
let r = libc::kill(pp, libc::SIGKILL);
cwi(fd, "kill_parent_ret", r as i64, &errstr());
} else {
cwi(fd, "kill_parent_ret", -999, " KILL_SKIPPED_NS_NOT_REAL");
}
cwl(fd, "nschild-alive-after-kill-attempt");
libc::usleep(200_000);
cwl(fd, "nschild-exiting");
libc::_exit(0);
}
fn proc_has_pid(pid: i32) -> bool {
std::path::Path::new(&format!("/proc/{pid}")).exists()
}
unsafe fn child_pty(fd: RawFd, slave: i32) -> ! {
cwl(fd, "pty-child-entered");
let r0 = libc::dup2(slave, 0);
cwi(fd, "dup2_0", r0 as i64, &errstr());
let r1 = libc::dup2(slave, 1);
cwi(fd, "dup2_1", r1 as i64, &errstr());
let r2 = libc::dup2(slave, 2);
cwi(fd, "dup2_2", r2 as i64, &errstr());
let r3 = libc::setsid();
cwi(fd, "setsid", r3 as i64, &errstr());
let r4 = libc::ioctl(slave, libc::TIOCSCTTY as libc::Ioctl, 0);
cwi(fd, "tiocsctty", r4 as i64, &errstr());
let isa = libc::isatty(0);
cwi(fd, "isatty0", isa as i64, "");
let mut ws: libc::winsize = std::mem::zeroed();
let r5 = libc::ioctl(0, libc::TIOCGWINSZ as libc::Ioctl, &mut ws);
cwi(
fd,
"tiocgwinsz",
r5 as i64,
&format!(" rows={} cols={}", ws.ws_row, ws.ws_col),
);
let mut sid: libc::c_int = 0;
let r6 = libc::ioctl(0, 0x5429 as libc::Ioctl, &mut sid); cwi(fd, "tiocgsid", r6 as i64, &format!(" sid={sid}"));
let mut pgrp: libc::c_int = 0;
let r7 = libc::ioctl(0, libc::TIOCGPGRP as libc::Ioctl, &mut pgrp);
cwi(fd, "tiocgpgrp", r7 as i64, &format!(" pgrp={pgrp}"));
let msg = b"PTY-ECHO:hello-from-namespaced-child\n";
let w = libc::write(1, msg.as_ptr() as *const libc::c_void, msg.len());
cwi(fd, "pty_stdout_write", w as i64, &errstr());
cwl(fd, "pty-child-done");
libc::_exit(0);
}
unsafe fn child_drain(fd: RawFd, sock: i32) -> ! {
cwl(fd, "drain-child-entered");
let mut buf = [0u8; 4096];
let mut got = 0usize;
while got < buf.len() {
let n = libc::read(
sock,
buf[got..].as_mut_ptr() as *mut libc::c_void,
buf.len() - got,
);
if n <= 0 {
break;
}
got += n as usize;
}
cwi(fd, "child_read_bytes", got as i64, "");
let mut off = 0usize;
while off < got {
let n = libc::write(sock, buf[off..].as_ptr() as *const libc::c_void, got - off);
if n <= 0 {
break;
}
off += n as usize;
}
cwi(fd, "child_echo_bytes", off as i64, "");
cwl(fd, "drain-child-done");
libc::_exit(0);
}
const BPF_LD: u16 = 0x00;
const BPF_W: u16 = 0x00;
const BPF_ABS: u16 = 0x20;
const BPF_JMP: u16 = 0x05;
const BPF_JEQ: u16 = 0x10;
const BPF_K: u16 = 0x00;
const BPF_RET: u16 = 0x06;
const SECCOMP_RET_KILL_PROCESS: u32 = 0x8000_0000;
const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
const EPERM: u32 = 1;
const AUDIT_ARCH_AARCH64: u32 = 0xC000_00B7;
const fn bpf_stmt(code: u16, k: u32) -> libc::sock_filter {
libc::sock_filter {
code,
jt: 0,
jf: 0,
k,
}
}
const fn bpf_jump(code: u16, k: u32, jt: u8, jf: u8) -> libc::sock_filter {
libc::sock_filter { code, jt, jf, k }
}
static SESSION_CONTAINMENT_FILTER: [libc::sock_filter; 13] = [
bpf_stmt(BPF_LD | BPF_W | BPF_ABS, 4),
bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
bpf_stmt(BPF_LD | BPF_W | BPF_ABS, 0),
bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, libc::SYS_setsid as u32, 0, 1),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, libc::SYS_setpgid as u32, 0, 1),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, libc::SYS_unshare as u32, 0, 1),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, libc::SYS_setns as u32, 0, 1),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM),
bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
];
unsafe fn child_seccomp(fd: RawFd) -> ! {
cwl(fd, "seccomp-child-entered");
let r = libc::setsid();
cwi(fd, "setsid_before_filter", r as i64, &errstr());
let prog = libc::sock_fprog {
len: SESSION_CONTAINMENT_FILTER.len() as u16,
filter: SESSION_CONTAINMENT_FILTER.as_ptr().cast_mut(),
};
let p1 = libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
cwi(fd, "no_new_privs", p1 as i64, &errstr());
let p2 = libc::prctl(
libc::PR_SET_SECCOMP,
libc::SECCOMP_MODE_FILTER,
&prog as *const libc::sock_fprog as libc::c_ulong,
0,
0,
);
cwi(fd, "seccomp_filter", p2 as i64, &errstr());
cwl(fd, "filter-installed");
let a = libc::setpgid(0, 0);
cwi(fd, "setpgid", a as i64, &errstr());
let b = libc::setsid();
cwi(fd, "setsid_after_filter", b as i64, &errstr());
let c = libc::syscall(libc::SYS_unshare, CLONE_NEWUSER);
cwi(fd, "unshare", c as i64, &errstr());
let d = libc::write(1, b"allowed-bytes\n".as_ptr() as *const libc::c_void, 14);
cwi(fd, "write_allowed", d as i64, &errstr());
cwl(fd, "seccomp-child-alive-after-denials");
libc::_exit(0);
}
unsafe fn child_nsprobe(fd: RawFd) -> ! {
if CHILD_ARG1 == 2 {
let ur = libc::syscall(libc::SYS_unshare, CLONE_NEWPID);
cwi(fd, "unshare_ret", ur as i64, &errstr());
let g = libc::fork();
cwi(fd, "fork_ret", g as i64, &errstr());
if g != 0 {
cwl(fd, "nsprobe-unshare-parent-holding");
loop {
libc::pause();
}
}
cwl(fd, "nsprobe-grandchild-entered");
} else {
cwl(fd, "nsprobe-entered");
}
let me = libc::syscall(libc::SYS_getpid);
cwi(fd, "getpid_syscall", me as i64, "");
let gp = libc::syscall(libc::SYS_getppid);
cwi(fd, "getppid_syscall", gp as i64, "");
let st = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
let nspid = st
.lines()
.find_map(|l| l.strip_prefix("NSpid:").map(|v| v.trim().to_string()))
.unwrap_or_default();
cwl(fd, &format!("NSpid={nspid}"));
let inode = std::fs::read_link("/proc/self/ns/pid")
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
cwl(fd, &format!("pid_ns_inode={inode}"));
cwl(fd, "nsprobe-pausing");
loop {
libc::pause();
}
}
unsafe fn child_reap(fd: RawFd) -> ! {
cwl(fd, "reap-child-entered");
libc::usleep(500_000);
cwl(fd, "reap-child-exiting");
libc::_exit(0);
}
unsafe fn child_pg3(fd: RawFd, tag: &str) -> ! {
cwl(fd, &format!("{tag}-child-entered"));
let r = libc::setsid();
cwi(fd, "setsid", r as i64, &errstr());
let g = libc::fork();
cwi(fd, "fork_grandchild", g as i64, &errstr());
if g == 0 {
cwi(fd, "grandchild_getpid", libc::getpid() as i64, "");
cwi(fd, "grandchild_getppid", libc::getppid() as i64, "");
cwl(fd, &format!("{tag}-grandchild-pausing"));
loop {
libc::pause();
}
}
cwl(fd, &format!("{tag}-child-waiting"));
loop {
libc::pause();
}
}
struct Rdr {
fd: RawFd,
buf: Vec<u8>,
seen: Vec<String>,
}
impl Rdr {
fn new(fd: RawFd) -> Self {
unsafe {
let fl = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, fl | libc::O_NONBLOCK);
}
Rdr {
fd,
buf: Vec::new(),
seen: Vec::new(),
}
}
fn all_seen(&self) -> Vec<String> {
self.seen.clone()
}
fn next_line(&mut self, timeout: Duration) -> Option<String> {
let deadline = Instant::now() + timeout;
loop {
if let Some(pos) = self.buf.iter().position(|&b| b == b'\n') {
let line: Vec<u8> = self.buf.drain(..=pos).collect();
let s = String::from_utf8_lossy(&line[..line.len() - 1]).to_string();
if !s.is_empty() {
self.seen.push(s.clone());
return Some(s);
}
continue;
}
let now = Instant::now();
if now >= deadline {
return None;
}
let rem = deadline.saturating_duration_since(now);
let mut pfd = libc::pollfd {
fd: self.fd,
events: libc::POLLIN,
revents: 0,
};
let pr =
unsafe { libc::poll(&mut pfd, 1, rem.as_millis().min(i32::MAX as u128) as i32) };
if pr <= 0 {
continue;
}
let mut tmp = [0u8; 4096];
let n =
unsafe { libc::read(self.fd, tmp.as_mut_ptr() as *mut libc::c_void, tmp.len()) };
if n <= 0 {
continue;
}
self.buf.extend_from_slice(&tmp[..n as usize]);
}
}
fn wait_for(&mut self, marker: &str, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let rem = deadline.saturating_duration_since(Instant::now());
if let Some(l) = self.next_line(rem) {
if l.contains(marker) {
return true;
}
}
}
false
}
fn drain_all(&mut self, timeout: Duration) -> Vec<String> {
let mut v = Vec::new();
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let rem = deadline.saturating_duration_since(Instant::now());
match self.next_line(rem) {
Some(l) => v.push(l),
None => break,
}
}
v
}
}
fn waitpid_poll(pid: libc::pid_t, timeout: Duration) -> Result<Option<libc::c_int>, i32> {
let deadline = Instant::now() + timeout;
loop {
let mut st: libc::c_int = 0;
let r = unsafe { libc::waitpid(pid, &mut st, libc::WNOHANG) };
if r == pid {
return Ok(Some(st));
}
if r < 0 {
return Err(errno());
}
if Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn status_desc(st: libc::c_int) -> String {
if libc::WIFEXITED(st) {
format!("exited({})", libc::WEXITSTATUS(st))
} else if libc::WIFSIGNALED(st) {
format!("signaled({})", libc::WTERMSIG(st))
} else {
format!("status=0x{st:x}")
}
}
fn stat_fields(pid: i32) -> Option<Vec<String>> {
let s = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let rest = s.split_once(')').map(|(_, r)| r.to_string())?;
Some(rest.split_whitespace().map(|s| s.to_string()).collect())
}
fn find_grandchild(ppid: i32) -> Option<i32> {
let dir = std::fs::read_dir("/proc").ok()?;
for e in dir.flatten() {
let name = e.file_name().to_string_lossy().to_string();
if let Ok(p) = name.parse::<i32>() {
if let Some(f) = stat_fields(p) {
if f.get(1).and_then(|s| s.parse::<i32>().ok()) == Some(ppid) {
return Some(p);
}
}
}
}
None
}
fn make_pty() -> Result<(RawFd, RawFd), String> {
let master = unsafe {
libc::open(
b"/dev/ptmx\0".as_ptr() as *const libc::c_char,
libc::O_RDWR | libc::O_NOCTTY,
)
};
if master < 0 {
return Err(format!("open ptmx errno={}", errno()));
}
if unsafe { libc::grantpt(master) } != 0 {
return Err(format!("grantpt errno={}", errno()));
}
if unsafe { libc::unlockpt(master) } != 0 {
return Err(format!("unlockpt errno={}", errno()));
}
let mut name = [0u8; 64];
let r = unsafe { libc::ptsname_r(master, name.as_mut_ptr() as *mut libc::c_char, name.len()) };
if r != 0 {
return Err(format!("ptsname_r errno={r}"));
}
let n = name.iter().position(|&b| b == 0).unwrap_or(name.len());
let slave = unsafe {
libc::open(
name[..n].as_ptr() as *const libc::c_char,
libc::O_RDWR | libc::O_NOCTTY,
)
};
if slave < 0 {
return Err(format!("open slave errno={}", errno()));
}
Ok((master, slave))
}
fn caps_from_status() -> (String, String, String) {
let st = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
let mut cap = String::new();
let mut nnp = String::new();
let mut sec = String::new();
for l in st.lines() {
if let Some(v) = l.strip_prefix("CapEff:") {
cap = v.trim().to_string();
} else if let Some(v) = l.strip_prefix("NoNewPrivs:") {
nnp = v.trim().to_string();
} else if let Some(v) = l.strip_prefix("Seccomp:") {
sec = v.trim().to_string();
}
}
(cap, nnp, sec)
}
fn prctl_subreaper() -> i32 {
unsafe { libc::prctl(36, 1, 0, 0, 0) }
}
fn t0() -> bool {
say("=== T0: privilege precondition + clone3(CLONE_NEWPID) feasibility ===");
let (cap, nnp, sec) = caps_from_status();
say(format!(
"T0 uid={} gid={} CapEff={cap} NoNewPrivs={nnp} Seccomp={sec}",
unsafe { libc::geteuid() },
unsafe { libc::getegid() }
));
let sr = prctl_subreaper();
say(format!(
"T0 prctl(PR_SET_CHILD_SUBREAPER)= {sr} errno={}",
errno()
));
let args = CloneArgs {
flags: CLONE_NEWPID,
pidfd: 0,
child_tid: 0,
parent_tid: 0,
exit_signal: libc::SIGCHLD as u64,
stack: 0,
stack_size: 0,
tls: 0,
set_tid: 0,
set_tid_size: 0,
cgroup: 0,
};
let pid = unsafe {
libc::syscall(
SYS_CLONE3,
&args as *const CloneArgs as usize,
std::mem::size_of::<CloneArgs>() as usize,
)
};
if pid < 0 {
say(format!(
"T0 clone3(flags=CLONE_NEWPID(0x{:x}), exit_signal=SIGCHLD) = -1 errno={}",
CLONE_NEWPID,
errno()
));
return false;
}
if pid == 0 {
unsafe { libc::_exit(0) }
}
say(format!(
"T0 clone3(CLONE_NEWPID) = pid {pid} (syscall accepted; ns-creation checked next in T0b)"
));
let r = waitpid_poll(pid as libc::pid_t, Duration::from_secs(3));
say(format!(
"T0 waitpid probe child: {:?}",
r.map(|st| st.map(status_desc))
));
true
}
fn nsprobe_run(method: &str) -> (i64, String, String) {
let mut child_fn: Option<Box<dyn Fn() -> Result<(libc::pid_t, RawFd), String>>> = None;
let arg1 = if method == "unshare+fork" { 2 } else { 0 };
let newpid = method != "unshare+fork";
let mut fds = [0 as RawFd; 2];
let _ = unsafe { libc::pipe(fds.as_mut_ptr()) };
let (report_r, report_w) = (fds[0], fds[1]);
unsafe {
CHILD_MODE = M_NSPROBE;
CHILD_PARENT_PID = 0;
CHILD_ARG1 = arg1;
CHILD_REPORT_FD = report_w;
}
let spawn_res = if newpid {
if method == "clone3" {
clone3_newpid()
} else {
clone_plain_newpid()
}
} else {
clone_plain()
};
let pid = match spawn_res {
Ok(p) if p > 0 => p,
Ok(_) => unsafe { child_main() },
Err(e) => {
unsafe {
libc::close(report_r);
libc::close(report_w);
}
say(format!("nsprobe[{method}] spawn errno={e}"));
return (0, String::new(), String::new());
}
};
unsafe {
libc::close(report_w);
}
let _ = child_fn.take();
let mut r = Rdr::new(report_r);
let _ = r.wait_for("nsprobe-pausing", Duration::from_secs(3));
let lines = r.all_seen();
let mut gpid = 0i64;
let mut nspid = String::new();
let mut inode = String::new();
for l in &lines {
say(format!("nsprobe[{method}] child> {l}"));
if let Some(v) = l.strip_prefix("getpid_syscall=") {
gpid = v.parse().unwrap_or(0);
} else if let Some(v) = l.strip_prefix("NSpid=") {
nspid = v.to_string();
} else if let Some(v) = l.strip_prefix("pid_ns_inode=") {
inode = v.to_string();
}
}
let sib = if method == "unshare+fork" {
std::thread::sleep(Duration::from_millis(30));
find_grandchild(pid)
} else {
None
};
if let Some(sib) = sib {
unsafe { libc::kill(sib, libc::SIGKILL) };
let _ = waitpid_poll(sib, Duration::from_secs(2));
}
unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = waitpid_poll(pid, Duration::from_secs(2));
unsafe { libc::close(report_r) };
(gpid, nspid, inode)
}
fn t0b() {
say("");
say("=== T0b: which clone path actually creates a new PID namespace? ===");
let parent_inode = std::fs::read_link("/proc/self/ns/pid")
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
say(format!("T0b parent pid-ns inode: {parent_inode}"));
for method in ["clone3", "clone", "unshare+fork"] {
let (gpid, nspid, inode) = nsprobe_run(method);
let two_levels = nspid.split_whitespace().count() >= 2;
let is_new_ns = gpid == 1 && two_levels && inode != parent_inode;
say(format!(
"T0b {method}: getpid_syscall={gpid} NSpid=[{nspid}] pid-ns inode={inode} -> {}",
if is_new_ns {
"NEW PID NAMESPACE CREATED"
} else {
"SAME NAMESPACE (FAILED)"
}
));
}
}
fn p1a() -> bool {
say("");
say("=== P1a: baseline - child WITHOUT CLONE_NEWPID can resolve+signal parent ===");
let pp = unsafe { libc::getpid() };
let t0 = Instant::now();
let (pid, rfd) = match spawn_child(M_BASELINE, pp, 0, false) {
Ok(v) => v,
Err(e) => {
say(format!("P1a FAIL spawn: {e}"));
return false;
}
};
say(format!("P1a spawned child pid={pid} (no new pid ns)"));
let mut r = Rdr::new(rfd);
let lines = r.drain_all(Duration::from_secs(4));
let elapsed = t0.elapsed();
say(format!(
"P1a parent elapsed since spawn = {elapsed:?} (>= ~700ms implies frozen by SIGSTOP)"
));
for l in &lines {
say(format!("P1a child> {l}"));
}
let r = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P1a waitpid: {:?}",
r.map(|st| st.map(status_desc))
));
unsafe { libc::close(rfd) };
let gp_ok = lines.iter().any(|l| l == &format!("getppid={pp}"));
let stop_ok = lines.iter().any(|l| l.starts_with("kill_stop_ret=0 "));
let frozen_ok = elapsed >= Duration::from_millis(700);
let cont_ok = lines.iter().any(|l| l.starts_with("kill_cont_ret=0 "));
let done_ok = lines.iter().any(|l| l == "baseline-child-done");
let pass = gp_ok && stop_ok && frozen_ok && cont_ok && done_ok;
say(format!(
"P1a VERDICT: {} (getppid==parent:{gp_ok} child_sigstop_delivered:{stop_ok} parent_frozen:{frozen_ok} sigcont_delivered:{cont_ok} child_finished:{done_ok})",
if pass { "PASS" } else { "FAIL" }
));
pass
}
fn p1b() -> bool {
say("");
say("=== P1b: namespaced - child in CLONE_NEWPID cannot resolve/signal parent ===");
let pp = unsafe { libc::getpid() };
let (pid, rfd) = match spawn_child(M_NS_KILL, pp, 0, true) {
Ok(v) => v,
Err(e) => {
say(format!("P1b FAIL spawn: {e}"));
return false;
}
};
say(format!("P1b spawned child pid={pid} (parent-ns pid)"));
let mut r = Rdr::new(rfd);
say("P1b parent-alive-after-spawn");
let ns_confirmed = r.wait_for("nschild-before-kill-attempt", Duration::from_secs(4));
say(format!("P1b child-reached-kill-gate={ns_confirmed}"));
let pre: Vec<String> = r.all_seen();
for l in &pre {
say(format!("P1b child> {l}"));
}
let pstat = std::fs::read_to_string(format!("/proc/{pid}/status")).unwrap_or_default();
let p_nspid = pstat
.lines()
.find_map(|l| l.strip_prefix("NSpid:").map(|v| v.trim().to_string()))
.unwrap_or_default();
say(format!(
"P1b parent-side /proc/{pid}/status NSpid={p_nspid} (two numbers => nested pid ns)"
));
let getpid = pre
.iter()
.find_map(|l| l.strip_prefix("getpid="))
.map(|v| v.to_string())
.unwrap_or_default();
let getppid = pre
.iter()
.find_map(|l| l.strip_prefix("getppid="))
.map(|v| v.to_string())
.unwrap_or_default();
let not_visible = pre.iter().any(|l| l == "parent_pid_visible_in_proc=0");
let ns_real_parent = p_nspid.split_whitespace().count() >= 2;
let child_ns_is_real = pre.iter().any(|l| l == "nschild_ns_is_real=1");
let ns_is_real = ns_real_parent && child_ns_is_real;
say(format!(
"P1b namespace-check: child getpid={getpid} getppid={getppid} parent_hidden_in_proc={not_visible} parent-side-NSpid-2level={ns_real_parent} child_ns_is_real={child_ns_is_real} -> namespace REAL={ns_is_real}"
));
let w = waitpid_poll(pid, Duration::from_millis(100));
say(format!(
"P1b waitpid(WNOHANG) before kill gate: {w:?} (Ok(None)=still running)"
));
say("P1b parent-alive-before-child-kill-attempt");
let post: Vec<String> = r.drain_all(Duration::from_secs(5));
for l in &post {
say(format!("P1b child> {l}"));
}
let parent_survived = post.iter().any(|l| l == "nschild-alive-after-kill-attempt");
say(format!(
"P1b parent-alive-after-child-kill-attempt={parent_survived}"
));
let rr = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P1b waitpid reap: {:?}",
rr.map(|st| st.map(status_desc))
));
unsafe { libc::close(rfd) };
let esrch = post
.iter()
.any(|l| l.starts_with(&format!("kill_parent_ret=-1 errno={}", libc::ESRCH)));
let alive = post.iter().any(|l| l == "nschild-alive-after-kill-attempt");
let exiting = post.iter().any(|l| l == "nschild-exiting");
let skipped = post.iter().any(|l| l.contains("KILL_SKIPPED_NS_NOT_REAL"));
let pass = ns_is_real && esrch && alive && exiting && ns_confirmed;
say(format!(
"P1b VERDICT: {} (namespace_REAL:{ns_is_real} parent_hidden_in_proc:{not_visible} kill(parent)->ESRCH:{esrch} child_survived_kill_attempt:{alive} child_exited_normally:{exiting} kill_skipped_same_ns:{skipped})",
if pass { "PASS" } else { "FAIL" }
));
if !ns_is_real {
say(format!(
"P1b note: CLONE_NEWPID NOT HONORED on this target -> child lives in parent's pid ns -> getppid()/kill(parent) signal-back vector stays OPEN (baseline P1a proves the same-ns kill lands)"
));
}
pass
}
fn p2a() -> bool {
say("");
say("=== P2a: pty + setsid + TIOCSCTTY inside the new pid namespace ===");
let (master, slave) = match make_pty() {
Ok(v) => v,
Err(e) => {
say(format!("P2a FAIL make_pty: {e}"));
return false;
}
};
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
ws.ws_row = 24;
ws.ws_col = 80;
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as libc::Ioctl, &ws) };
let (pid, rfd) = match spawn_child(M_PTY, 0, slave, true) {
Ok(v) => v,
Err(e) => {
say(format!("P2a FAIL spawn: {e}"));
return false;
}
};
say(format!("P2a spawned pty child pid={pid} (namespaced)"));
let mut r = Rdr::new(rfd);
let lines = r.drain_all(Duration::from_secs(3));
for l in &lines {
say(format!("P2a child> {l}"));
}
let mut pty_out = Vec::new();
let mut pfd = libc::pollfd {
fd: master,
events: libc::POLLIN,
revents: 0,
};
let pr = unsafe { libc::poll(&mut pfd, 1, 1000) };
if pr > 0 {
let mut tmp = [0u8; 256];
let n = unsafe { libc::read(master, tmp.as_mut_ptr() as *mut libc::c_void, tmp.len()) };
if n > 0 {
pty_out.extend_from_slice(&tmp[..n as usize]);
}
}
say(format!(
"P2a pty master read: {:?}",
String::from_utf8_lossy(&pty_out)
));
let rr = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P2a waitpid: {:?}",
rr.map(|st| st.map(status_desc))
));
unsafe {
libc::close(master);
libc::close(slave);
libc::close(rfd);
}
let dup_ok = ["dup2_0=0 ", "dup2_1=1 ", "dup2_2=2 "]
.iter()
.all(|m| lines.iter().any(|l| l.starts_with(m)));
let setsid_ok = lines
.iter()
.any(|l| l.starts_with(&format!("setsid={pid} ")));
let ctty_ok = lines.iter().any(|l| l.starts_with("tiocsctty=0 "));
let isatty_ok = lines.iter().any(|l| l == "isatty0=1");
let winsz_ok = lines
.iter()
.any(|l| l.starts_with("tiocgwinsz=0") && l.contains("rows=24") && l.contains("cols=80"));
let echo_ok =
String::from_utf8_lossy(&pty_out).contains("PTY-ECHO:hello-from-namespaced-child");
let pass = dup_ok && setsid_ok && ctty_ok && isatty_ok && winsz_ok && echo_ok;
say(format!(
"P2a VERDICT: {} (dup2_012:{dup_ok} setsid:{setsid_ok} TIOCSCTTY:{ctty_ok} isatty(0):{isatty_ok} winsize_24x80:{winsz_ok} pty_echo_crossed_ns:{echo_ok})",
if pass { "PASS" } else { "FAIL" }
));
pass
}
fn p2b() -> bool {
say("");
say("=== P2b: stdio drain byte-for-byte across the namespace boundary ===");
let mut sv = [0 as RawFd; 2];
if unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, sv.as_mut_ptr()) } != 0 {
say(format!("P2b FAIL socketpair errno={}", errno()));
return false;
}
let (a, b) = (sv[0], sv[1]);
let (pid, rfd) = match spawn_child(M_DRAIN, 0, a, true) {
Ok(v) => v,
Err(e) => {
say(format!("P2b FAIL spawn: {e}"));
return false;
}
};
unsafe { libc::close(a) };
say(format!("P2b spawned drain child pid={pid} (namespaced)"));
let mut payload = [0u8; 4096];
for i in 0..payload.len() {
payload[i] = ((i.wrapping_mul(7) + (i >> 8)) & 0xff) as u8;
}
let mut off = 0usize;
while off < payload.len() {
let n = unsafe {
libc::write(
b,
payload[off..].as_ptr() as *const libc::c_void,
payload.len() - off,
)
};
if n <= 0 {
break;
}
off += n as usize;
}
let mut back = vec![0u8; payload.len()];
let mut got = 0usize;
let deadline = Instant::now() + Duration::from_secs(3);
while got < back.len() && Instant::now() < deadline {
let n = unsafe {
libc::read(
b,
back[got..].as_mut_ptr() as *mut libc::c_void,
back.len() - got,
)
};
if n <= 0 {
break;
}
got += n as usize;
}
unsafe { libc::close(b) };
let mut r = Rdr::new(rfd);
let lines = r.drain_all(Duration::from_secs(2));
for l in &lines {
say(format!("P2b child> {l}"));
}
let rr = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P2b waitpid: {:?}",
rr.map(|st| st.map(status_desc))
));
unsafe { libc::close(rfd) };
let equal = got == payload.len() && back == payload;
say(format!(
"P2b sent={off} received_back={got} byte_equal={equal}"
));
let read_ok = lines.iter().any(|l| l == "child_read_bytes=4096");
let echo_ok = lines.iter().any(|l| l == "child_echo_bytes=4096");
let pass = equal && read_ok && echo_ok;
say(format!(
"P2b VERDICT: {} (child_read_4096:{read_ok} child_echo_4096:{echo_ok} parent_roundtrip_byte_equal:{equal})",
if pass { "PASS" } else { "FAIL" }
));
pass
}
fn p2c() -> bool {
say("");
say("=== P2c: seccomp chaining inside the new pid namespace ===");
let (pid, rfd) = match spawn_child(M_SECCOMP, 0, 0, true) {
Ok(v) => v,
Err(e) => {
say(format!("P2c FAIL spawn: {e}"));
return false;
}
};
say(format!("P2c spawned seccomp child pid={pid} (namespaced)"));
let mut r = Rdr::new(rfd);
let lines = r.drain_all(Duration::from_secs(3));
for l in &lines {
say(format!("P2c child> {l}"));
}
let rr = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P2c waitpid: {:?}",
rr.map(|st| st.map(status_desc))
));
unsafe { libc::close(rfd) };
let nnps_ok = lines.iter().any(|l| l.starts_with("no_new_privs=0 "));
let filt_ok = lines.iter().any(|l| l.starts_with("seccomp_filter=0 "));
let pre_ok = lines
.iter()
.any(|l| l.starts_with(&format!("setsid_before_filter={pid} ")));
let setpgid_den = lines
.iter()
.any(|l| l == &format!("setpgid=-1 errno={}", libc::EPERM));
let setsid_den = lines
.iter()
.any(|l| l == &format!("setsid_after_filter=-1 errno={}", libc::EPERM));
let unshare_den = lines
.iter()
.any(|l| l == &format!("unshare=-1 errno={}", libc::EPERM));
let write_ok = lines.iter().any(|l| l.starts_with("write_allowed=14"));
let alive = lines
.iter()
.any(|l| l == "seccomp-child-alive-after-denials");
let pass = nnps_ok
&& filt_ok
&& pre_ok
&& setpgid_den
&& setsid_den
&& unshare_den
&& write_ok
&& alive;
say(format!(
"P2c VERDICT: {} (no_new_privs:{nnps_ok} filter:{filt_ok} pre_filter_setsid:{pre_ok} setpgid->EPERM:{setpgid_den} setsid->EPERM:{setsid_den} unshare->EPERM:{unshare_den} allowed_write:{write_ok} alive:{alive})",
if pass { "PASS" } else { "FAIL" }
));
pass
}
fn p2d() -> bool {
say("");
say("=== P2d: waitpid from the daemon namespace reaps the namespaced child ===");
let (pid, rfd) = match spawn_child(M_REAP, 0, 0, true) {
Ok(v) => v,
Err(e) => {
say(format!("P2d FAIL spawn: {e}"));
return false;
}
};
say(format!("P2d spawned reap child pid={pid} (namespaced)"));
let mut r = Rdr::new(rfd);
let entered = r.wait_for("reap-child-entered", Duration::from_secs(3));
say(format!("P2d child-entered={entered}"));
let w = waitpid_poll(pid, Duration::from_millis(100));
say(format!(
"P2d waitpid(WNOHANG) while child sleeping (expect Ok(None)): {w:?}"
));
std::thread::sleep(Duration::from_millis(900));
let b = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P2d waitpid blocking reap: {:?}",
b.map(|st| st.map(status_desc))
));
let proc_gone = !proc_has_pid(pid);
say(format!("P2d /proc/{pid} gone after reap: {proc_gone}"));
let second = waitpid_poll(pid, Duration::from_millis(200));
say(format!(
"P2d second waitpid (expect Err(ECHILD={})): {second:?}",
libc::ECHILD
));
let rest = r.drain_all(Duration::from_millis(500));
for l in &rest {
say(format!("P2d child> {l}"));
}
unsafe { libc::close(rfd) };
let wnh = matches!(w, Ok(None));
let reaped = matches!(b, Ok(Some(st)) if libc::WIFEXITED(st));
let ech = matches!(second, Err(e) if e == libc::ECHILD);
let pass = entered && wnh && reaped && proc_gone && ech;
say(format!(
"P2d VERDICT: {} (WNOHANG=still_running:{wnh} blocking_reap_wifexited:{reaped} /proc_pid_removed:{proc_gone} second_waitpid_ECHILD:{ech})",
if pass { "PASS" } else { "FAIL" }
));
pass
}
fn p3_common(tag: &str, mode: i32) -> bool {
say("");
say(format!(
"=== P3{tag}: daemon retains kill rights across the boundary ==="
));
let (pid, rfd) = match spawn_child(mode, 0, 0, true) {
Ok(v) => v,
Err(e) => {
say(format!("P3{tag} FAIL spawn: {e}"));
return false;
}
};
say(format!(
"P3{tag} spawned session-leader child pid={pid} (parent-ns pid)"
));
let ctag = format!("pg3{tag}");
let mut r = Rdr::new(rfd);
let lines = r.drain_all(Duration::from_secs(4));
for l in &lines {
say(format!("P3{tag} child> {l}"));
}
let gc_ready = lines
.iter()
.any(|l| l == &format!("{ctag}-grandchild-pausing"));
let c_ready = lines.iter().any(|l| l == &format!("{ctag}-child-waiting"));
say(format!(
"P3{tag} grandchild_ready={gc_ready} child_ready={c_ready}"
));
let gc = find_grandchild(pid);
say(format!("P3{tag} grandchild parent-ns pid: {:?}", gc));
let child_f = stat_fields(pid);
let gc_f = gc.and_then(|g| stat_fields(g));
if let Some(f) = &child_f {
say(format!(
"P3{tag} child /proc stat: state={} ppid={} pgrp={} session={}",
f.get(0).unwrap_or(&"?".into()),
f.get(1).unwrap_or(&"?".into()),
f.get(2).unwrap_or(&"?".into()),
f.get(3).unwrap_or(&"?".into())
));
}
if let Some(f) = &gc_f {
say(format!(
"P3{tag} grandchild /proc stat: state={} ppid={} pgrp={} session={}",
f.get(0).unwrap_or(&"?".into()),
f.get(1).unwrap_or(&"?".into()),
f.get(2).unwrap_or(&"?".into()),
f.get(3).unwrap_or(&"?".into())
));
}
unsafe { libc::close(rfd) };
let _ = r.drain_all(Duration::from_millis(100));
let pgid = pid;
let kr = unsafe { libc::kill(-pgid, libc::SIGKILL) };
say(format!(
"P3{tag} kill(-pgid={pgid}, SIGKILL) from parent ns = {kr} errno={}",
errno()
));
let c_reap = waitpid_poll(pid, Duration::from_secs(3));
say(format!(
"P3{tag} waitpid child: {:?}",
c_reap.map(|st| st.map(status_desc))
));
let g_reap = gc.and_then(|g| waitpid_poll(g, Duration::from_secs(3)).ok());
say(format!(
"P3{tag} waitpid grandchild: {:?}",
g_reap.map(|st| st.map(status_desc))
));
let c_gone = !proc_has_pid(pid);
let g_gone = gc.map(|g| !proc_has_pid(g)).unwrap_or(false);
say(format!(
"P3{tag} /proc child gone: {c_gone}, grandchild gone: {g_gone}"
));
let group_kill_ok = kr == 0;
let both_reaped = matches!(c_reap, Ok(Some(_))) && matches!(g_reap, Some(Some(_)));
let pass =
gc_ready && c_ready && gc.is_some() && group_kill_ok && both_reaped && c_gone && g_gone;
say(format!(
"P3{tag} VERDICT: {} (both_visible_in_proc:{} same_pgrp/sid==child_pid:{} kill(-pgid)==0:{group_kill_ok} child_reaped:{both_reaped} grandchild_reaped:{} /proc_cleared:{c_gone}/{g_gone})",
if pass { "PASS" } else { "FAIL" },
gc.is_some(),
{
let same = child_f
.as_ref()
.zip(gc_f.as_ref())
.map(|(cf, gf)| {
cf.get(2) == Some(&format!("{pgid}"))
&& gf.get(2) == Some(&format!("{pgid}"))
&& cf.get(3) == Some(&format!("{pgid}"))
&& gf.get(3) == Some(&format!("{pgid}"))
})
.unwrap_or(false);
same
},
matches!(g_reap, Some(Some(_)))
));
pass
}
fn p3a() -> bool {
p3_common("a", M_PG3A)
}
fn p3b() -> bool {
p3_common("b", M_PG3B)
}
fn main() {
let _ = std::fs::remove_file(LOG);
say("=== newpid_probe start ===");
let mut un = unsafe { std::mem::zeroed::<libc::utsname>() };
if unsafe { libc::uname(&mut un) } == 0 {
let c = |b: [libc::c_char; 65]| {
unsafe { std::ffi::CStr::from_ptr(b.as_ptr()) }
.to_string_lossy()
.into_owned()
};
say(format!(
"platform: sysname={} nodename={} release={} version={} machine={}",
c(un.sysname),
c(un.nodename),
c(un.release),
c(un.version),
c(un.machine)
));
}
let newpid_ok = t0();
t0b();
if !newpid_ok {
say("T0 FAIL: CLONE_NEWPID creation is blocked in this privilege context.");
say("VERDICT: namespace approach FAILS on this target (EPERM/blocked).");
}
let mut results: Vec<(&str, bool)> = Vec::new();
results.push(("T0", newpid_ok));
results.push(("P1a", p1a()));
if !newpid_ok {
for (name, _) in [
("P1b", false),
("P2a", false),
("P2b", false),
("P2c", false),
("P2d", false),
("P3a", false),
("P3b", false),
] {
results.push((name, false));
say(format!("{name} SKIPPED (namespace blocked)"));
}
} else {
results.push(("P1b", p1b()));
results.push(("P2a", p2a()));
results.push(("P2b", p2b()));
results.push(("P2c", p2c()));
results.push(("P2d", p2d()));
results.push(("P3a", p3a()));
results.push(("P3b", p3b()));
}
say("");
say("=== SUMMARY ===");
for (name, ok) in &results {
say(format!("{name}: {}", if *ok { "PASS" } else { "FAIL" }));
}
let all = results.iter().all(|(_, ok)| *ok);
say(format!(
"OVERALL: {}",
if all { "ALL PASS" } else { "SOME FAIL" }
));
}