Skip to main content

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}