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}