Skip to main content

proot/exec/
shim.rs

1//! The exec trampoline: a static binary installed in the alias directory as
2//! `e`, and the record files that tell it what to run.
3//!
4//! Without ptrace every tracee has the alias directory as its kernel working
5//! directory, so the two-byte path `e` names the trampoline from any
6//! process, whatever length the original path string had.  The supervisor
7//! writes `r<pid>` next to it with the plan (see [`write_record`]), rewrites
8//! the path string to `e`, lets the exec proceed, and the trampoline execs
9//! the real target with the arguments rebuilt from the record and its own,
10//! untouched, `argv[1..]`.  A nested launch (see [`super::nested`]) takes
11//! the trampoline in both modes, exec'd by its full path with ptrace, and
12//! its record carries the process setup as well: a complete argument vector,
13//! an environment, a directory to enter, session and death-signal flags,
14//! seccomp programs, status descriptors, or a message to print and exit with.
15//!
16//! In the guest-stub restricted mode every exec takes the trampoline, which
17//! arms the guest stub and loads the program in place rather than exec'ing
18//! it (see `guest_stub`); its record then also names the mailbox descriptor,
19//! the `AT_EXECFN` path and the process name, see [`Trapped`].
20//!
21//! Outside the guest-stub mode a dynamically linked program of the host's
22//! machine is exec'd through a copy of the trampoline installed as `l`, the
23//! loader, which maps the program where the kernel would and its interpreter
24//! after it (see [`loader_path`] and `ExecPlan`).
25//!
26//! The binaries under `shim/bin/` are built by `shim/build.sh` from the
27//! `no_std` crate in `shim/` and embedded here; one per host architecture.
28
29use crate::exec::ExecPlan;
30use crate::sys::Result;
31use std::os::unix::ffi::OsStrExt;
32use std::path::{Path, PathBuf};
33use std::sync::OnceLock;
34
35#[cfg(target_arch = "x86_64")]
36const SHIM: &[u8] = include_bytes!("shim/bin/shim-x86_64");
37#[cfg(target_arch = "aarch64")]
38const SHIM: &[u8] = include_bytes!("shim/bin/shim-aarch64");
39/// The 32-bit trampoline, which loads the compat ABI's programs in the
40/// guest-stub mode: i386 on x86_64, 32-bit ARM on aarch64.
41#[cfg(target_arch = "x86_64")]
42const SHIM32: Option<&[u8]> = Some(include_bytes!("shim/bin/shim-i686"));
43/// The 32-bit trampoline, which loads the compat ABI's programs in the
44/// guest-stub mode: i386 on x86_64, 32-bit ARM on aarch64.
45#[cfg(target_arch = "aarch64")]
46const SHIM32: Option<&[u8]> = Some(include_bytes!("shim/bin/shim-armv7"));
47
48/// Name of the trampoline inside the alias directory.
49pub const SHIM_NAME: &str = "e";
50
51static INSTALLED: OnceLock<PathBuf> = OnceLock::new();
52
53/// Whether the i386 trampoline was installed and runs here.
54static HAS_32BIT: OnceLock<bool> = OnceLock::new();
55
56/// Whether the loader was installed and runs here.
57static HAS_LOADER: OnceLock<bool> = OnceLock::new();
58
59/// Installs the trampoline and returns the directory holding it.
60///
61/// The directory becomes every tracee's kernel working directory.  A base
62/// where the trampoline cannot be executed (a `noexec` mount) is skipped for
63/// the next candidate.
64///
65/// # Errors
66/// A message when no candidate directory can hold an executable file.
67pub fn install() -> std::result::Result<PathBuf, String> {
68    if let Some(dir) = INSTALLED.get() {
69        return Ok(dir.clone());
70    }
71    let mut candidates: Vec<PathBuf> = Vec::new();
72    if let Some(p) = std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from) {
73        candidates.push(p);
74    }
75    candidates.push(std::env::temp_dir());
76    if let Some(home) = std::env::var_os("HOME") {
77        candidates.push(PathBuf::from(home).join(".cache"));
78    }
79    let mut last = String::from("no candidate directory");
80    for base in candidates.iter().filter(|b| b.is_dir()) {
81        let dir = base.join(format!("proot-next-{}", std::process::id()));
82        match install_in(&dir) {
83            Ok(()) => {
84                let _ = INSTALLED.set(dir.clone());
85                crate::exec::set_alias_dir(dir.clone());
86                return Ok(dir);
87            }
88            Err(e) => {
89                last = format!("{}: {e}", dir.display());
90                let _ = std::fs::remove_dir_all(&dir);
91            }
92        }
93    }
94    Err(format!("cannot install the exec shim ({last})"))
95}
96
97/// Writes the trampoline into `dir` and checks it runs there.
98///
99/// Run without a record the trampoline exits 126 after one line on stderr;
100/// any other outcome means it did not run at all (a `noexec` mount, the
101/// wrong architecture).
102///
103/// # Arguments
104/// * `dir` - Directory to install into, created if needed.
105fn install_in(dir: &Path) -> std::io::Result<()> {
106    use std::os::unix::fs::PermissionsExt;
107    std::fs::create_dir_all(dir)?;
108    let runs = |name: &str, bytes: &[u8]| -> std::io::Result<std::process::Output> {
109        let path = dir.join(name);
110        std::fs::write(&path, bytes)?;
111        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))?;
112        std::process::Command::new(&path)
113            .current_dir(dir)
114            .stdin(std::process::Stdio::null())
115            .output()
116    };
117    let out = runs(SHIM_NAME, SHIM)?;
118    if out.status.code() != Some(126) {
119        return Err(std::io::Error::other(format!("trampoline did not run: {out:?}")));
120    }
121    if let Some(bytes) = SHIM32 {
122        let ok =
123            runs(crate::guest_stub::layout::SHIM32_NAME, bytes).is_ok_and(|o| o.status.code() == Some(126));
124        let _ = HAS_32BIT.set(ok);
125    }
126    let ok = loader_bytes(SHIM).is_some_and(|bytes| {
127        runs(crate::guest_stub::layout::LOADER_NAME, &bytes).is_ok_and(|o| o.status.code() == Some(126))
128    });
129    let _ = HAS_LOADER.set(ok);
130    Ok(())
131}
132
133/// The loader: a copy of the trampoline `shim` with the last byte of its role marker rewritten.
134///
135/// # Arguments
136/// * `shim` - The trampoline's file.
137///
138/// # Returns
139/// The copy, or `None` when the marker is not in the file exactly once.
140fn loader_bytes(shim: &[u8]) -> Option<Vec<u8>> {
141    use crate::guest_stub::layout::{ROLE_LOADER, ROLE_MARK};
142    let mut at = shim
143        .windows(ROLE_MARK.len())
144        .enumerate()
145        .filter(|(_, w)| *w == ROLE_MARK);
146    let (first, _) = at.next()?;
147    if at.next().is_some() {
148        return None;
149    }
150    let mut bytes = shim.to_vec();
151    bytes[first + ROLE_MARK.len() - 1] = ROLE_LOADER;
152    Some(bytes)
153}
154
155/// Path of the installed loader, when it runs here.
156///
157/// # Returns
158/// The loader's path, or `None` before [`install`] or where it did not run.
159pub fn loader_path() -> Option<PathBuf> {
160    if !HAS_LOADER.get().copied().unwrap_or(false) {
161        return None;
162    }
163    INSTALLED
164        .get()
165        .map(|d| d.join(crate::guest_stub::layout::LOADER_NAME))
166}
167
168/// Whether the 32-bit trampoline is installed and runs here, which takes a
169/// kernel with ia32 emulation, or a CPU and kernel with AArch32.
170///
171/// # Returns
172/// `true` when 32-bit programs can run in the guest-stub mode.
173pub fn has_32bit() -> bool {
174    HAS_32BIT.get().copied().unwrap_or(false)
175}
176
177/// Path of the installed trampoline.
178pub fn path() -> Option<PathBuf> {
179    INSTALLED.get().map(|d| d.join(SHIM_NAME))
180}
181
182/// Whether thread `tid` is currently executing the trampoline.
183///
184/// # Arguments
185/// * `tid` - Thread id.
186pub fn running(tid: u32) -> bool {
187    let Some(me) = path() else { return false };
188    std::fs::read_link(format!("/proc/{tid}/exe"))
189        .map(|p| p == me)
190        .unwrap_or(false)
191}
192
193/// Writes the record the trampoline will read for process `tgid`.
194///
195/// The record is a sequence of netstrings (`<length>:<bytes>,`), each
196/// holding a one-byte tag and its value; text values carry their
197/// terminating NUL so the trampoline can hand them to `execve` in place.
198/// The first field is `v2`.  Then either `A<n>` (leading arguments, the
199/// trampoline's own `argv[1..]` follow) or `a<n>` (the complete vector),
200/// each followed by `n` fields tagged `-`; `p<path>` to exec; optionally
201/// `e<n>` and `n` environment entries tagged `-`; `c<dir>` to enter; `s`
202/// for a new session; `d` to die with the parent; `S<program>` (binary,
203/// repeatable); `I<fd>` and `J<fd>` status descriptors; `W<text>` lines to
204/// print; or, instead of all of the above, `E<code>:<text>` or
205/// `O<code>:<text>` to print on stderr or stdout and exit.  In the
206/// guest-stub mode `m<fd>`, `x<path>`, `n<name>` and, for a 32-bit program,
207/// `w` follow `v2`, and an
208/// ordinary exec that keeps the caller's `argv[0]` is `A0` with `k`, so the
209/// trampoline passes on its own argument vector whole.
210///
211/// # Arguments
212/// * `tgid` - Process about to exec the trampoline.
213/// * `plan` - What it should exec.
214/// * `trapped` - The guest-stub mode's additions, if in that mode.
215///
216/// # Errors
217/// Errno from writing the file.
218pub fn write_record(tgid: i32, plan: &ExecPlan, trapped: Option<&Trapped>) -> Result<()> {
219    let Some(dir) = INSTALLED.get() else {
220        return Err(libc::ENOENT);
221    };
222    let mut rec = Record::default();
223    rec.text(b'v', b"2");
224    if let Some(t) = trapped {
225        rec.text(b'm', t.mailbox.to_string().as_bytes());
226        rec.text(b'x', &t.execfn);
227        rec.text(b'n', &t.name);
228        if t.narrow {
229            rec.text(b'w', b"");
230        }
231    }
232    match &plan.trampoline {
233        Some(t) => {
234            for w in &t.warnings {
235                rec.text(b'W', w);
236            }
237            if let Some((code, text, to_stdout)) = &t.exit {
238                let mut v = format!("{code}:").into_bytes();
239                v.extend_from_slice(text);
240                rec.text(if *to_stdout { b'O' } else { b'E' }, &v);
241            } else {
242                rec.text(b'a', plan.argv.len().to_string().as_bytes());
243                for a in &plan.argv {
244                    rec.text(b'-', a);
245                }
246                rec.text(b'p', &plan.exec_path);
247                if let Some(env) = &t.env {
248                    rec.text(b'e', env.len().to_string().as_bytes());
249                    for e in env {
250                        rec.text(b'-', e);
251                    }
252                }
253                if let Some(dir) = &t.chdir {
254                    rec.text(b'c', dir);
255                }
256                if t.new_session {
257                    rec.text(b's', b"");
258                }
259                if t.die_with_parent {
260                    rec.text(b'd', b"");
261                }
262                for prog in &t.seccomp_programs {
263                    rec.binary(b'S', prog);
264                }
265                for fd in &t.info_fds {
266                    rec.text(b'I', fd.to_string().as_bytes());
267                }
268                for fd in &t.json_status_fds {
269                    rec.text(b'J', fd.to_string().as_bytes());
270                }
271            }
272        }
273        None if trapped.is_some() && plan.prefix == 1 => {
274            rec.text(b'A', b"0");
275            rec.text(b'k', b"");
276            rec.text(b'p', &plan.exec_path);
277        }
278        None => {
279            rec.text(b'A', plan.prefix.to_string().as_bytes());
280            for a in &plan.argv[..plan.prefix] {
281                rec.text(b'-', a);
282            }
283            rec.text(b'p', &plan.exec_path);
284        }
285    }
286    std::fs::write(dir.join(format!("r{tgid}")), rec.bytes).map_err(|e| e.raw_os_error().unwrap_or(libc::EIO))
287}
288
289/// What a record carries in the guest-stub restricted mode.
290#[derive(Clone, Debug)]
291pub struct Trapped {
292    /// The mailbox descriptor installed in the process.
293    pub mailbox: i32,
294    /// The path to report as `AT_EXECFN`: the one the caller named.
295    pub execfn: Vec<u8>,
296    /// The process name the kernel would give, at most 15 bytes.
297    pub name: Vec<u8>,
298    /// The program is 32-bit: `e` hands the record to `e32`.
299    pub narrow: bool,
300}
301
302/// A record under construction.
303#[derive(Default)]
304struct Record {
305    bytes: Vec<u8>,
306}
307
308impl Record {
309    /// Appends a text field: the tag, the text and a NUL.
310    fn text(&mut self, tag: u8, value: &[u8]) {
311        self.field(tag, value, true);
312    }
313
314    /// Appends a binary field: the tag and the bytes.
315    fn binary(&mut self, tag: u8, value: &[u8]) {
316        self.field(tag, value, false);
317    }
318
319    fn field(&mut self, tag: u8, value: &[u8], nul: bool) {
320        let len = 1 + value.len() + usize::from(nul);
321        self.bytes.extend_from_slice(len.to_string().as_bytes());
322        self.bytes.push(b':');
323        self.bytes.push(tag);
324        self.bytes.extend_from_slice(value);
325        if nul {
326            self.bytes.push(0);
327        }
328        self.bytes.push(b',');
329    }
330}
331
332/// Path of the record file of process `tgid`.
333pub fn record_path(tgid: i32) -> Option<PathBuf> {
334    INSTALLED.get().map(|d| d.join(format!("r{tgid}")))
335}
336
337/// The alias directory as bytes, for building rewritten paths.
338pub fn dir_bytes() -> Option<Vec<u8>> {
339    INSTALLED.get().map(|d| d.as_os_str().as_bytes().to_vec())
340}