1use super::{ExecPlan, Trampoline};
29use crate::cli::{self, CliError, host::Host};
30use crate::config::{Binding, Config};
31use crate::fs::Fs;
32use crate::fs::binding::{is_prefix, join_prefix, rest_after};
33use crate::fs::translate::{detranslate_path, guest_cwd, read_link, translate_path};
34use crate::note;
35use crate::state::{Ctx, View};
36use crate::sys::Result;
37use crate::tracee::mem::MAX_ARG_STRLEN;
38use std::ffi::OsString;
39use std::os::unix::ffi::{OsStrExt, OsStringExt};
40use std::sync::{Arc, OnceLock};
41
42const MAX_ENV: usize = 1 << 16;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum FrontEnd {
48 Proot,
49 Bwrap,
50}
51
52const NAMES: &[&[u8]] = &[b"proot", b"proot-next", b"bwrap"];
55
56pub fn detect(
68 host_prog: &[u8],
69 user_path: &[u8],
70 guest_prog: &[u8],
71 argv0: &[u8],
72 is_elf: bool,
73) -> Option<FrontEnd> {
74 let name = super::exec_name(user_path, guest_prog);
75 let ours = is_elf && same_file_as_us(host_prog);
76 let by_name = is_elf && by_name_enabled() && NAMES.contains(&name);
77 if !ours && !by_name {
78 return None;
79 }
80 let argv0_name = argv0.rsplit(|&b| b == b'/').next().unwrap_or(argv0);
81 Some(if argv0_name.ends_with(b"bwrap") || name.ends_with(b"bwrap") {
82 FrontEnd::Bwrap
83 } else {
84 FrontEnd::Proot
85 })
86}
87
88fn by_name_enabled() -> bool {
90 static ENABLED: OnceLock<bool> = OnceLock::new();
91 *ENABLED.get_or_init(|| std::env::var_os("PROOT_NEST").is_none_or(|v| v != "0"))
92}
93
94fn same_file_as_us(host: &[u8]) -> bool {
96 static US: OnceLock<Option<(u64, u64)>> = OnceLock::new();
97 let us = US.get_or_init(|| dev_ino(b"/proc/self/exe"));
98 us.is_some() && *us == dev_ino(host)
99}
100
101fn dev_ino(host: &[u8]) -> Option<(u64, u64)> {
103 use std::os::unix::fs::MetadataExt;
104 let m = std::fs::metadata(std::ffi::OsStr::from_bytes(host)).ok()?;
105 Some((m.dev(), m.ino()))
106}
107
108pub enum EnvSource {
110 Address { envp: u64, ptr_size: usize },
113 Given(Vec<(Vec<u8>, Vec<u8>)>),
115}
116
117struct Tracee<'a> {
119 view: &'a View,
120 pid: u32,
121 env: Vec<(Vec<u8>, Vec<u8>)>,
122}
123
124impl Host for Tracee<'_> {
125 fn exists(&self, path: &[u8]) -> bool {
126 translate_path(&self.view.fs, self.pid, libc::AT_FDCWD, path, true)
127 .map(|h| std::fs::metadata(std::ffi::OsStr::from_bytes(&h)).is_ok())
128 .unwrap_or(false)
129 }
130
131 fn is_dir(&self, path: &[u8]) -> bool {
132 translate_path(&self.view.fs, self.pid, libc::AT_FDCWD, path, true)
133 .map(|h| std::fs::metadata(std::ffi::OsStr::from_bytes(&h)).is_ok_and(|m| m.is_dir()))
134 .unwrap_or(false)
135 }
136
137 fn host_path(&self, path: &[u8]) -> Vec<u8> {
138 if let Ok(h) = translate_path(&self.view.fs, self.pid, libc::AT_FDCWD, path, true) {
139 return h;
140 }
141 let mut guest = if path.first() == Some(&b'/') {
142 Vec::new()
143 } else {
144 self.cwd()
145 };
146 if guest != b"/" && !guest.is_empty() {
147 guest.push(b'/');
148 }
149 guest.extend_from_slice(path);
150 self.view.fs.substitute_guest_to_host(&guest)
151 }
152
153 fn cwd(&self) -> Vec<u8> {
154 guest_cwd(&self.view.fs, self.pid).unwrap_or_else(|_| b"/".to_vec())
155 }
156
157 fn var(&self, name: &[u8]) -> Option<Vec<u8>> {
158 self.env.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone())
159 }
160
161 fn vars(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
162 self.env.clone()
163 }
164
165 fn read_fd(&self, fd: i32) -> std::io::Result<Vec<u8>> {
166 if fd < 0 {
167 return Err(std::io::Error::from_raw_os_error(libc::EBADF));
168 }
169 std::fs::read(format!("/proc/{}/fd/{fd}", self.pid))
170 }
171
172 fn fd_path(&self, fd: i32) -> std::io::Result<Vec<u8>> {
173 let link = read_link(format!("/proc/{}/fd/{fd}", self.pid).as_bytes())
174 .map_err(std::io::Error::from_raw_os_error)?;
175 if link.first() != Some(&b'/') {
176 return Err(std::io::Error::from_raw_os_error(libc::ENOTDIR));
177 }
178 Ok(link)
179 }
180}
181
182pub fn plan(
200 ctx: &Ctx,
201 view: &View,
202 pid: u32,
203 front: FrontEnd,
204 argv: Vec<Vec<u8>>,
205 env: EnvSource,
206) -> Result<ExecPlan> {
207 let env = match env {
208 EnvSource::Address { envp, ptr_size } => read_env(ctx, pid, envp, ptr_size)?,
209 EnvSource::Given(e) => e,
210 };
211 let tracee = Tracee { view, pid, env };
212 let args: Vec<OsString> = argv
213 .iter()
214 .skip(1)
215 .map(|a| OsString::from_vec(a.clone()))
216 .collect();
217 let parsed = match front {
218 FrontEnd::Proot => cli::parse(args, &tracee),
219 FrontEnd::Bwrap => cli::bwrap::parse(args, &tracee),
220 };
221 let message = |code: i32, text: String, to_stdout: bool| -> Result<ExecPlan> {
222 note!(ctx, 1, "pid {pid} nested launch: {}", text.trim_end());
223 Ok(ExecPlan::message(Trampoline {
224 exit: Some((code, text.into_bytes(), to_stdout)),
225 ..Trampoline::default()
226 }))
227 };
228 let config = match parsed {
229 Ok(c) => c,
230 Err(CliError::Exit { code, text }) => return message(code, text, true),
231 Err(CliError::Usage(msg)) => return message(125, format!("proot: {msg}\n"), false),
232 };
233 let uname_served = ctx.config.kernel_release.is_some() || ctx.config.hostname.is_some();
234 if config.fake_root && !ctx.root.fake_root {
235 return message(
236 1,
237 "proot: -0 inside proot-next needs the outer proot started with -0\n".into(),
238 false,
239 );
240 }
241 if config.kernel_release.is_some() && !uname_served {
242 return message(
243 1,
244 "proot: -k inside proot-next needs the outer proot started with -k\n".into(),
245 false,
246 );
247 }
248 let mut config = config;
249 if !uname_served {
250 config.hostname = None;
251 }
252 compose_sub_bindings(&view.fs, &mut config);
253 let keep = std::env::var_os("PROOT_BWRAP_KEEP").is_some();
254 let nested = Arc::new(View::new(&config, ctx.procs.clone(), ctx.cache.clone(), !keep));
255 note!(
256 ctx,
257 1,
258 "pid {pid} nested {:?} launch: root {} with {} bindings",
259 front,
260 String::from_utf8_lossy(&nested.fs.root),
261 nested.fs.bindings.len()
262 );
263 let (cwd, pwd, warning) = nested_cwd(&tracee, &nested, &config);
264 let cwd_host = translate_path(&nested.fs, pid, libc::AT_FDCWD, &cwd, true)
265 .unwrap_or_else(|_| nested.fs.root.clone());
266 let command = match resolve_command(&nested, pid, &config, &tracee, &cwd) {
267 Ok(c) => c,
268 Err(e) => {
269 let name = config
270 .command
271 .first()
272 .map(|c| c.as_bytes().to_vec())
273 .unwrap_or_default();
274 let reason = std::io::Error::from_raw_os_error(e);
275 let reason = reason.to_string();
276 let reason = reason.split(" (os error").next().unwrap_or(&reason).to_string();
277 let text = if config.bwrap_mode {
278 format!("bwrap: execvp {}: {reason}\n", String::from_utf8_lossy(&name))
279 } else {
280 format!(
281 "proot: cannot execute {}: {reason}\n",
282 String::from_utf8_lossy(&name)
283 )
284 };
285 return message(if e == libc::ENOENT { 127 } else { 126 }, text, false);
286 }
287 };
288 let mut cmd_argv: Vec<Vec<u8>> = config.command.iter().map(|c| c.as_bytes().to_vec()).collect();
289 if let Some(a0) = &config.argv0 {
290 cmd_argv[0] = a0.as_bytes().to_vec();
291 }
292 let mut inner_env: Vec<(Vec<u8>, Vec<u8>)> = match &config.env {
293 Some(e) => e
294 .iter()
295 .map(|c| {
296 let b = c.as_bytes();
297 match b.iter().position(|&x| x == b'=') {
298 Some(i) => (b[..i].to_vec(), b[i + 1..].to_vec()),
299 None => (b.to_vec(), Vec::new()),
300 }
301 })
302 .collect(),
303 None => tracee.env.clone(),
304 };
305 if config.bwrap_mode {
306 inner_env.retain(|(k, _)| k != b"PWD");
308 inner_env.push((b"PWD".to_vec(), pwd));
309 }
310 let mut plan = super::plan_in(
311 ctx,
312 &nested,
313 pid,
314 libc::AT_FDCWD,
315 &command,
316 cmd_argv,
317 true,
318 EnvSource::Given(inner_env),
319 )?;
320 if let Some(t) = plan.trampoline.as_mut() {
321 if t.exit.is_some() {
322 return Ok(plan);
323 }
324 merge_setup(t, &config, warning);
325 return Ok(plan);
326 }
327 let mut setup = Trampoline {
328 enter: Some((nested, cwd)),
329 chdir: if ctx.no_ptrace { None } else { Some(cwd_host) },
330 ..Trampoline::default()
331 };
332 merge_setup(&mut setup, &config, warning);
333 plan.trampoline = Some(setup);
334 plan.prefix = plan.argv.len();
335 plan.changed = true;
336 Ok(plan)
337}
338
339fn compose_sub_bindings(outer: &Fs, config: &mut Config) {
359 let targets: Vec<(Vec<u8>, Vec<u8>, bool)> = config
360 .bindings
361 .iter()
362 .map(|b| (b.host.clone(), b.guest.clone(), b.read_only))
363 .collect();
364 for (host, guest, read_only) in targets {
365 let source = detranslate_path(outer, &host);
366 for b in &outer.bindings {
367 if b.guest != source && is_prefix(&source, &b.guest) {
368 config.bindings.push(Binding {
369 host: b.host.clone(),
370 guest: join_prefix(&guest, rest_after(&source, &b.guest)),
371 read_only: read_only || b.read_only,
372 });
373 }
374 }
375 }
376}
377
378fn merge_setup(t: &mut Trampoline, config: &Config, warning: Option<String>) {
382 if t.env.is_none() {
383 t.env = config
384 .env
385 .as_ref()
386 .map(|e| e.iter().map(|c| c.as_bytes().to_vec()).collect());
387 }
388 t.new_session |= config.new_session;
389 t.die_with_parent |= config.kill_on_exit && config.bwrap_mode;
390 t.seccomp_programs.extend(config.seccomp_programs.iter().cloned());
391 t.info_fds.extend(config.info_fds.iter().copied());
392 t.json_status_fds.extend(config.json_status_fds.iter().copied());
393 if let Some(w) = warning {
394 t.warnings.push(w.into_bytes());
395 }
396}
397
398fn nested_cwd(tracee: &Tracee, nested: &View, config: &Config) -> (Vec<u8>, Vec<u8>, Option<String>) {
410 let wanted: Option<Vec<u8>> = if config.cwd_default {
411 let host = tracee.host_path(&tracee.cwd());
412 nested.fs.substitute_host_to_guest(&host)
413 } else if config.cwd.first() == Some(&b'/') {
414 Some(config.cwd.clone())
415 } else {
416 let mut p = b"/".to_vec();
417 p.extend_from_slice(&config.cwd);
418 Some(p)
419 };
420 let canonical = |guest: &[u8]| {
421 translate_path(&nested.fs, tracee.pid, libc::AT_FDCWD, guest, true)
422 .ok()
423 .filter(|h| std::fs::metadata(std::ffi::OsStr::from_bytes(h)).is_ok_and(|m| m.is_dir()))
424 .map(|h| detranslate_path(&nested.fs, &h))
425 };
426 if let Some(wanted) = &wanted
427 && let Some(c) = canonical(wanted)
428 {
429 return (c, wanted.clone(), None);
430 }
431 if config.cwd_default {
432 let home = config.env.as_deref().and_then(|env| {
433 env.iter()
434 .find_map(|e| e.as_bytes().strip_prefix(b"HOME=").map(<[u8]>::to_vec))
435 });
436 if let Some(home) = home.filter(|_| config.bwrap_mode)
437 && let Some(c) = canonical(&home)
438 {
439 return (c, home, None);
440 }
441 return (b"/".to_vec(), b"/".to_vec(), None);
442 }
443 (
444 b"/".to_vec(),
445 b"/".to_vec(),
446 Some(format!(
447 "proot: can't chdir to {} in the guest rootfs, using /\n",
448 String::from_utf8_lossy(&wanted.unwrap_or_default())
449 )),
450 )
451}
452
453fn resolve_command(nested: &View, pid: u32, config: &Config, tracee: &Tracee, cwd: &[u8]) -> Result<Vec<u8>> {
459 let name = config
460 .command
461 .first()
462 .map(|c| c.as_bytes().to_vec())
463 .ok_or(libc::ENOENT)?;
464 if name.is_empty() {
465 return Err(libc::ENOENT);
466 }
467 let candidates: Vec<Vec<u8>> = if name.contains(&b'/') {
468 if name.first() == Some(&b'/') {
469 vec![name]
470 } else {
471 let mut p = cwd.to_vec();
472 if p != b"/" {
473 p.push(b'/');
474 }
475 p.extend_from_slice(&name);
476 vec![p]
477 }
478 } else {
479 let path_var = config
480 .env
481 .as_ref()
482 .and_then(|e| {
483 e.iter()
484 .find_map(|c| c.as_bytes().strip_prefix(b"PATH=").map(|v| v.to_vec()))
485 })
486 .or_else(|| tracee.var(b"PATH"))
487 .unwrap_or_else(|| b"/usr/local/bin:/usr/bin:/bin".to_vec());
488 path_var
489 .split(|&c| c == b':')
490 .filter(|d| d.first() == Some(&b'/'))
491 .map(|d| {
492 let mut p = d.to_vec();
493 if !p.ends_with(b"/") {
494 p.push(b'/');
495 }
496 p.extend_from_slice(&name);
497 p
498 })
499 .collect()
500 };
501 let mut last = libc::ENOENT;
502 for guest in candidates {
503 let Ok(host) = translate_path(&nested.fs, pid, libc::AT_FDCWD, &guest, true) else {
504 continue;
505 };
506 match executable(&host) {
507 Ok(()) => return Ok(guest),
508 Err(e) => {
509 if e != libc::ENOENT {
510 last = e;
511 }
512 }
513 }
514 }
515 Err(last)
516}
517
518fn executable(host: &[u8]) -> Result<()> {
521 let c = std::ffi::CString::new(host.to_vec()).map_err(|_| libc::ENOENT)?;
522 let mut st: libc::stat = unsafe { std::mem::zeroed() };
523 if unsafe { libc::stat(c.as_ptr(), &mut st) } < 0 {
524 return Err(crate::sys::errno());
525 }
526 if st.st_mode & libc::S_IFMT == libc::S_IFDIR {
527 return Err(libc::EACCES);
528 }
529 if unsafe { libc::access(c.as_ptr(), libc::X_OK) } < 0 {
530 return Err(crate::sys::errno());
531 }
532 Ok(())
533}
534
535fn read_env(ctx: &Ctx, pid: u32, envp: u64, ptr_size: usize) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
543 if envp == 0 {
544 return Ok(Vec::new());
545 }
546 let ptrs = ctx.mem.read_ptr_array_sized(pid, envp, MAX_ENV, ptr_size)?;
547 let mut out = Vec::with_capacity(ptrs.len());
548 for p in ptrs {
549 let entry = ctx.mem.read_string(pid, p, MAX_ARG_STRLEN)?;
550 match entry.iter().position(|&b| b == b'=') {
551 Some(i) => out.push((entry[..i].to_vec(), entry[i + 1..].to_vec())),
552 None => out.push((entry, Vec::new())),
553 }
554 }
555 Ok(out)
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn detects_our_binary_by_inode_and_others_by_name() {
564 let us = std::fs::read_link("/proc/self/exe").unwrap();
565 let us = us.as_os_str().as_bytes();
566 assert_eq!(
567 detect(us, b"/x/anything", b"/x/anything", b"anything", true),
568 Some(FrontEnd::Proot)
569 );
570 assert_eq!(
571 detect(us, b"/x/anything", b"/x/anything", b"bwrap", true),
572 Some(FrontEnd::Bwrap)
573 );
574 assert_eq!(
575 detect(us, b"/x/anything", b"/x/anything", b"anything", false),
576 None
577 );
578 assert_eq!(
579 detect(b"/bin/true", b"/usr/bin/bwrap", b"/usr/bin/bwrap", b"bwrap", true),
580 Some(FrontEnd::Bwrap)
581 );
582 assert_eq!(
583 detect(b"/bin/true", b"proot", b"/usr/bin/proot", b"proot", true),
584 Some(FrontEnd::Proot)
585 );
586 assert_eq!(
587 detect(b"/bin/true", b"/usr/bin/true", b"/usr/bin/true", b"true", true),
588 None
589 );
590 assert_eq!(
591 detect(
592 b"/bin/true",
593 b"/usr/bin/bwrap",
594 b"/usr/bin/bwrap",
595 b"bwrap",
596 false
597 ),
598 None
599 );
600 }
601}