1use crate::exec::nested::EnvSource;
38use crate::exec::shim::Trapped;
39use crate::exec::{self, ExecPlan, shim};
40use crate::fs::canon::RETRY;
41use crate::fs::translate::{guest_dirfd_path, guest_fd_path, read_link, strip_deleted};
42use crate::guest_stub::layout;
43use crate::note;
44use crate::seccomp::notify::{addfd, addfd_at, reply_continue};
45use crate::state::Ctx;
46use crate::sys::{Result, errno};
47use crate::syscall::notify::{Outcome, Req};
48use crate::syscall::nr::*;
49use std::ffi::{CString, OsStr};
50use std::os::unix::ffi::OsStrExt;
51use std::os::unix::fs::DirBuilderExt;
52use std::path::{Path, PathBuf};
53use std::sync::Mutex;
54use std::time::{Duration, Instant};
55
56const CLONE_VFORK: u64 = 0x4000;
58const CLONE_THREAD: u64 = 0x10000;
60const RESTORE_TIMEOUT: Duration = Duration::from_secs(2);
62const SETTLED_CPU: u64 = 100_000;
66
67const ALIAS_CHARS: &[u8] = b"0123456789abcdfghijklmnopqstuvwxyzABCDFGHIJKLMNOPQSTUVWXYZ";
70
71static ALIASES: Mutex<([bool; ALIAS_CHARS.len()], usize)> = Mutex::new(([false; ALIAS_CHARS.len()], 0));
75
76static STAGING: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
82
83pub fn dispatch(r: &Req, nr: i64) -> Option<Result<Outcome>> {
89 Some(match nr {
90 EXECVE | EXECVEAT if r.ctx.trap_mode() => do_execve_trapped(r, nr == EXECVEAT),
91 EXECVE => do_execve(r, false),
92 EXECVEAT => do_execve(r, true),
93 CHDIR => r.path(0).and_then(|p| set_cwd(r, libc::AT_FDCWD, &p)),
94 FCHDIR => guest_fd_path(r.fs(), r.pid, r.arg(0) as i32).and_then(|p| set_cwd(r, libc::AT_FDCWD, &p)),
95 CHROOT => do_chroot(r),
96 CONNECT => do_sockaddr(r, r.arg(0) as i32, r.arg(1), r.arg(2) as usize, true),
97 BIND => do_sockaddr(r, r.arg(0) as i32, r.arg(1), r.arg(2) as usize, false),
98 INOTIFY_ADD_WATCH => do_inotify_add_watch(r),
99 CLONE | FORK | VFORK | CLONE3 => do_clone(r, nr),
100 _ => return None,
101 })
102}
103
104pub fn shim_passthrough(r: &Req, nr: i64) -> Option<Outcome> {
120 if matches!(nr, EXECVE | EXECVEAT) || r.ctx.trap_mode() {
121 return None;
122 }
123 let tgid = r.tgid();
124 r.ctx.procs.shim_pending(tgid)?;
125 if shim::running(r.pid) {
126 return Some(Outcome::Continue);
127 }
128 r.ctx.procs.shim_leave(tgid);
129 None
130}
131
132pub fn do_socketcall(r: &Req) -> Result<Outcome> {
137 let call = r.arg(0);
138 if call != 2 && call != 3 {
139 return Ok(Outcome::Continue);
140 }
141 let raw = r.read(r.arg(1), 12)?;
142 let word = |i: usize| u32::from_ne_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap()) as u64;
143 do_sockaddr(r, word(0) as i32, word(1), word(2) as usize, call == 3)
144}
145
146fn do_execve(r: &Req, is_at: bool) -> Result<Outcome> {
165 let tgid = r.tgid();
166 if let Some(p) = r.ctx.procs.shim_pending(tgid) {
167 r.ctx.procs.shim_leave(tgid);
168 if shim::running(r.pid) {
169 if let Some((parent, addr, orig, written)) = &p.restore {
170 restore_if_intact(r, *parent as u32, *addr, orig, written);
171 }
172 exec::finish_pending(r.ctx, tgid, p);
173 return Ok(Outcome::Continue);
174 }
175 }
176 let (dirfd, path_idx, argv_idx, at_flags) = if is_at {
177 (r.arg(0) as i32, 1, 2, r.arg(4) as i32)
178 } else {
179 (libc::AT_FDCWD, 0, 1, 0)
180 };
181 let user_path = r.path(path_idx)?;
182 let argv = exec::read_argv(r.ctx, r.pid, r.arg(argv_idx), r.abi.ptr_size())?;
183 let env = EnvSource::Address {
184 envp: r.arg(argv_idx + 1),
185 ptr_size: r.abi.ptr_size(),
186 };
187 let empty_path = is_at && user_path.is_empty();
188 let plan = if empty_path {
189 if at_flags & libc::AT_EMPTY_PATH == 0 {
190 return Err(libc::ENOENT);
191 }
192 let guest = guest_dirfd_path(r.fs(), r.pid, dirfd)?;
193 exec::plan(r.ctx, r.pid, libc::AT_FDCWD, &guest, argv, true, env)?
194 } else {
195 let deref = at_flags & libc::AT_SYMLINK_NOFOLLOW == 0;
196 exec::plan(r.ctx, r.pid, dirfd, &user_path, argv, deref, env)?
197 };
198 if !is_at && !plan.changed {
199 r.ctx.set_exe(tgid, plan.guest_prog, plan.host_prog);
200 return Ok(Outcome::Continue);
201 }
202 if plan.trampoline.is_none() {
203 check_executable(&plan)?;
204 }
205 r.valid()?;
206 exec::debug_exec(r.pid, &plan);
207 shim::write_record(tgid, &plan, None)?;
208 let vfork_parent = r.ctx.tracee(tgid).vfork_parent;
209 let mut orig = user_path.clone();
210 orig.push(0);
211 let mut pending = exec::pending_of(&plan);
212 pending.restore = vfork_parent
213 .filter(|_| !empty_path)
214 .map(|parent| (parent, r.arg(path_idx), orig, b"e\0".to_vec()));
215 r.ctx.procs.shim_enter(tgid, pending);
216 let undo = |e: i32| {
217 r.ctx.procs.shim_leave(tgid);
218 if let Some(p) = shim::record_path(tgid) {
219 let _ = std::fs::remove_file(p);
220 }
221 e
222 };
223 let Some(shim_path) = shim::path() else {
224 return Err(undo(libc::ENOENT));
225 };
226 if empty_path {
227 let fd =
228 open_host(shim_path.as_os_str().as_bytes(), libc::O_RDONLY | libc::O_CLOEXEC).map_err(undo)?;
229 addfd_at(r.listener, r.id, fd, dirfd, true).map_err(undo)?;
230 return Ok(Outcome::Continue);
231 }
232 r.ctx
233 .mem
234 .write_force(r.pid, r.arg(path_idx), b"e\0")
235 .map_err(undo)?;
236 if is_at && dirfd != libc::AT_FDCWD && user_path.first() != Some(&b'/') {
237 let dir = shim_path
238 .parent()
239 .map(|d| d.as_os_str().as_bytes().to_vec())
240 .unwrap_or_default();
241 let fd = open_host(&dir, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC).map_err(undo)?;
242 addfd_at(r.listener, r.id, fd, dirfd, true).map_err(undo)?;
243 }
244 Ok(Outcome::Continue)
245}
246
247fn do_execve_trapped(r: &Req, is_at: bool) -> Result<Outcome> {
268 let (dirfd, path_idx, argv_idx, at_flags) = if is_at {
269 (r.arg(0) as i32, 1, 2, r.arg(4) as i32)
270 } else {
271 (libc::AT_FDCWD, 0, 1, 0)
272 };
273 let truncated = r.arg(layout::EXEC_TRUNCATED_ARG) != 0;
274 let user_path = r.path(path_idx)?;
275 let argv = exec::read_argv(r.ctx, r.pid, r.arg(argv_idx), r.abi.ptr_size())?;
276 let env = EnvSource::Address {
277 envp: r.arg(argv_idx + 1),
278 ptr_size: r.abi.ptr_size(),
279 };
280 let plan = if is_at && user_path.is_empty() {
281 if at_flags & libc::AT_EMPTY_PATH == 0 {
282 return Err(libc::ENOENT);
283 }
284 let guest = guest_dirfd_path(r.fs(), r.pid, dirfd)?;
285 exec::plan(r.ctx, r.pid, libc::AT_FDCWD, &guest, argv, true, env)?
286 } else {
287 let deref = at_flags & libc::AT_SYMLINK_NOFOLLOW == 0;
288 exec::plan(r.ctx, r.pid, dirfd, &user_path, argv, deref, env)?
289 };
290 if truncated && plan.trampoline.is_some() {
291 return Err(libc::E2BIG);
292 }
293 let narrow = check_loadable(r.ctx, &plan)?;
294 r.valid()?;
295 exec::debug_exec(r.pid, &plan);
296 let mailbox = r.ctx.mailbox.ok_or(libc::ENOSYS)?;
297 let copy = unsafe { libc::fcntl(mailbox, libc::F_DUPFD_CLOEXEC, 0) };
298 if copy < 0 {
299 return Err(errno());
300 }
301 let fd = addfd(r.listener, r.id, copy, false)?;
302 commit_trapped(r.ctx, r.tgid(), &plan, fd, narrow)?;
303 Ok(Outcome::Value(Ok(fd as i64)))
304}
305
306pub fn plan_first_trapped(
320 ctx: &Ctx,
321 pid: i32,
322 path: &[u8],
323 argv: Vec<Vec<u8>>,
324 env: Vec<(Vec<u8>, Vec<u8>)>,
325) -> Result<()> {
326 let plan = exec::plan(
327 ctx,
328 pid as u32,
329 libc::AT_FDCWD,
330 path,
331 argv,
332 true,
333 EnvSource::Given(env),
334 )?;
335 let narrow = check_loadable(ctx, &plan)?;
336 exec::debug_exec(pid as u32, &plan);
337 commit_trapped(ctx, pid, &plan, ctx.mailbox.ok_or(libc::ENOSYS)?, narrow)
338}
339
340fn commit_trapped(ctx: &Ctx, tgid: i32, plan: &ExecPlan, mailbox: i32, narrow: bool) -> Result<()> {
353 let execfn = if plan.user_path.is_empty() {
354 plan.guest_prog.clone()
355 } else {
356 plan.user_path.clone()
357 };
358 let mut name = exec::exec_name(&plan.user_path, &plan.guest_prog).to_vec();
359 name.truncate(15);
360 let trapped = Trapped {
361 mailbox,
362 execfn,
363 name,
364 narrow,
365 };
366 shim::write_record(tgid, plan, Some(&trapped))?;
367 let mut pending = exec::pending_of(plan);
368 if plan.trampoline.is_none() && plan.prefix > 1 {
369 pending.cmdline_prefix = Some(plan.argv[..plan.prefix].to_vec());
370 }
371 ctx.procs.shim_enter(tgid, pending);
372 Ok(())
373}
374
375fn check_loadable(ctx: &Ctx, plan: &ExecPlan) -> Result<bool> {
393 if plan.trampoline.is_some() {
394 return Ok(!plan.exec_path.is_empty()
395 && matches!(
396 exec::elf::classify_file(&plan.exec_path),
397 Ok(exec::elf::FileKind::Elf32(_))
398 ));
399 }
400 check_executable(plan)?;
401 match exec::elf::classify_file(&plan.exec_path) {
402 Ok(exec::elf::FileKind::Elf64(_)) => Ok(false),
403 Ok(exec::elf::FileKind::Elf32(_)) if shim::has_32bit() => Ok(true),
404 Ok(exec::elf::FileKind::Elf32(_)) => {
405 note!(
406 ctx,
407 1,
408 "{}: 32-bit programs cannot run here (no i386 trampoline)",
409 String::from_utf8_lossy(&plan.guest_prog)
410 );
411 Err(libc::EOPNOTSUPP)
412 }
413 _ => Err(libc::ENOEXEC),
414 }
415}
416
417pub fn hello(ctx: &Ctx, tid: u32) {
425 let tgid = ctx.tgid(tid as i32);
426 if let Some(p) = ctx.procs.shim_pending(tgid) {
427 ctx.procs.shim_leave(tgid);
428 exec::finish_pending(ctx, tgid, p);
429 }
430}
431
432fn check_executable(plan: &exec::ExecPlan) -> Result<()> {
437 let c = CString::new(plan.host_prog.clone()).map_err(|_| libc::ENOENT)?;
438 let mut st: libc::stat = unsafe { std::mem::zeroed() };
439 if unsafe { libc::stat(c.as_ptr(), &mut st) } < 0 {
440 return Err(errno());
441 }
442 if st.st_mode & libc::S_IFMT == libc::S_IFDIR {
443 return Err(libc::EACCES);
444 }
445 if unsafe { libc::access(c.as_ptr(), libc::X_OK) } < 0 {
446 return Err(errno());
447 }
448 if !plan.understood && !binfmt_misc_matches(&plan.host_prog) {
449 return Err(libc::ENOEXEC);
450 }
451 Ok(())
452}
453
454fn binfmt_misc_matches(host: &[u8]) -> bool {
460 let Ok(entries) = std::fs::read_dir("/proc/sys/fs/binfmt_misc") else {
461 return false;
462 };
463 let head = std::fs::read(std::ffi::OsStr::from_bytes(host))
464 .map(|mut d| {
465 d.truncate(256);
466 d
467 })
468 .unwrap_or_default();
469 let name = host.rsplit(|&b| b == b'/').next().unwrap_or(host);
470 for entry in entries.flatten() {
471 if entry.file_name() == "register" || entry.file_name() == "status" {
472 continue;
473 }
474 let Ok(text) = std::fs::read_to_string(entry.path()) else {
475 continue;
476 };
477 if !text.starts_with("enabled") {
478 continue;
479 }
480 let field = |key: &str| {
481 text.lines()
482 .find_map(|l| l.strip_prefix(key))
483 .map(|v| v.trim().to_string())
484 };
485 if let Some(ext) = field("extension ") {
486 if name.ends_with(format!(".{ext}").as_bytes()) {
487 return true;
488 }
489 continue;
490 }
491 let (Some(magic), offset) = (
492 field("magic "),
493 field("offset ")
494 .and_then(|o| o.parse::<usize>().ok())
495 .unwrap_or(0),
496 ) else {
497 continue;
498 };
499 let unhex = |h: &str| -> Vec<u8> {
500 h.as_bytes()
501 .chunks(2)
502 .filter_map(|c| u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok())
503 .collect()
504 };
505 let magic = unhex(&magic);
506 let mask = field("mask ")
507 .map(|m| unhex(&m))
508 .unwrap_or_else(|| vec![0xff; magic.len()]);
509 if head.len() < offset + magic.len() {
510 continue;
511 }
512 if magic
513 .iter()
514 .zip(mask.iter().chain(std::iter::repeat(&0xff)))
515 .zip(&head[offset..])
516 .all(|((m, k), b)| b & k == m & k)
517 {
518 return true;
519 }
520 }
521 false
522}
523
524fn open_host(host: &[u8], flags: i32) -> Result<i32> {
526 let c = CString::new(host.to_vec()).map_err(|_| libc::EINVAL)?;
527 let fd = unsafe { libc::open(c.as_ptr(), flags) };
528 if fd < 0 { Err(errno()) } else { Ok(fd) }
529}
530
531fn set_cwd(r: &Req, dirfd: i32, path: &[u8]) -> Result<Outcome> {
533 let guest = retry(|| {
534 let resolved = r.translate(dirfd, path, true)?;
535 let mode = resolved.mode_nofollow()?;
536 if mode & libc::S_IFMT == libc::S_IFLNK && resolved.pinned() {
537 return Err(RETRY);
538 }
539 if mode & libc::S_IFMT != libc::S_IFDIR {
540 return Err(libc::ENOTDIR);
541 }
542 let (dfd, c, nofollow) = resolved.at(false)?;
543 let flags = if nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 };
544 if unsafe { libc::syscall(libc::SYS_faccessat2, dfd, c.as_ptr(), libc::X_OK, flags) } < 0 {
545 return Err(errno());
546 }
547 Ok(resolved.guest)
548 })?;
549 r.valid()?;
550 r.ctx.procs.set_cwd(r.pid as i32, guest);
551 Ok(Outcome::Value(Ok(0)))
552}
553
554fn retry<T>(mut f: impl FnMut() -> Result<T>) -> Result<T> {
556 for _ in 0..3 {
557 match f() {
558 Err(RETRY) => continue,
559 r => return r,
560 }
561 }
562 Err(libc::ELOOP)
563}
564
565fn do_chroot(r: &Req) -> Result<Outcome> {
568 let resolved = r.translate(libc::AT_FDCWD, &r.path(0)?, true)?;
569 if resolved.guest == b"/" {
570 return Ok(Outcome::Value(Ok(0)));
571 }
572 Err(libc::EPERM)
573}
574
575fn do_sockaddr(r: &Req, sockfd: i32, addr: u64, len: usize, connect: bool) -> Result<Outcome> {
596 if addr == 0 || len <= 2 || len > 110 {
597 return Ok(Outcome::Continue);
598 }
599 let sa = r.read(addr, len)?;
600 if u16::from_ne_bytes([sa[0], sa[1]]) != libc::AF_UNIX as u16 || sa[2] == 0 {
601 return Ok(Outcome::Continue);
602 }
603 let end = sa[2..].iter().position(|&b| b == 0).map(|p| p + 2).unwrap_or(len);
604 let path = sa[2..end].to_vec();
605 let resolved = r.translate(libc::AT_FDCWD, &path, connect)?;
606 if resolved.host == path {
607 return Ok(Outcome::Continue);
608 }
609 if !connect {
610 r.check_writable(&resolved)?;
611 }
612 let host = resolved.parent_path()?.into_bytes();
613 if host.len() >= 108 {
614 return Err(libc::ENAMETOOLONG);
615 }
616 let Some((kind, flags)) = socket_description(r.pid, sockfd) else {
617 return Ok(Outcome::Continue);
618 };
619 let nonblock = flags & libc::O_NONBLOCK != 0;
620 let cloexec = flags & libc::O_CLOEXEC != 0;
621 let new = unsafe {
622 libc::socket(
623 libc::AF_UNIX,
624 kind | libc::SOCK_CLOEXEC | if nonblock { libc::SOCK_NONBLOCK } else { 0 },
625 0,
626 )
627 };
628 if new < 0 {
629 return Err(errno());
630 }
631 let mut sun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
632 sun.sun_family = libc::AF_UNIX as u16;
633 for (i, b) in host.iter().enumerate() {
634 sun.sun_path[i] = *b as libc::c_char;
635 }
636 let sun_len = (2 + host.len() + 1) as libc::socklen_t;
637 let sun_ptr = &sun as *const libc::sockaddr_un as *const libc::sockaddr;
638 let rc = unsafe {
639 if connect {
640 libc::connect(new, sun_ptr, sun_len)
641 } else {
642 libc::bind(new, sun_ptr, sun_len)
643 }
644 };
645 if rc < 0 {
646 let e = errno();
647 unsafe { libc::close(new) };
648 return Err(e);
649 }
650 note!(
651 r.ctx,
652 2,
653 "pid {} {} fd {} -> {}",
654 r.pid,
655 if connect { "connect" } else { "bind" },
656 sockfd,
657 String::from_utf8_lossy(&resolved.host)
658 );
659 r.valid().inspect_err(|_| unsafe {
660 libc::close(new);
661 })?;
662 addfd_at(r.listener, r.id, new, sockfd, cloexec)?;
663 Ok(Outcome::Value(Ok(0)))
664}
665
666fn socket_description(tid: u32, fd: i32) -> Option<(i32, i32)> {
678 let link = read_link(format!("/proc/{tid}/fd/{fd}").as_bytes()).ok()?;
679 let inode = link.strip_prefix(b"socket:[")?.strip_suffix(b"]")?;
680 let inode = std::str::from_utf8(inode).ok()?;
681 let table = std::fs::read_to_string("/proc/net/unix").ok()?;
682 let kind = table.lines().skip(1).find_map(|l| {
683 let f: Vec<&str> = l.split_whitespace().collect();
684 if f.len() >= 7 && f[6] == inode {
685 i32::from_str_radix(f[4], 16).ok()
686 } else {
687 None
688 }
689 })?;
690 let info = std::fs::read_to_string(format!("/proc/{tid}/fdinfo/{fd}")).ok()?;
691 let flags = info
692 .lines()
693 .find_map(|l| l.strip_prefix("flags:"))
694 .and_then(|v| i32::from_str_radix(v.trim(), 8).ok())?;
695 Some((kind, flags))
696}
697
698fn do_inotify_add_watch(r: &Req) -> Result<Outcome> {
701 const IN_DONT_FOLLOW: u64 = 0x0200_0000;
702 let path = r.path(1)?;
703 let dont_follow = r.arg(2) & IN_DONT_FOLLOW != 0;
704 let resolved = r.translate(libc::AT_FDCWD, &path, !dont_follow)?;
705 if resolved.host == path {
706 return Ok(Outcome::Continue);
707 }
708 let form = if dont_follow
709 && resolved
710 .mode_nofollow()
711 .is_ok_and(|m| m & libc::S_IFMT == libc::S_IFLNK)
712 {
713 Form::Dir
714 } else {
715 Form::File
716 };
717 let mut orig = path.clone();
718 orig.push(0);
719 transient_rewrite(r, r.arg(1), &orig, &resolved.host, form)
720}
721
722pub fn do_open_path(r: &Req, dirfd: i32, path: &[u8], path_addr: u64, flags: i64) -> Result<Outcome> {
741 let nofollow = flags & libc::O_NOFOLLOW as i64 != 0;
742 let resolved = r.translate(dirfd, path, !nofollow)?;
743 if r.ctx.trap_mode() {
744 return rewrite_staged(r, path_addr, &resolved.host);
745 }
746 let cloexec = flags & libc::O_CLOEXEC as i64 != 0;
747 let kind = resolved.mode_nofollow()? & libc::S_IFMT;
748 if flags & libc::O_DIRECTORY as i64 != 0 && kind != libc::S_IFDIR {
749 return Err(libc::ENOTDIR);
750 }
751 let (dfd, c, nofollow) = resolved.at(true)?;
752 let base = libc::O_RDONLY | libc::O_CLOEXEC | if nofollow { libc::O_NOFOLLOW } else { 0 };
753 let direct = match kind {
754 libc::S_IFDIR => Some(base | libc::O_DIRECTORY),
755 libc::S_IFREG => Some(base),
756 _ => None,
757 };
758 if let Some(open_flags) = direct {
759 r.valid()?;
760 let fd = unsafe { libc::openat(dfd, c.as_ptr(), open_flags) };
761 if fd >= 0 {
762 return Ok(Outcome::Fd { fd, cloexec });
763 }
764 if errno() != libc::EACCES {
765 return Err(errno());
766 }
767 }
768 if dirfd != libc::AT_FDCWD && path.first() != Some(&b'/') {
769 if kernel_walks_alike(r.pid, dirfd, path, &resolved.host) {
770 return Ok(Outcome::Continue);
771 }
772 return Err(libc::EACCES);
773 }
774 let form = if nofollow { Form::Dir } else { Form::File };
775 let mut orig = path.to_vec();
776 orig.push(0);
777 transient_rewrite(r, path_addr, &orig, &resolved.host, form)
778}
779
780fn kernel_walks_alike(pid: u32, dirfd: i32, path: &[u8], host: &[u8]) -> bool {
793 let Ok(dir) = read_link(format!("/proc/{pid}/fd/{dirfd}").as_bytes()) else {
794 return false;
795 };
796 let mut joined = strip_deleted(dir);
797 if joined.first() != Some(&b'/') {
798 return false;
799 }
800 if joined != b"/" {
801 joined.push(b'/');
802 }
803 joined.extend_from_slice(path);
804 joined == host
805}
806
807pub fn do_clone(r: &Req, nr: i64) -> Result<Outcome> {
817 let flags = match nr {
818 CLONE => r.arg(0),
819 CLONE3 => {
820 let raw = r.read(r.arg(0), 8)?;
821 u64::from_ne_bytes(raw.try_into().unwrap())
822 }
823 VFORK => CLONE_VFORK,
824 _ => 0,
825 };
826 if flags & crate::syscall::CLONE_NEWNS_FLAGS != 0 {
827 return Err(libc::EPERM);
828 }
829 if r.ctx.no_ptrace && flags & CLONE_THREAD == 0 {
830 r.ctx.procs.forked(r.pid as i32, flags & CLONE_VFORK != 0);
831 }
832 Ok(Outcome::Continue)
833}
834
835#[derive(Clone, Copy, PartialEq, Eq)]
837enum Form {
838 File,
840 Dir,
845}
846
847struct Placed {
850 repl: Vec<u8>,
851 staged: Option<PathBuf>,
852}
853
854fn transient_rewrite(r: &Req, addr: u64, orig: &[u8], host: &[u8], form: Form) -> Result<Outcome> {
870 if r.ctx.trap_mode() {
871 return rewrite_staged(r, addr, host);
872 }
873 let dir = shim::dir_bytes().ok_or(libc::ENOENT)?;
874 let name = acquire_alias().ok_or(libc::EAGAIN)?;
875 let placed = match place_alias(&dir, name, orig.len(), host, form, &staging_candidates(r.ctx)) {
876 Ok(placed) => placed,
877 Err(e) => {
878 release_alias(name);
879 return Err(e);
880 }
881 };
882 let fail = |e: i32| {
883 unstage(&placed);
884 release_alias(name);
885 e
886 };
887 let mut written = placed.repl.clone();
888 written.push(0);
889 r.valid().map_err(fail)?;
890 let cpu_before = cpu_time(r.pid);
891 r.ctx.mem.write_force(r.pid, addr, &written).map_err(fail)?;
892 note!(
893 r.ctx,
894 2,
895 "pid {} rewrote {} -> {} for {}",
896 r.pid,
897 String::from_utf8_lossy(&orig[..orig.len().saturating_sub(1)]),
898 String::from_utf8_lossy(&placed.repl),
899 String::from_utf8_lossy(host)
900 );
901 reply_continue(r.listener, r.id);
902 wait_syscall_consumed(r, cpu_before);
903 restore_if_intact(r, r.pid, addr, orig, &written);
904 unstage(&placed);
905 release_alias(name);
906 Ok(Outcome::Replied)
907}
908
909fn place_alias(
930 dir: &[u8],
931 name: u8,
932 room: usize,
933 host: &[u8],
934 form: Form,
935 candidates: &[PathBuf],
936) -> Result<Placed> {
937 let link = Path::new(OsStr::from_bytes(dir)).join(OsStr::from_bytes(&[name]));
938 let _ = std::fs::remove_file(&link);
939 let (target, repl): (Vec<u8>, Vec<u8>) = match form {
940 Form::File => (host.to_vec(), vec![name]),
941 Form::Dir => {
942 let cut = host.iter().rposition(|&b| b == b'/').unwrap_or(0);
943 let parent = if cut == 0 {
944 b"/".to_vec()
945 } else {
946 host[..cut].to_vec()
947 };
948 let mut p = vec![name, b'/'];
949 p.extend_from_slice(&host[cut + 1..]);
950 (parent, p)
951 }
952 };
953 if repl.len() < room {
954 std::os::unix::fs::symlink(OsStr::from_bytes(&target), &link).map_err(io_errno)?;
955 return Ok(Placed { repl, staged: None });
956 }
957 if form != Form::Dir || room < 2 {
958 return Err(libc::ENAMETOOLONG);
959 }
960 if link_nofollow(host, &link).is_ok() {
961 return Ok(Placed {
962 repl: vec![name],
963 staged: Some(link),
964 });
965 }
966 if room < 4 {
967 return Err(libc::ENAMETOOLONG);
968 }
969 let staged = stage_hard_link(host, name, candidates).ok_or(libc::ENAMETOOLONG)?;
970 let stage_dir = staged.parent().ok_or(libc::ENAMETOOLONG)?;
971 if let Err(e) = std::os::unix::fs::symlink(stage_dir, &link) {
972 let _ = std::fs::remove_file(&staged);
973 return Err(io_errno(e));
974 }
975 Ok(Placed {
976 repl: vec![name, b'/', name],
977 staged: Some(staged),
978 })
979}
980
981fn unstage(placed: &Placed) {
987 if let Some(p) = &placed.staged {
988 let _ = std::fs::remove_file(p);
989 }
990}
991
992fn io_errno(e: std::io::Error) -> i32 {
997 e.raw_os_error().unwrap_or(libc::EIO)
998}
999
1000fn link_nofollow(host: &[u8], at: &Path) -> Result<()> {
1010 let src = CString::new(host.to_vec()).map_err(|_| libc::EINVAL)?;
1011 let dst = CString::new(at.as_os_str().as_bytes().to_vec()).map_err(|_| libc::EINVAL)?;
1012 if unsafe { libc::linkat(libc::AT_FDCWD, src.as_ptr(), libc::AT_FDCWD, dst.as_ptr(), 0) } < 0 {
1013 return Err(errno());
1014 }
1015 Ok(())
1016}
1017
1018fn stage_hard_link(host: &[u8], name: u8, candidates: &[PathBuf]) -> Option<PathBuf> {
1031 let link_in = |d: &Path| {
1032 let p = d.join(OsStr::from_bytes(&[name]));
1033 let _ = std::fs::remove_file(&p);
1034 link_nofollow(host, &p).ok().map(|()| p)
1035 };
1036 let mut staging = STAGING.lock().unwrap();
1037 if let Some(p) = staging.iter().find_map(|d| link_in(d)) {
1038 return Some(p);
1039 }
1040 for base in candidates {
1041 let d = base.join(format!("proot-next-{}-stage", std::process::id()));
1042 if std::fs::DirBuilder::new().mode(0o700).create(&d).is_err() {
1043 continue;
1044 }
1045 if let Some(p) = link_in(&d) {
1046 staging.push(d);
1047 return Some(p);
1048 }
1049 let _ = std::fs::remove_dir(&d);
1050 }
1051 None
1052}
1053
1054fn staging_candidates(ctx: &Ctx) -> Vec<PathBuf> {
1060 let mut v = Vec::new();
1061 if let Some(c) = std::env::var_os("XDG_CACHE_HOME").filter(|c| !c.is_empty()) {
1062 v.push(PathBuf::from(c));
1063 } else if let Some(h) = std::env::var_os("HOME") {
1064 v.push(PathBuf::from(h).join(".cache"));
1065 }
1066 if let Some(parent) = Path::new(OsStr::from_bytes(&ctx.config.root)).parent() {
1067 v.push(parent.to_path_buf());
1068 }
1069 v.push(std::env::temp_dir());
1070 v
1071}
1072
1073pub fn remove_staging() {
1075 for d in STAGING.lock().unwrap().drain(..) {
1076 let _ = std::fs::remove_dir_all(d);
1077 }
1078}
1079
1080fn under_alias(host: &[u8]) -> bool {
1085 let within = |d: &[u8]| host.len() > d.len() && host.starts_with(d) && host[d.len()] == b'/';
1086 shim::dir_bytes().is_some_and(|d| within(&d))
1087 || STAGING
1088 .lock()
1089 .unwrap()
1090 .iter()
1091 .any(|d| within(d.as_os_str().as_bytes()))
1092}
1093
1094pub fn readlink_alias_fd(r: &Req, dirfd: i32, path: &[u8]) -> Option<Result<Vec<u8>>> {
1107 if !r.ctx.no_ptrace || !path.is_empty() || dirfd < 0 {
1108 return None;
1109 }
1110 let proc_link = format!("/proc/{}/fd/{}", r.pid, dirfd);
1111 let host = strip_deleted(read_link(proc_link.as_bytes()).ok()?);
1112 if !under_alias(&host) {
1113 return None;
1114 }
1115 Some(readlink_reopened(&proc_link))
1116}
1117
1118fn readlink_reopened(proc_link: &str) -> Result<Vec<u8>> {
1129 let c = CString::new(proc_link).map_err(|_| libc::EINVAL)?;
1130 let fd = unsafe { libc::open(c.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
1131 if fd < 0 {
1132 return Err(errno());
1133 }
1134 let mut buf = vec![0u8; libc::PATH_MAX as usize];
1135 let n = unsafe { libc::readlinkat(fd, c"".as_ptr(), buf.as_mut_ptr() as *mut libc::c_char, buf.len()) };
1136 let e = errno();
1137 unsafe { libc::close(fd) };
1138 if n < 0 {
1139 return Err(if e == libc::EINVAL { libc::ENOENT } else { e });
1140 }
1141 buf.truncate(n as usize);
1142 Ok(buf)
1143}
1144
1145fn rewrite_staged(r: &Req, addr: u64, host: &[u8]) -> Result<Outcome> {
1157 if host.len() >= layout::PATH_AREA {
1158 return Err(libc::ENAMETOOLONG);
1159 }
1160 let mut bytes = host.to_vec();
1161 bytes.push(0);
1162 r.valid()?;
1163 r.ctx.mem.write(r.pid, addr, &bytes)?;
1164 note!(
1165 r.ctx,
1166 2,
1167 "pid {} rewrote the staged path to {}",
1168 r.pid,
1169 String::from_utf8_lossy(host)
1170 );
1171 Ok(Outcome::Continue)
1172}
1173
1174fn restore_if_intact(r: &Req, pid: u32, addr: u64, orig: &[u8], written: &[u8]) {
1188 let mut cur = vec![0u8; orig.len()];
1189 let intact = r.ctx.mem.read(pid, addr, &mut cur) == Ok(orig.len())
1190 && cur[..written.len()] == *written
1191 && cur[written.len()..] == orig[written.len()..];
1192 if intact {
1193 let _ = r.ctx.mem.write_force(pid, addr, orig);
1194 } else {
1195 note!(
1196 r.ctx,
1197 2,
1198 "pid {pid} rewritten string at {addr:#x} not restored: memory reused"
1199 );
1200 }
1201}
1202
1203fn wait_syscall_consumed(r: &Req, cpu_before: Option<u64>) {
1219 let path = format!("/proc/{}/syscall", r.pid);
1220 let start = Instant::now();
1221 let mut polls = 0u32;
1222 while start.elapsed() < RESTORE_TIMEOUT {
1223 match (cpu_before, cpu_time(r.pid)) {
1224 (Some(before), Some(now)) if now.saturating_sub(before) >= SETTLED_CPU => return,
1225 (Some(_), None) => return,
1226 (None, _) => {
1227 std::thread::sleep(Duration::from_millis(2));
1228 return;
1229 }
1230 _ => {}
1231 }
1232 let Ok(s) = std::fs::read_to_string(&path) else {
1233 return;
1234 };
1235 let mut it = s.split_whitespace();
1236 let first = it.next().unwrap_or("");
1237 if first != "running" {
1238 let in_it = first.parse::<i64>().ok() == Some(r.raw_nr as i64)
1239 && (0..6).all(|i| {
1240 it.next()
1241 .and_then(|a| u64::from_str_radix(a.trim_start_matches("0x"), 16).ok())
1242 == Some(r.args[i])
1243 });
1244 if !in_it {
1245 return;
1246 }
1247 }
1248 polls += 1;
1249 if polls < 50 {
1250 std::thread::yield_now();
1251 } else {
1252 std::thread::sleep(Duration::from_micros(200.min(50 * polls as u64)));
1253 }
1254 }
1255}
1256
1257fn cpu_time(tid: u32) -> Option<u64> {
1262 let s = std::fs::read_to_string(format!("/proc/{tid}/schedstat")).ok()?;
1263 s.split_whitespace().next()?.parse().ok()
1264}
1265
1266fn acquire_alias() -> Option<u8> {
1268 let mut g = ALIASES.lock().unwrap();
1269 let (used, next) = &mut *g;
1270 let n = ALIAS_CHARS.len();
1271 let i = (0..n).map(|k| (*next + k) % n).find(|&i| !used[i])?;
1272 used[i] = true;
1273 *next = (i + 1) % n;
1274 Some(ALIAS_CHARS[i])
1275}
1276
1277fn release_alias(name: u8) {
1280 if let Some(i) = ALIAS_CHARS.iter().position(|&c| c == name) {
1281 ALIASES.lock().unwrap().0[i] = false;
1282 }
1283}
1284
1285pub fn ptrace_usable() -> bool {
1291 let rc = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, std::process::id() as i32, 0usize, 0usize) };
1292 rc < 0 && errno() == libc::ESRCH
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298
1299 #[test]
1300 fn alias_names_are_short_and_rotate() {
1301 let a = acquire_alias().unwrap();
1302 let b = acquire_alias().unwrap();
1303 assert_ne!(a, b);
1304 assert!(a != b'e' && a != b'r' && b != b'e' && b != b'r');
1305 release_alias(a);
1306 release_alias(b);
1307 let c = acquire_alias().unwrap();
1308 assert_ne!(c, a);
1309 assert_ne!(c, b);
1310 release_alias(c);
1311 }
1312
1313 #[test]
1314 fn describes_own_unix_socket() {
1315 let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_DGRAM | libc::SOCK_NONBLOCK, 0) };
1316 assert!(fd >= 0);
1317 let (kind, flags) = socket_description(std::process::id(), fd).expect("own socket is described");
1318 assert_eq!(kind, libc::SOCK_DGRAM);
1319 assert!(flags & libc::O_NONBLOCK != 0);
1320 assert!(flags & libc::O_CLOEXEC == 0);
1321 unsafe { libc::close(fd) };
1322 assert!(socket_description(std::process::id(), fd).is_none());
1323 }
1324
1325 #[test]
1326 fn ptrace_probe_does_not_panic() {
1327 let _ = ptrace_usable();
1328 }
1329
1330 fn scratch(name: &str) -> PathBuf {
1331 let d = std::env::temp_dir().join(format!("proot-direct-{name}-{}", std::process::id()));
1332 let _ = std::fs::remove_dir_all(&d);
1333 std::fs::create_dir_all(&d).unwrap();
1334 d
1335 }
1336
1337 #[test]
1338 fn hard_link_alias_is_the_symlink_itself() {
1339 use std::os::unix::fs::MetadataExt;
1340 let d = scratch("hardlink");
1341 let link = d.join("link");
1342 std::os::unix::fs::symlink("target", &link).unwrap();
1343 let alias_dir = d.join("alias");
1344 std::fs::create_dir(&alias_dir).unwrap();
1345 let (dir, host) = (alias_dir.as_os_str().as_bytes(), link.as_os_str().as_bytes());
1346 let name = acquire_alias().unwrap();
1347 let want = std::fs::symlink_metadata(&link).unwrap();
1348 let placed = place_alias(dir, name, b"link\0".len(), host, Form::Dir, &[]).unwrap();
1349 assert_eq!(placed.repl, vec![name]);
1350 let got = std::fs::symlink_metadata(alias_dir.join(OsStr::from_bytes(&[name]))).unwrap();
1351 assert!(got.file_type().is_symlink());
1352 assert_eq!((got.dev(), got.ino()), (want.dev(), want.ino()));
1353 assert_eq!(std::fs::symlink_metadata(&link).unwrap().nlink(), 2);
1354 unstage(&placed);
1355 assert_eq!(std::fs::symlink_metadata(&link).unwrap().nlink(), 1);
1356 assert_eq!(
1357 place_alias(dir, name, b"\0".len(), host, Form::Dir, &[]).err(),
1358 Some(libc::ENAMETOOLONG)
1359 );
1360 let fits = place_alias(dir, name, b"./link\0".len(), host, Form::Dir, &[]).unwrap();
1361 assert!(fits.staged.is_none());
1362 assert_eq!(fits.repl, [&[name, b'/'][..], b"link"].concat());
1363 release_alias(name);
1364 std::fs::remove_dir_all(d).unwrap();
1365 }
1366
1367 #[test]
1368 fn staging_directory_holds_a_link_on_the_symlinks_mount() {
1369 use std::os::unix::fs::MetadataExt;
1370 let d = scratch("staging");
1371 let link = d.join("link");
1372 std::os::unix::fs::symlink("target", &link).unwrap();
1373 let host = link.as_os_str().as_bytes();
1374 let name = acquire_alias().unwrap();
1375 let staged =
1376 stage_hard_link(host, name, &[d.join("missing"), d.clone()]).expect("a staging directory");
1377 let stage_dir = staged.parent().unwrap().to_path_buf();
1378 assert_eq!(stage_dir.parent(), Some(d.as_path()));
1379 let (want, got) = (
1380 std::fs::symlink_metadata(&link).unwrap(),
1381 std::fs::symlink_metadata(&staged).unwrap(),
1382 );
1383 assert_eq!((got.dev(), got.ino()), (want.dev(), want.ino()));
1384 assert!(under_alias(staged.as_os_str().as_bytes()));
1385 assert!(!under_alias(host));
1386 assert_eq!(stage_hard_link(host, name, &[]), Some(staged.clone()));
1387 remove_staging();
1388 assert!(!stage_dir.exists());
1389 release_alias(name);
1390 std::fs::remove_dir_all(d).unwrap();
1391 }
1392
1393 #[test]
1394 fn kernel_walks_a_single_component_from_a_directory_descriptor() {
1395 let d = scratch("walk");
1396 std::os::unix::fs::symlink("target", d.join("link")).unwrap();
1397 let c = CString::new(d.as_os_str().as_bytes()).unwrap();
1398 let fd = unsafe { libc::open(c.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC) };
1399 assert!(fd >= 0);
1400 let host = d.join("link");
1401 let host = host.as_os_str().as_bytes();
1402 let pid = std::process::id();
1403 assert!(kernel_walks_alike(pid, fd, b"link", host));
1404 assert!(!kernel_walks_alike(pid, fd, b"./link", host));
1405 assert!(!kernel_walks_alike(pid, fd, b"link", b"/elsewhere/link"));
1406 assert!(!kernel_walks_alike(pid, -1, b"link", host));
1407 unsafe { libc::close(fd) };
1408 std::fs::remove_dir_all(d).unwrap();
1409 }
1410}