1use crate::access;
24use crate::cli::bwrap;
25use crate::config::Config;
26use crate::exec::shim;
27use crate::fs::translate::{detranslate_path, translate_path};
28use crate::guest_stub::{self, layout};
29use crate::note;
30use crate::procs::{ProcTable, TraceeInfo};
31use crate::seccomp::{filter, notify};
32use crate::state::Ctx;
33use crate::sys::{errno, strerror};
34use crate::syscall::{
35 DUMPABLE_SYSCALLS, NAMESPACE_FLAG_SYSCALLS, NO_PTRACE_SYSCALLS, NOTIFY_SYSCALLS, REFUSED_SYSCALLS,
36 TRACE_SYSCALLS, direct, nr,
37};
38use crate::tracee::ptrace;
39use crate::tracer;
40use std::ffi::CString;
41use std::os::unix::ffi::OsStringExt;
42use std::sync::Arc;
43
44unsafe extern "C" {
45 static environ: *const *const libc::c_char;
46}
47
48pub fn run(config: Config) -> Result<i32, String> {
60 if config.command.is_empty() {
61 return Err("no command given".into());
62 }
63 let mut config = config;
64 resolve_command_in_path(&mut config);
65 let access = match access::probe(
66 !no_ptrace_forced() && direct::ptrace_usable(),
67 guest_stub_choice(),
68 ) {
69 Ok(a) => a,
70 Err(e) if config.bwrap_mode => exit_like_bwrap_without_namespaces(&e),
71 Err(e) => return Err(e),
72 };
73 let no_ptrace = !access.ptrace;
74 let mut ctx = Ctx::new(config, no_ptrace);
75 if access.trap_mode {
76 ctx.mailbox = Some(guest_stub::Mailbox::create()?.fd);
77 }
78 let ctx = Arc::new(ctx);
79 let trap_mode = ctx.trap_mode();
80 if no_ptrace {
81 note!(
82 ctx,
83 1,
84 "running without ptrace: the trace tier is served through notifications"
85 );
86 }
87 if trap_mode {
88 note!(
89 ctx,
90 1,
91 "running in the guest-stub mode: a stub in each guest stages syscall memory through a shared mailbox"
92 );
93 }
94 if access.userns.is_some() {
95 note!(
96 ctx,
97 1,
98 "the guest runs in a user namespace of its own: the ptrace access check refuses a plain child"
99 );
100 }
101 let mut notify_list: Vec<i64> = NOTIFY_SYSCALLS.to_vec();
102 notify_list.push(nr::CLONE3);
103 let trace_list: &[i64] = if no_ptrace {
104 notify_list.extend_from_slice(NO_PTRACE_SYSCALLS);
105 &[]
106 } else {
107 TRACE_SYSCALLS
108 };
109 if ctx.config.kernel_release.is_some() || ctx.config.hostname.is_some() {
110 notify_list.push(nr::UNAME);
111 }
112 if ctx.config.fake_root {
113 notify_list.extend_from_slice(crate::syscall::FAKE_ROOT_SYSCALLS);
114 }
115 let (native, compat) = (crate::abi::Abi::native(), crate::abi::Abi::compat());
116 let (notify64, trace64) = (native.numbers_for(¬ify_list), native.numbers_for(trace_list));
117 let (notify32, trace32) = (compat.numbers_for(¬ify_list), compat.numbers_for(trace_list));
118 let ns_list: Vec<i64> = NAMESPACE_FLAG_SYSCALLS
119 .iter()
120 .copied()
121 .filter(|nr| !notify_list.contains(nr))
122 .collect();
123 let (ns64, ns32) = (native.numbers_for(&ns_list), compat.numbers_for(&ns_list));
124 let (dumpable64, dumpable32) = (
125 native.numbers_for(DUMPABLE_SYSCALLS),
126 compat.numbers_for(DUMPABLE_SYSCALLS),
127 );
128 let (refused64, refused32) = (
129 native.numbers_for(REFUSED_SYSCALLS),
130 compat.numbers_for(REFUSED_SYSCALLS),
131 );
132 let program = if trap_mode {
133 let (mut trapped64, direct64): (Vec<i64>, Vec<i64>) =
134 notify64.iter().partition(|nr| layout::trapped64(**nr));
135 let (mut trapped32, direct32): (Vec<i64>, Vec<i64>) =
136 notify32.iter().partition(|nr| layout::trapped32(**nr));
137 trapped64.extend(layout::SIGNAL_CALLS64);
138 trapped32.extend(layout::SIGNAL_CALLS32);
139 filter::build_trapped(
140 &filter::Table {
141 abi: native,
142 notify: &direct64,
143 trace: &trapped64,
144 ns_flags: &ns64,
145 refused: &refused64,
146 dumpable: &dumpable64,
147 },
148 &filter::Table {
149 abi: compat,
150 notify: &direct32,
151 trace: &trapped32,
152 ns_flags: &ns32,
153 refused: &refused32,
154 dumpable: &dumpable32,
155 },
156 )
157 } else {
158 filter::build(
159 &filter::Table {
160 abi: native,
161 notify: ¬ify64,
162 trace: &trace64,
163 ns_flags: &ns64,
164 refused: &refused64,
165 dumpable: &dumpable64,
166 },
167 &filter::Table {
168 abi: compat,
169 notify: ¬ify32,
170 trace: &trace32,
171 ns_flags: &ns32,
172 refused: &refused32,
173 dumpable: &dumpable32,
174 },
175 no_ptrace,
176 )
177 };
178
179 let program_path = ctx.config.command[0].as_ptr();
181 let argv: Vec<*const libc::c_char> = std::iter::once(
182 ctx.config
183 .argv0
184 .as_ref()
185 .unwrap_or(&ctx.config.command[0])
186 .as_ptr(),
187 )
188 .chain(ctx.config.command.iter().skip(1).map(|c| c.as_ptr()))
189 .chain(std::iter::once(std::ptr::null()))
190 .collect();
191 let is_dir = |guest: &[u8]| {
194 translate_path(&ctx.root.fs, std::process::id(), libc::AT_FDCWD, guest, true)
195 .map(|h| {
196 std::fs::metadata(std::ffi::OsStr::from_bytes_hack(&h))
197 .map(|m| m.is_dir())
198 .unwrap_or(false)
199 })
200 .unwrap_or(false)
201 };
202 let start = bwrap::start_dir(&ctx.config, is_dir);
203 let host_cwd_exists = start.is_some();
204 if !(host_cwd_exists || (ctx.config.bwrap_mode && ctx.config.cwd_default)) {
205 eprintln!(
206 "proot: can't chdir to {} in the guest rootfs, using /",
207 String::from_utf8_lossy(&ctx.config.cwd)
208 );
209 }
210 let start = start.unwrap_or_else(|| b"/".to_vec());
211 let cwd = CString::new(start.clone()).map_err(|_| "cwd contains NUL")?;
212 let env = ctx.config.env.as_ref().map(|e| {
213 if ctx.config.bwrap_mode {
214 bwrap::with_pwd(e, &start)
215 } else {
216 e.clone()
217 }
218 });
219 let env_storage: Option<Vec<*const libc::c_char>> = env.as_ref().map(|e| {
220 e.iter()
221 .map(|c| c.as_ptr())
222 .chain(std::iter::once(std::ptr::null()))
223 .collect()
224 });
225 let envp: *const *const libc::c_char = match &env_storage {
226 Some(v) => v.as_ptr(),
227 None => unsafe { environ },
228 };
229 let slash = CString::new("/").unwrap();
230 let mailbox = ctx.mailbox;
231 let pinned_cwd: Option<CString> = if no_ptrace {
232 let dir = shim::install()?;
233 become_subreaper();
234 Some(CString::new(dir.into_os_string().into_vec()).map_err(|_| "alias directory contains NUL")?)
235 } else {
236 let _ = shim::install();
237 None
238 };
239 let initial_cwd: Vec<u8> = if host_cwd_exists {
240 translate_path(&ctx.root.fs, std::process::id(), libc::AT_FDCWD, &start, true)
241 .map(|h| detranslate_path(&ctx.root.fs, &h))
242 .unwrap_or_else(|_| b"/".to_vec())
243 } else {
244 b"/".to_vec()
245 };
246 let extra_programs: Vec<Vec<filter::SockFilter>> = ctx
248 .config
249 .seccomp_programs
250 .iter()
251 .map(|bytes| filter::from_bytes(bytes))
252 .collect::<std::result::Result<_, _>>()
253 .map_err(|e| format!("--seccomp program: {e}"))?;
254
255 let mut sv = [0i32; 2];
256 if unsafe {
257 libc::socketpair(
258 libc::AF_UNIX,
259 libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
260 0,
261 sv.as_mut_ptr(),
262 )
263 } < 0
264 {
265 return Err(format!("socketpair: {}", strerror(errno())));
266 }
267 let pid = unsafe { libc::fork() };
268 if pid < 0 {
269 return Err(format!("fork: {}", strerror(errno())));
270 }
271 if pid == 0 {
272 unsafe {
274 libc::close(sv[0]);
275 if let Some(dir) = &pinned_cwd {
276 if libc::chdir(dir.as_ptr()) < 0 {
277 libc::_exit(121);
278 }
279 libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL, 0, 0, 0);
280 }
281 if let Some(ns) = &access.userns
282 && ns.enter().is_err()
283 {
284 libc::_exit(118);
285 }
286 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0 {
287 libc::_exit(120);
288 }
289 let fd = match filter::install(&program) {
290 Ok(fd) => fd,
291 Err(libc::EBUSY) => libc::_exit(119),
292 Err(_) => libc::_exit(122),
293 };
294 if !send_fd(sv[1], fd) {
295 libc::_exit(123);
296 }
297 libc::close(fd);
298 if ctx.config.new_session {
299 libc::setsid();
300 }
301 for prog in &extra_programs {
302 if filter::install_plain(prog).is_err() {
303 libc::_exit(124);
304 }
305 }
306 let mut go = 0u8;
308 if libc::read(sv[1], &mut go as *mut u8 as *mut _, 1) != 1 {
309 libc::_exit(125);
310 }
311 libc::close(sv[1]);
312 if go != 0 {
313 libc::_exit(go as i32);
314 }
315 if pinned_cwd.is_none() && (!host_cwd_exists || libc::chdir(cwd.as_ptr()) < 0) {
317 libc::chdir(slash.as_ptr());
318 }
319 if let Some(mail) = mailbox {
320 guest_stub::exec_trampoline(
321 mail,
322 guest_stub::layout::SHIM_PATH.as_ptr().cast(),
323 argv.as_ptr(),
324 envp,
325 );
326 }
327 libc::execve(program_path, argv.as_ptr(), envp);
328 let e = *libc::__errno_location();
329 libc::_exit(if e == libc::ENOENT { 127 } else { 126 });
330 }
331 }
332 unsafe { libc::close(sv[1]) };
333 let listener = match recv_fd(sv[0]) {
334 Some(fd) => fd,
335 None => {
336 unsafe { libc::close(sv[0]) };
337 let mut st = 0;
338 unsafe { libc::waitpid(pid, &mut st, libc::__WALL) };
339 return Err(match libc::WEXITSTATUS(st) {
340 119 if ctx.config.bwrap_mode => {
341 exit_like_bwrap_without_namespaces("proot-next cannot run under another supervisor")
342 }
343 118 => "cannot enter the user namespace the probe entered".into(),
344 119 => "the seccomp filter chain already has a listener (a supervisor other than proot-next)"
345 .into(),
346 120 => "PR_SET_NO_NEW_PRIVS refused".into(),
347 121 => "cannot enter the alias directory".into(),
348 124 => "the caller's --seccomp program was refused by the kernel".into(),
349 122 => "seccomp user notification is not available on this kernel or is filtered".into(),
350 _ => "child failed before handing over the seccomp listener".into(),
351 });
352 }
353 };
354 if no_ptrace {
355 ctx.procs.register_root(
356 pid,
357 TraceeInfo {
358 cwd: Some(initial_cwd),
359 ..Default::default()
360 },
361 );
362 if let Err(e) = ProcTable::start_reaper(&ctx.procs) {
363 eprintln!("proot: cannot watch process exits: {}", strerror(e));
364 }
365 } else if let Err(e) = ptrace::seize(pid, tracer::options(&ctx)) {
366 unsafe {
367 libc::kill(pid, libc::SIGKILL);
368 libc::close(sv[0]);
369 libc::close(listener);
370 let mut st = 0;
371 libc::waitpid(pid, &mut st, libc::__WALL);
372 }
373 if e == libc::EPERM && ctx.config.bwrap_mode {
374 exit_like_bwrap_without_namespaces("proot-next cannot run under another supervisor");
375 }
376 return Err(match e {
377 libc::EPERM => "ptrace(PTRACE_SEIZE) refused: this process is already traced (a debugger, or a supervisor other than proot-next) or ptrace is disallowed here".into(),
378 e => format!("ptrace(PTRACE_SEIZE): {}", strerror(e)),
379 });
380 }
381 report_status(&ctx.config.info_fds, &format!("{{\"child-pid\": {pid}}}\n"));
382 report_status(
383 &ctx.config.json_status_fds,
384 &format!("{{\"child-pid\": {pid}}}\n"),
385 );
386 let go = if trap_mode {
387 match plan_first_trapped(&ctx, pid, env.as_deref()) {
388 Ok(()) => 0u8,
389 Err(libc::ENOENT) => 127,
390 Err(_) => 126,
391 }
392 } else {
393 0
394 };
395 let pool = notify::start(listener, ctx.clone());
396 if unsafe { libc::write(sv[0], &go as *const u8 as *const _, 1) } != 1 {
397 return Err("could not release the first tracee".into());
398 }
399 unsafe { libc::close(sv[0]) };
400 let code = if no_ptrace {
401 let code = wait_root(pid);
402 pool.wait_closed();
403 code
404 } else {
405 tracer::run(&ctx, pid)
406 };
407 unsafe { libc::close(listener) };
408 report_status(
409 &ctx.config.json_status_fds,
410 &format!("{{\"exit-code\": {code}}}\n"),
411 );
412 let keep = std::env::var_os("PROOT_BWRAP_KEEP").is_some();
414 ctx.root.remove_placeholders();
415 let _ = std::fs::remove_dir_all(crate::exec::alias_dir());
416 if no_ptrace {
417 direct::remove_staging();
418 }
419 if let Some(dir) = shim::path().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
420 let _ = std::fs::remove_dir_all(dir);
421 }
422 for dir in ctx.config.cleanup_dirs.iter().filter(|_| !keep) {
423 let _ = std::fs::remove_dir_all(std::ffi::OsStr::from_bytes_hack(dir));
424 }
425 Ok(code)
426}
427
428fn plan_first_trapped(ctx: &Ctx, pid: i32, env: Option<&[CString]>) -> crate::sys::Result<()> {
439 let argv: Vec<Vec<u8>> = std::iter::once(ctx.config.argv0.as_ref().unwrap_or(&ctx.config.command[0]))
440 .chain(ctx.config.command.iter().skip(1))
441 .map(|c| c.as_bytes().to_vec())
442 .collect();
443 let split = |e: &[u8]| match e.iter().position(|&b| b == b'=') {
444 Some(i) => (e[..i].to_vec(), e[i + 1..].to_vec()),
445 None => (e.to_vec(), Vec::new()),
446 };
447 let env: Vec<(Vec<u8>, Vec<u8>)> = match env {
448 Some(e) => e.iter().map(|c| split(c.as_bytes())).collect(),
449 None => std::env::vars_os()
450 .map(|(k, v)| (k.into_vec(), v.into_vec()))
451 .collect(),
452 };
453 direct::plan_first_trapped(ctx, pid, ctx.config.command[0].as_bytes(), argv, env)
454}
455
456fn guest_stub_choice() -> Option<bool> {
463 match std::env::var_os("PROOT_GUEST_STUB") {
464 Some(v) if v == "0" => None,
465 Some(v) => Some(v == "force"),
466 None => Some(false),
467 }
468}
469
470fn no_ptrace_forced() -> bool {
476 std::env::var_os("PROOT_NO_PTRACE").is_some_and(|v| v != "0")
477}
478
479fn exit_like_bwrap_without_namespaces(reason: &str) -> ! {
491 eprintln!("bwrap: No permissions to create a new namespace: {reason}");
492 std::process::exit(1);
493}
494
495fn become_subreaper() {
503 unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) };
504}
505
506fn wait_root(pid: i32) -> i32 {
518 let code = loop {
519 let mut status = 0;
520 let got = unsafe { libc::waitpid(-1, &mut status, 0) };
521 if got < 0 {
522 match errno() {
523 libc::EINTR => continue,
524 _ => break 1,
525 }
526 }
527 if got != pid {
528 continue;
529 }
530 if libc::WIFEXITED(status) {
531 break libc::WEXITSTATUS(status);
532 }
533 if libc::WIFSIGNALED(status) {
534 break 128 + libc::WTERMSIG(status);
535 }
536 };
537 std::thread::Builder::new()
538 .name("subreaper".into())
539 .spawn(|| {
540 loop {
541 let mut status = 0;
542 if unsafe { libc::waitpid(-1, &mut status, 0) } < 0 && errno() == libc::ECHILD {
543 std::thread::sleep(std::time::Duration::from_millis(50));
544 }
545 }
546 })
547 .expect("spawn subreaper thread");
548 code
549}
550
551fn report_status(fds: &[i32], line: &str) {
557 for &fd in fds {
558 unsafe { libc::write(fd, line.as_ptr() as *const _, line.len()) };
559 }
560}
561
562fn resolve_command_in_path(config: &mut Config) {
572 let Some(first) = config.command.first() else {
573 return;
574 };
575 let name = first.as_bytes().to_vec();
576 if name.is_empty() || name.contains(&b'/') {
577 return;
578 }
579 let path_var: Vec<u8> = match &config.env {
580 Some(env) => env
581 .iter()
582 .find_map(|e| e.as_bytes().strip_prefix(b"PATH=").map(|v| v.to_vec()))
583 .unwrap_or_default(),
584 None => std::env::var_os("PATH")
585 .map(|p| std::os::unix::ffi::OsStrExt::as_bytes(p.as_os_str()).to_vec())
586 .unwrap_or_default(),
587 };
588 let fs = crate::fs::Fs::new(config);
589 for dir in path_var.split(|&c| c == b':').filter(|d| !d.is_empty()) {
590 let mut guest = dir.to_vec();
591 if !guest.ends_with(b"/") {
592 guest.push(b'/');
593 }
594 guest.extend_from_slice(&name);
595 let Ok(host) = translate_path(&fs, std::process::id(), libc::AT_FDCWD, &guest, true) else {
596 continue;
597 };
598 let Ok(c) = CString::new(host) else { continue };
599 if unsafe { libc::access(c.as_ptr(), libc::X_OK) } == 0 {
600 if let Ok(g) = CString::new(guest) {
601 if config.argv0.is_none() {
603 config.argv0 = Some(config.command[0].clone());
604 }
605 config.command[0] = g;
606 }
607 return;
608 }
609 }
610}
611
612fn send_fd(sock: i32, fd: i32) -> bool {
614 let mut byte = 1u8;
615 let mut iov = libc::iovec {
616 iov_base: &mut byte as *mut u8 as *mut _,
617 iov_len: 1,
618 };
619 let mut cbuf = [0u8; 24];
620 cbuf[0..8].copy_from_slice(&20u64.to_ne_bytes());
621 cbuf[8..12].copy_from_slice(&libc::SOL_SOCKET.to_ne_bytes());
622 cbuf[12..16].copy_from_slice(&libc::SCM_RIGHTS.to_ne_bytes());
623 cbuf[16..20].copy_from_slice(&fd.to_ne_bytes());
624 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
625 msg.msg_iov = &mut iov;
626 msg.msg_iovlen = 1;
627 msg.msg_control = cbuf.as_mut_ptr() as *mut _;
628 msg.msg_controllen = 24;
629 unsafe { libc::sendmsg(sock, &msg, 0) == 1 }
630}
631
632fn recv_fd(sock: i32) -> Option<i32> {
634 let mut byte = 0u8;
635 let mut iov = libc::iovec {
636 iov_base: &mut byte as *mut u8 as *mut _,
637 iov_len: 1,
638 };
639 let mut cbuf = [0u8; 24];
640 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
641 msg.msg_iov = &mut iov;
642 msg.msg_iovlen = 1;
643 msg.msg_control = cbuf.as_mut_ptr() as *mut _;
644 msg.msg_controllen = 24;
645 let n = unsafe { libc::recvmsg(sock, &mut msg, 0) };
646 if n != 1 || msg.msg_controllen < 20 {
647 return None;
648 }
649 Some(i32::from_ne_bytes(cbuf[16..20].try_into().unwrap()))
650}
651
652trait FromBytesHack {
654 fn from_bytes_hack(b: &[u8]) -> &std::ffi::OsStr;
655}
656impl FromBytesHack for std::ffi::OsStr {
657 fn from_bytes_hack(b: &[u8]) -> &std::ffi::OsStr {
658 use std::os::unix::ffi::OsStrExt;
659 std::ffi::OsStr::from_bytes(b)
660 }
661}