proot/exec/mod.rs
1//! `execve` handling at the ptrace stop.
2//!
3//! The kernel resolves the program path and its `PT_INTERP` on the host, so
4//! both must be rewritten before the syscall proceeds:
5//!
6//! 1. the program path is translated, and `#!` scripts are expanded into
7//! interpreter plus argument, repeatedly, until an ELF is reached;
8//! 2. a dynamically linked ELF is not executed directly. One of the host's
9//! machine is exec'd through the loader the exec shim installs (see
10//! [`shim::loader_path`]), with the program's guest path as an argument:
11//! the loader opens the program and the interpreter its `PT_INTERP` names
12//! through `openat`, which the handlers translate, maps them where the
13//! kernel would and jumps to the interpreter, which then runs as if the
14//! kernel had loaded the program. Any other, a 32-bit one or one where
15//! the loader cannot run, is re-targeted at the host copy of its dynamic
16//! loader, with the program's guest path as the dynamic loader's argument;
17//! 3. the new argv is written on the tracee's stack and the registers point
18//! at it. `envp` is untouched.
19//!
20//! Set `PROOT_EXEC_DEBUG=1` to print every rewritten exec on stderr.
21//!
22//! Without ptrace the same plan is carried out by a trampoline inside the
23//! tracee instead of a register rewrite, see [`shim`]. An exec of proot or
24//! bwrap themselves is a nested launch, planned by [`nested`] and carried out
25//! by the trampoline in both modes.
26//!
27//! Known gaps: at the kernel level `/proc/<pid>/exe` names the loader, or
28//! the dynamic loader; the supervisor answers `readlink` on `/proc/self/exe`
29//! from the record kept here. A program the loader started reads the path
30//! its caller named as `AT_EXECFN`; one started by its dynamic loader reads
31//! the alias it was exec'd through.
32//!
33//! Both ABIs are handled: argv is read and written with the *caller's* pointer
34//! width (the kernel interprets `execve` arguments in the caller's ABI), and a
35//! 32-bit ELF is re-targeted at its own 32-bit loader.
36
37pub mod elf;
38pub mod nested;
39pub mod shebang;
40pub mod shim;
41
42use crate::fs::translate::{detranslate_path, guest_dirfd_path, translate_path};
43use crate::procs::PendingExec;
44use crate::state::{Ctx, View};
45use crate::sys::Result;
46use crate::syscall::nr::EXECVEAT;
47use crate::tracee::mem::MAX_ARG_STRLEN;
48use crate::tracee::regs::Regs;
49use elf::FileKind;
50use nested::EnvSource;
51use std::collections::HashMap;
52use std::os::unix::ffi::OsStrExt;
53use std::sync::{Arc, Mutex};
54
55/// Upper bound on argv entries read from the tracee.
56const MAX_ARGV: usize = 1 << 18;
57/// `e_machine` of the host's programs, the ones the loader maps.
58#[cfg(target_arch = "x86_64")]
59const HOST_MACHINE: u16 = 62;
60/// `e_machine` of the host's programs, the ones the loader maps.
61#[cfg(target_arch = "aarch64")]
62const HOST_MACHINE: u16 = 183;
63/// Number of entries the loader takes before the program's own argument vector: its alias, the
64/// program and the path to report as `AT_EXECFN`.
65pub const LOADER_ARGS: usize = 3;
66/// Maximum nesting of `#!` interpreters (the kernel allows 4).
67const MAX_SHEBANG_DEPTH: usize = 4;
68
69/// What the kernel should load in place of what the tracee asked for.
70#[derive(Clone, Debug)]
71pub struct ExecPlan {
72 /// Host path to exec.
73 pub exec_path: Vec<u8>,
74 /// The complete argument vector to exec it with.
75 pub argv: Vec<Vec<u8>>,
76 /// Entries at the front of `argv` that did not come from the caller's
77 /// `argv[1..]`; the rest is the caller's tail, unchanged.
78 pub prefix: usize,
79 /// Whether `exec_path` or `argv` differ from what the tracee asked.
80 pub changed: bool,
81 /// Canonical guest path of the program (after `#!` expansion).
82 pub guest_prog: Vec<u8>,
83 /// Host path of the program (after `#!` expansion).
84 pub host_prog: Vec<u8>,
85 /// Whether the program's format was recognized (ELF or `#!`).
86 pub understood: bool,
87 /// Process setup the trampoline carries out before the exec (a nested
88 /// launch); `None` for an ordinary exec, which only takes the trampoline
89 /// without ptrace.
90 pub trampoline: Option<Trampoline>,
91 /// The program path as its exec named it: the caller's for an ordinary
92 /// exec, the inner command's for a nested launch, empty for a plan that
93 /// only prints a message. What the kernel reports as `AT_EXECFN`.
94 pub user_path: Vec<u8>,
95}
96
97/// What the trampoline does inside the tracee before exec'ing the plan.
98#[derive(Clone, Debug, Default)]
99pub struct Trampoline {
100 /// The view the process enters at the real exec, with its working
101 /// directory there as a guest path.
102 pub enter: Option<(Arc<View>, Vec<u8>)>,
103 /// Host directory to `chdir` to first (with ptrace, where the kernel's
104 /// cwd is the real one).
105 pub chdir: Option<Vec<u8>>,
106 /// Environment to exec with, replacing the inherited one.
107 pub env: Option<Vec<Vec<u8>>>,
108 /// `setsid` before the exec.
109 pub new_session: bool,
110 /// Die with `SIGKILL` when the parent dies.
111 pub die_with_parent: bool,
112 /// Classic BPF programs (raw `sock_filter` bytes) to install before the exec.
113 pub seccomp_programs: Vec<Vec<u8>>,
114 /// Descriptors that receive `{"child-pid": N}` and are closed.
115 pub info_fds: Vec<i32>,
116 /// Descriptors that receive `{"child-pid": N}` then `{"exit-code": N}`:
117 /// the trampoline forks, and its parent stays to report.
118 pub json_status_fds: Vec<i32>,
119 /// Lines printed on stderr before anything else.
120 pub warnings: Vec<Vec<u8>>,
121 /// Print this and exit with the code instead of exec'ing: on stdout when
122 /// the flag is set, else stderr.
123 pub exit: Option<(i32, Vec<u8>, bool)>,
124}
125
126impl ExecPlan {
127 /// A plan that only runs the trampoline for its message and exit code.
128 ///
129 /// # Arguments
130 /// * `trampoline` - The setup, with `exit` set.
131 pub fn message(trampoline: Trampoline) -> ExecPlan {
132 ExecPlan {
133 exec_path: Vec::new(),
134 argv: Vec::new(),
135 prefix: 0,
136 changed: true,
137 guest_prog: Vec::new(),
138 host_prog: Vec::new(),
139 understood: true,
140 trampoline: Some(trampoline),
141 user_path: Vec::new(),
142 }
143 }
144}
145
146/// Works out what to exec for a program the tracee named.
147///
148/// `#!` scripts are expanded into interpreter plus argument, repeatedly,
149/// until an ELF is reached. A dynamically linked ELF of the host's machine
150/// is re-targeted at the shim's loader, given its alias, the program's
151/// canonical guest path, the path to report as `AT_EXECFN` (the one the
152/// caller named) and then the caller's argument vector whole; the loader
153/// maps the program where the kernel would, below the break, far from the
154/// libraries and from what the program maps later. The dynamic loader then
155/// takes `$ORIGIN` from `/proc/self/exe`, answered with the canonical path.
156///
157/// Any other dynamically linked ELF is re-targeted at the host copy of its
158/// dynamic loader with the program's guest path as the dynamic loader's
159/// argument. The dynamic loader takes the program's `$ORIGIN` from the
160/// path it is given, where the kernel would take it from the resolved
161/// executable, so one that accepts `--argv0` gets the canonical path and the
162/// name as typed separately; one without it uses the path as `argv[0]`, on
163/// which multi-call binaries (busybox) dispatch, so it gets the name as
164/// typed unless that name is dirfd-relative and meaningless after the
165/// rewrite.
166///
167/// Either loader is reached through an alias named as the tracee named the
168/// program, which is where the kernel takes `comm` from, so a script keeps
169/// its own name rather than its interpreter's. In the guest-stub restricted
170/// mode the program itself is the plan: the trampoline loads it and its
171/// interpreter the way the kernel would.
172///
173/// An exec of proot or bwrap is a nested launch and is planned by
174/// [`nested::plan`] in the view it describes.
175///
176/// # Arguments
177/// * `ctx` - Shared supervisor state.
178/// * `pid` - Tracee thread id.
179/// * `dirfd` - Base of a relative `user_path`.
180/// * `user_path` - Program path as the tracee passed it.
181/// * `argv` - The tracee's argument vector (empty when it passed NULL).
182/// * `deref_final` - Follow a symlink in the final component of the program path.
183/// * `env` - The tracee's environment, read only for a nested launch.
184///
185/// # Errors
186/// `ELOOP` for too many nested `#!`, otherwise the errno of translating a
187/// path. A missing or unreadable program is not an error here: the plan
188/// names its translated path and `understood` is false.
189pub fn plan(
190 ctx: &Ctx,
191 pid: u32,
192 dirfd: i32,
193 user_path: &[u8],
194 argv: Vec<Vec<u8>>,
195 deref_final: bool,
196 env: EnvSource,
197) -> Result<ExecPlan> {
198 let view = ctx.view(pid as i32);
199 plan_in(ctx, &view, pid, dirfd, user_path, argv, deref_final, env)
200}
201
202/// [`plan`] in a given view.
203///
204/// # Arguments
205/// * `ctx` - Shared supervisor state.
206/// * `view` - The view to translate in.
207/// * `pid` - Tracee thread id.
208/// * `dirfd` - Base of a relative `user_path`.
209/// * `user_path` - Program path as the tracee passed it.
210/// * `argv` - The tracee's argument vector (empty when it passed NULL).
211/// * `deref_final` - Follow a symlink in the final component of the program path.
212/// * `env` - The tracee's environment, read only for a nested launch.
213///
214/// # Errors
215/// As [`plan`].
216#[allow(clippy::too_many_arguments)]
217pub fn plan_in(
218 ctx: &Ctx,
219 view: &View,
220 pid: u32,
221 dirfd: i32,
222 user_path: &[u8],
223 mut argv: Vec<Vec<u8>>,
224 deref_final: bool,
225 env: EnvSource,
226) -> Result<ExecPlan> {
227 let fs = &view.fs;
228 let argv_was_null = argv.is_empty();
229 if argv.is_empty() {
230 // The kernel runs a program with an empty argv as if argv[0] were "".
231 argv.push(Vec::new());
232 }
233 let tail = argv.len() - 1;
234
235 // 1. Program path, then #! expansion.
236 let mut host_prog = translate_path(fs, pid, dirfd, user_path, deref_final)?;
237 let mut argv0_user = user_path.to_vec();
238 let mut kind = elf::classify_file(&host_prog);
239 let mut depth = 0;
240 while let Ok(FileKind::Shebang(line)) = &kind {
241 if depth == MAX_SHEBANG_DEPTH {
242 return Err(libc::ELOOP);
243 }
244 depth += 1;
245 let sb = shebang::parse(line).ok_or(libc::ENOEXEC)?;
246 // argv becomes: interp [arg] script argv[1..]
247 let mut new_argv = vec![sb.interp.clone()];
248 if let Some(a) = &sb.arg {
249 new_argv.push(a.clone());
250 }
251 new_argv.push(argv0_user.clone());
252 new_argv.extend(argv.drain(1..));
253 argv = new_argv;
254 argv0_user = sb.interp.clone();
255 host_prog = translate_path(fs, pid, libc::AT_FDCWD, &sb.interp, true)?;
256 kind = elf::classify_file(&host_prog);
257 }
258
259 // 2. Decide what the kernel should load.
260 let guest_prog = detranslate_path(fs, &host_prog);
261 let understood = matches!(kind, Ok(FileKind::Elf64(_)) | Ok(FileKind::Elf32(_)));
262 if let Some(front) = nested::detect(
263 &host_prog,
264 user_path,
265 &guest_prog,
266 &argv[0],
267 understood && depth == 0,
268 ) {
269 return nested::plan(ctx, view, pid, front, argv, env);
270 }
271 let mut changed = depth > 0 || argv_was_null;
272 let native = matches!(&kind, Ok(FileKind::Elf64(info)) if info.machine == HOST_MACHINE);
273 let loader = shim::loader_path().filter(|_| native && !fs.identity && !ctx.trap_mode());
274 let (exec_path, new_argv): (Vec<u8>, Vec<Vec<u8>>) = match kind {
275 Ok(FileKind::Elf64(info)) | Ok(FileKind::Elf32(info)) => match (info.interp, loader) {
276 (Some(_), Some(loader)) => {
277 let alias = loader_alias(loader.as_os_str().as_bytes(), exec_name(user_path, &guest_prog))
278 .unwrap_or_else(|| loader.as_os_str().as_bytes().to_vec());
279 let execfn = if user_path.is_empty() {
280 guest_prog.clone()
281 } else {
282 user_path.to_vec()
283 };
284 let mut v = vec![alias.clone(), guest_prog.clone(), execfn, argv[0].clone()];
285 v.extend(argv[1..].iter().cloned());
286 changed = true;
287 (alias, v)
288 }
289 (Some(interp), None) if !fs.identity && !ctx.trap_mode() => {
290 let host_interp = translate_path(fs, pid, libc::AT_FDCWD, &interp, true)?;
291 let mut v = vec![host_interp.clone()];
292 let argv0_supported = loader_supports_argv0(&host_interp);
293 if argv0_supported {
294 v.push(b"--argv0".to_vec());
295 v.push(argv[0].clone());
296 }
297 v.push(
298 if !argv0_supported && (dirfd == libc::AT_FDCWD || argv0_user.first() == Some(&b'/')) {
299 argv0_user.clone()
300 } else {
301 guest_prog.clone()
302 },
303 );
304 v.extend(argv[1..].iter().cloned());
305 changed = true;
306 let exec_as = loader_alias(&host_interp, exec_name(user_path, &guest_prog))
307 .unwrap_or_else(|| host_interp.clone());
308 v[0] = exec_as.clone();
309 (exec_as, v)
310 }
311 // Static, or identity translation where the interpreter resolves as is.
312 _ => (host_prog.clone(), argv),
313 },
314 // Not something we understand (or unreadable): let the kernel decide.
315 _ => (host_prog.clone(), argv),
316 };
317 if exec_path != user_path {
318 changed = true;
319 }
320 Ok(ExecPlan {
321 prefix: new_argv.len() - tail,
322 exec_path,
323 argv: new_argv,
324 changed,
325 guest_prog,
326 host_prog,
327 understood,
328 trampoline: None,
329 user_path: user_path.to_vec(),
330 })
331}
332
333/// Rewrites an `execve`/`execveat` so the kernel loads host-side files only.
334///
335/// # Arguments
336/// * `ctx` - Shared supervisor state.
337/// * `regs` - Registers at the stop; edits are committed by the caller.
338///
339/// An empty path is left for the kernel to refuse, unless `AT_EMPTY_PATH`
340/// makes it name `dirfd`: then the plan is made from the descriptor's own
341/// path, or the working directory's for `AT_FDCWD`.
342///
343/// # Errors
344/// `ELOOP` for too many nested `#!`, otherwise the errno of reading the tracee
345/// or translating a path. A missing or unreadable program is not an error:
346/// the translated path is left for the kernel to judge.
347pub fn handle_execve(ctx: &Ctx, regs: &mut Regs) -> Result<()> {
348 let pid = regs.pid() as u32;
349 let abi = regs.abi();
350 let is_at = matches!(abi.to_canonical(regs.sysnum()), Some((EXECVEAT, _)));
351 let ptr_size = abi.ptr_size();
352 let (dirfd, path_idx, argv_idx) = if is_at {
353 (regs.arg(0) as i32, 1, 2)
354 } else {
355 (libc::AT_FDCWD, 0, 1)
356 };
357 let at_flags = if is_at { regs.arg(4) as i32 } else { 0 };
358 let deref_final = at_flags & libc::AT_SYMLINK_NOFOLLOW == 0;
359
360 let user_path = ctx.mem.read_cstr(pid, regs.arg(path_idx))?;
361 let (dirfd, user_path) = if user_path.is_empty() {
362 if !is_at || at_flags & libc::AT_EMPTY_PATH == 0 {
363 return Ok(());
364 }
365 (
366 libc::AT_FDCWD,
367 guest_dirfd_path(&ctx.view(pid as i32).fs, pid, dirfd)?,
368 )
369 } else {
370 (dirfd, user_path)
371 };
372 let argv = read_argv(ctx, pid, regs.arg(argv_idx), ptr_size)?;
373 let env = EnvSource::Address {
374 envp: regs.arg(argv_idx + 1),
375 ptr_size,
376 };
377 let plan = plan(ctx, pid, dirfd, &user_path, argv, deref_final, env)?;
378 ctx.set_exe(pid as i32, plan.guest_prog.clone(), plan.host_prog.clone());
379
380 // 3. Materialize path and argv in the tracee, unless nothing changed.
381 if !is_at && !plan.changed {
382 return Ok(());
383 }
384 debug_exec(pid, &plan);
385 let (exec_path, argv): (Vec<u8>, Vec<Vec<u8>>) = if plan.trampoline.is_some() {
386 let shim_path = shim::install().map_err(|_| libc::ENOENT)?.join(shim::SHIM_NAME);
387 let shim_path = shim_path.as_os_str().as_bytes().to_vec();
388 shim::write_record(ctx.tgid(pid as i32), &plan, None)?;
389 ctx.procs.shim_enter(pid as i32, pending_of(&plan));
390 (shim_path.clone(), vec![shim_path])
391 } else {
392 (plan.exec_path.clone(), plan.argv.clone())
393 };
394 let (path_addr, argv_addr) = push_argv(ctx, regs, &exec_path, &argv, ptr_size)?;
395 if is_at {
396 regs.set_arg(0, libc::AT_FDCWD as i64 as u64);
397 regs.set_arg(1, path_addr);
398 regs.set_arg(2, argv_addr);
399 regs.set_arg(
400 4,
401 (at_flags & !(libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW)) as u64,
402 );
403 } else {
404 regs.set_arg(0, path_addr);
405 regs.set_arg(1, argv_addr);
406 }
407 Ok(())
408}
409
410/// What to remember about a process between its exec of the trampoline and
411/// the trampoline's exec of the real target.
412///
413/// # Arguments
414/// * `plan` - The exec being carried out.
415pub fn pending_of(plan: &ExecPlan) -> PendingExec {
416 PendingExec {
417 guest: plan.guest_prog.clone(),
418 host: plan.host_prog.clone(),
419 restore: None,
420 enter: plan
421 .trampoline
422 .as_ref()
423 .filter(|t| t.exit.is_none())
424 .and_then(|t| t.enter.clone()),
425 cmdline_prefix: None,
426 }
427}
428
429/// Completes a pending exec at the trampoline's exec of the real target:
430/// records the program and moves the process into its new view, if any.
431///
432/// # Arguments
433/// * `ctx` - Shared supervisor state.
434/// * `tgid` - The process.
435/// * `pending` - What was recorded at the rewritten exec.
436pub fn finish_pending(ctx: &Ctx, tgid: i32, pending: PendingExec) {
437 ctx.set_exe(tgid, pending.guest, pending.host);
438 ctx.procs
439 .update(tgid, |t| t.cmdline_prefix = pending.cmdline_prefix);
440 if let Some((view, cwd)) = pending.enter {
441 ctx.enter_view(tgid, view, cwd);
442 }
443}
444
445/// Reads the tracee's argument vector.
446///
447/// # Arguments
448/// * `ctx` - Shared supervisor state.
449/// * `pid` - Tracee thread id.
450/// * `argv_ptr` - Address of the pointer array, or 0.
451/// * `ptr_size` - Pointer width of the tracee's ABI.
452///
453/// # Returns
454/// The strings; empty for a NULL `argv`.
455pub fn read_argv(ctx: &Ctx, pid: u32, argv_ptr: u64, ptr_size: usize) -> Result<Vec<Vec<u8>>> {
456 if argv_ptr == 0 {
457 return Ok(Vec::new());
458 }
459 let ptrs = ctx.mem.read_ptr_array_sized(pid, argv_ptr, MAX_ARGV, ptr_size)?;
460 ptrs.iter()
461 .map(|&p| ctx.mem.read_string(pid, p, MAX_ARG_STRLEN))
462 .collect()
463}
464
465/// Prints the rewritten exec when `PROOT_EXEC_DEBUG` is set.
466///
467/// # Arguments
468/// * `pid` - Tracee thread id.
469/// * `plan` - The exec about to happen.
470pub fn debug_exec(pid: u32, plan: &ExecPlan) {
471 if std::env::var_os("PROOT_EXEC_DEBUG").is_some() {
472 eprintln!(
473 "proot: exec pid {pid}: {} argv={:?}",
474 String::from_utf8_lossy(&plan.exec_path),
475 plan.argv
476 .iter()
477 .map(|a| String::from_utf8_lossy(a).into_owned())
478 .collect::<Vec<_>>()
479 );
480 }
481}
482
483/// The name the kernel would give a process exec'd as `user_path`: the last
484/// component of that path, or of `guest_prog` when the path is empty
485/// (`execveat` of a descriptor) or ends in a slash.
486///
487/// # Arguments
488/// * `user_path` - Program path as the tracee passed it.
489/// * `guest_prog` - Canonical guest path of what will run.
490pub(super) fn exec_name<'a>(user_path: &'a [u8], guest_prog: &'a [u8]) -> &'a [u8] {
491 let base = |p: &'a [u8]| p.rsplit(|&b| b == b'/').next().unwrap_or(p);
492 match base(user_path) {
493 b"" => guest_prog,
494 name => name,
495 }
496}
497
498/// Writes a path and an argv array onto the tracee's stack.
499///
500/// Layout, from the returned base upwards: the NUL-terminated pointer array
501/// (entries `ptr_size` bytes wide, base 16-byte aligned), then every string,
502/// then the path.
503///
504/// # Arguments
505/// * `ctx` - Shared supervisor state.
506/// * `regs` - Registers whose stack pointer locates the scratch area.
507/// * `path` - Program path for the kernel.
508/// * `argv` - Argument strings, without NULs.
509/// * `ptr_size` - Pointer width of the calling process's ABI (4 or 8).
510///
511/// # Returns
512/// `(path address, argv address)` in the tracee.
513fn push_argv(
514 ctx: &Ctx,
515 regs: &mut Regs,
516 path: &[u8],
517 argv: &[Vec<u8>],
518 ptr_size: usize,
519) -> Result<(u64, u64)> {
520 let table = (argv.len() + 1) * ptr_size;
521 let strings: usize = argv.iter().map(|a| a.len() + 1).sum::<usize>() + path.len() + 1;
522 let total = table + strings;
523 // Below the red zone, without moving the stack pointer: no exit stop is
524 // taken, so a lowered rsp would survive a failed execve and crash the tracee.
525 let base = regs.sp().saturating_sub(crate::sys::RED_ZONE_SIZE + total as u64) & !0xf;
526 let mut buf = vec![0u8; total];
527 let mut off = table;
528 for (i, a) in argv.iter().enumerate() {
529 let addr = base + off as u64;
530 if ptr_size == 8 {
531 buf[i * 8..i * 8 + 8].copy_from_slice(&addr.to_le_bytes());
532 } else {
533 buf[i * 4..i * 4 + 4].copy_from_slice(&(addr as u32).to_le_bytes());
534 }
535 buf[off..off + a.len()].copy_from_slice(a);
536 off += a.len() + 1;
537 }
538 let path_addr = base + off as u64;
539 buf[off..off + path.len()].copy_from_slice(path);
540 ctx.mem.write(regs.pid() as u32, base, &buf)?;
541 Ok((path_addr, base))
542}
543
544/// A symlink to `host_interp` whose basename is the program's, for `execve`.
545///
546/// The kernel names a process after the basename of the path it exec'd and
547/// exposes that path as `AT_EXECFN`. Exec'ing a loader directly would make
548/// every dynamic program show up as `ld-linux-x86-64.so.2` or `l`, so the
549/// loader is reached through `<alias dir>/<hash of loader>/<program name>`
550/// instead. One
551/// alias per (loader, name), created on first use; the directory lives for
552/// the supervisor's life (see [`alias_dir`]).
553///
554/// # Arguments
555/// * `host_interp` - Host path of the loader or the dynamic loader.
556/// * `name` - Name to give the alias, or a path whose last component is taken.
557///
558/// # Returns
559/// The alias path, or `None` when the directory or link cannot be created.
560fn loader_alias(host_interp: &[u8], name: &[u8]) -> Option<Vec<u8>> {
561 let name = name.rsplit(|&b| b == b'/').next().filter(|n| !n.is_empty())?;
562 let dir = alias_subdir(host_interp);
563 let link = dir.join(std::ffi::OsStr::from_bytes(name));
564 match std::fs::read_link(&link) {
565 Ok(target) if target.as_os_str().as_bytes() == host_interp => {}
566 Ok(_) => {
567 let _ = std::fs::remove_file(&link);
568 std::fs::create_dir_all(&dir).ok()?;
569 std::os::unix::fs::symlink(std::ffi::OsStr::from_bytes(host_interp), &link).ok()?;
570 }
571 Err(_) => {
572 std::fs::create_dir_all(&dir).ok()?;
573 if let Err(e) = std::os::unix::fs::symlink(std::ffi::OsStr::from_bytes(host_interp), &link)
574 && e.kind() != std::io::ErrorKind::AlreadyExists
575 {
576 return None;
577 }
578 }
579 }
580 Some(link.as_os_str().as_bytes().to_vec())
581}
582
583/// The directory of the aliases of one target, named by a hash of its path.
584///
585/// # Arguments
586/// * `target` - Host path the aliases point to.
587///
588/// # Returns
589/// `<alias dir>/<hash of target>`.
590fn alias_subdir(target: &[u8]) -> std::path::PathBuf {
591 use std::collections::hash_map::DefaultHasher;
592 use std::hash::{Hash, Hasher};
593 let mut h = DefaultHasher::new();
594 target.hash(&mut h);
595 alias_dir().join(format!("{:016x}", h.finish()))
596}
597
598/// Whether `path` is an alias of the loader, the `argv[0]` of a program it loaded.
599///
600/// # Arguments
601/// * `path` - A path, typically the first entry of a process's command line.
602///
603/// # Returns
604/// `true` when `path` lies in the loader's alias directory.
605pub fn is_loader_alias(path: &[u8]) -> bool {
606 let Some(loader) = shim::loader_path() else {
607 return false;
608 };
609 let dir = alias_subdir(loader.as_os_str().as_bytes());
610 std::path::Path::new(std::ffi::OsStr::from_bytes(path)).parent() == Some(dir.as_path())
611}
612
613static ALIAS_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
614
615/// Directory holding the loader aliases, private to this supervisor.
616///
617/// Created under `$XDG_RUNTIME_DIR` or the temp dir, named by our pid, and
618/// removed by the launcher when the supervisor exits. Without ptrace the
619/// same directory holds the exec shim and is every tracee's kernel cwd, see
620/// [`shim`].
621pub fn alias_dir() -> std::path::PathBuf {
622 ALIAS_DIR
623 .get_or_init(|| {
624 let base = std::env::var_os("XDG_RUNTIME_DIR")
625 .map(std::path::PathBuf::from)
626 .filter(|p| p.is_dir())
627 .unwrap_or_else(std::env::temp_dir);
628 base.join(format!("proot-next-{}", std::process::id()))
629 })
630 .clone()
631}
632
633/// Fixes the alias directory before anything used it (the shim installer).
634///
635/// # Arguments
636/// * `dir` - The directory to use from now on.
637pub fn set_alias_dir(dir: std::path::PathBuf) {
638 let _ = ALIAS_DIR.set(dir);
639}
640
641/// Whether the loader at `host_interp` understands `--argv0`.
642///
643/// glibc (>= 2.33) stores the option as `--argv0`; musl parses the leading
644/// dashes itself and stores `argv0`. Probed once per loader by scanning the
645/// file; the result is cached for the life of the supervisor.
646///
647/// # Arguments
648/// * `host_interp` - Host path of the dynamic loader.
649fn loader_supports_argv0(host_interp: &[u8]) -> bool {
650 static CACHE: Mutex<Option<HashMap<Vec<u8>, bool>>> = Mutex::new(None);
651 let mut guard = CACHE.lock().unwrap();
652 let cache = guard.get_or_insert_with(HashMap::new);
653 if let Some(&v) = cache.get(host_interp) {
654 return v;
655 }
656 let data = std::fs::read(std::ffi::OsStr::from_bytes(host_interp)).unwrap_or_default();
657 let supported = data.windows(8).any(|w| w == b"--argv0\0") || data.windows(7).any(|w| w == b"\0argv0\0");
658 cache.insert(host_interp.to_vec(), supported);
659 supported
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn host_loader_argv0_probe_is_cached() {
668 let ld = b"/lib64/ld-linux-x86-64.so.2";
669 if std::path::Path::new("/lib64/ld-linux-x86-64.so.2").exists() {
670 let a = loader_supports_argv0(ld);
671 let b = loader_supports_argv0(ld);
672 assert_eq!(a, b);
673 }
674 }
675}