Skip to main content

proot/cli/
mod.rs

1//! PRoot command-line front end (the subset proot-apps and users rely on).
2
3pub mod bwrap;
4pub mod host;
5
6use crate::config::{Binding, Config, trim_slashes};
7use host::Host;
8use std::ffi::{CString, OsString};
9use std::os::unix::ffi::OsStrExt;
10
11/// Why parsing stopped.
12#[derive(Debug)]
13pub enum CliError {
14    /// Print `text` on stdout and exit with `code` (`--help`, `--version`).
15    Exit { code: i32, text: String },
16    /// A usage error with its message.
17    Usage(String),
18}
19
20const VERSION: &str = env!("CARGO_PKG_VERSION");
21
22const USAGE: &str = "\
23Usage:
24  proot [option] ... [command]
25
26Regular options:
27  -r *path*, --rootfs=*path*    Use *path* as the new guest root file-system, default is /.
28  -b *path*, --bind=*path*      Make the content of *path* accessible in the guest rootfs (host:guest).
29  -w *path*, --cwd=*path*       Set the initial working directory to *path*.
30  --kill-on-exit                Kill all processes on command exit (always on; accepted for compatibility).
31  -v *value*, --verbose=*value* Set the level of debug information to *value*.
32  -V, --version                 Print version, copyright, license and contact, then exit.
33  -h, --help                    Print the version and the command-line usage, then exit.
34
35Extension options:
36  -k *string*, --kernel-release=*string*  Make current kernel appear as kernel release *string*.
37  -0, --root-id                 Make current user appear as \"root\": ids read as 0, ownership changes succeed.
38  -l, --link2symlink            Accepted and ignored.
39
40Alias options:
41  -R *path*                     Alias: -r *path* + a couple of recommended -b.
42  -S *path*                     Alias: -0 -r *path* + a couple of recommended -b.
43
44Environment: PROOT_THREADS (default 4) and PROOT_SPIN_US (default 200) tune the supervisor pool;
45PROOT_NO_PTRACE=1 forces the mode used where ptrace is filtered out; PROOT_USERNS=1 forces, and
46PROOT_USERNS=0 rules out, the user namespace used where Yama's ptrace_scope is 2; where neither
47works the guest-stub mode is the fallback (x86_64 and aarch64 hosts), which PROOT_GUEST_STUB=0
48rules out and PROOT_GUEST_STUB=force takes anywhere.
49";
50
51/// Host paths bound into the guest by `-R` and `-S`, when they exist.
52const RECOMMENDED_R: &[&str] = &[
53    "/etc/host.conf",
54    "/etc/hosts",
55    "/etc/hosts.equiv",
56    "/etc/mtab",
57    "/etc/netgroup",
58    "/etc/networks",
59    "/etc/passwd",
60    "/etc/group",
61    "/etc/nsswitch.conf",
62    "/etc/resolv.conf",
63    "/etc/localtime",
64    "/dev/",
65    "/sys/",
66    "/proc/",
67    "/tmp/",
68    "/run/",
69    "/var/run/dbus/system_bus_socket",
70];
71const RECOMMENDED_S: &[&str] = &[
72    "/etc/host.conf",
73    "/etc/hosts",
74    "/etc/nsswitch.conf",
75    "/etc/resolv.conf",
76    "/dev/",
77    "/sys/",
78    "/proc/",
79    "/tmp/",
80    "/run/shm",
81];
82
83/// Parses PRoot-style arguments (without `argv[0]`).
84///
85/// Host-side paths (the root, the host side of a binding) are resolved
86/// through `host` into host paths; the working directory and the command
87/// stay guest paths.
88///
89/// # Arguments
90/// * `args` - Command-line arguments.
91/// * `host` - The process the command line is interpreted for.
92///
93/// # Returns
94/// The configuration to launch.
95///
96/// # Errors
97/// [`CliError::Exit`] after `--help`/`--version`, [`CliError::Usage`] otherwise.
98pub fn parse(args: Vec<OsString>, host: &dyn Host) -> Result<Config, CliError> {
99    let mut cfg = Config::default();
100    let mut it = args.into_iter().peekable();
101    let mut cwd_set = false;
102    while let Some(a) = it.peek().cloned() {
103        let s = a.as_bytes();
104        if s.is_empty() || s[0] != b'-' || s == b"-" {
105            break;
106        }
107        it.next();
108        if s == b"--" {
109            break;
110        }
111        // Split "--opt=value" and recognise "--opt value" / "-o value".
112        let (opt, inline): (Vec<u8>, Option<Vec<u8>>) = match s.iter().position(|&c| c == b'=') {
113            Some(i) if s.starts_with(b"--") => (s[..i].to_vec(), Some(s[i + 1..].to_vec())),
114            _ => (s.to_vec(), None),
115        };
116        let value =
117            |it: &mut std::iter::Peekable<std::vec::IntoIter<OsString>>| -> Result<Vec<u8>, CliError> {
118                if let Some(v) = inline.clone() {
119                    return Ok(v);
120                }
121                it.next().map(|v| v.as_bytes().to_vec()).ok_or_else(|| {
122                    CliError::Usage(format!("missing value for {}", String::from_utf8_lossy(&opt)))
123                })
124            };
125        match &opt[..] {
126            b"-r" | b"--rootfs" => cfg.root = host.host_path(&value(&mut it)?),
127            b"-R" | b"-S" => {
128                cfg.root = host.host_path(&value(&mut it)?);
129                let list = if opt == b"-R" {
130                    RECOMMENDED_R
131                } else {
132                    RECOMMENDED_S
133                };
134                for p in list {
135                    if host.exists(p.as_bytes()) {
136                        cfg.bindings.push(bind(host, p.as_bytes(), None));
137                    }
138                }
139                if let Some(home) = host.var(b"HOME") {
140                    cfg.bindings.push(bind(host, &home, None));
141                }
142                if opt == b"-S" {
143                    cfg.fake_root = true;
144                }
145            }
146            b"-b" | b"--bind" | b"-m" | b"--mount" => {
147                let v = value(&mut it)?;
148                match v.iter().position(|&c| c == b':') {
149                    Some(i) => cfg.bindings.push(bind(host, &v[..i], Some(&v[i + 1..]))),
150                    None => cfg.bindings.push(bind(host, &v, None)),
151                }
152            }
153            b"-w" | b"--cwd" | b"--pwd" => {
154                cfg.cwd = value(&mut it)?;
155                cwd_set = true;
156            }
157            b"--kill-on-exit" => cfg.kill_on_exit = true,
158            b"-v" | b"--verbose" => {
159                let v = value(&mut it)?;
160                cfg.verbose = String::from_utf8_lossy(&v)
161                    .parse()
162                    .map_err(|_| CliError::Usage("bad verbosity".into()))?;
163            }
164            b"-k" | b"--kernel-release" => cfg.kernel_release = Some(value(&mut it)?),
165            b"-0" | b"--root-id" => cfg.fake_root = true,
166            b"-l" | b"--link2symlink" | b"-i" | b"--change-id" | b"-p" | b"--port" => {
167                if matches!(&opt[..], b"-i" | b"--change-id" | b"-p" | b"--port") {
168                    value(&mut it)?;
169                }
170                eprintln!(
171                    "proot: option {} is accepted but not implemented yet",
172                    String::from_utf8_lossy(&opt)
173                );
174            }
175            b"-q" | b"--qemu" => return Err(CliError::Usage("qemu support is not implemented".into())),
176            b"-V" | b"--version" => {
177                return Err(CliError::Exit {
178                    code: 0,
179                    text: format!(
180                        "proot-next {VERSION}\nA Rust reimplementation of PRoot (seccomp user notification + ptrace).\n"
181                    ),
182                });
183            }
184            b"-h" | b"--help" | b"--usage" => {
185                return Err(CliError::Exit {
186                    code: 0,
187                    text: format!("proot-next {VERSION}\n\n{USAGE}"),
188                });
189            }
190            _ => {
191                return Err(CliError::Usage(format!(
192                    "unknown option {}",
193                    String::from_utf8_lossy(&opt)
194                )));
195            }
196        }
197    }
198    cfg.command = it
199        .map(|a| CString::new(a.as_bytes()).unwrap_or_default())
200        .collect();
201    if cfg.command.is_empty() {
202        let shell = host.var(b"SHELL").unwrap_or_else(|| b"/bin/sh".to_vec());
203        cfg.command = vec![CString::new(shell).unwrap_or_default()];
204    }
205    if !cwd_set {
206        // PRoot: the initial cwd is the host cwd, as a guest path, when it is visible there.
207        cfg.cwd = host.cwd();
208        cfg.cwd_default = true;
209    }
210    Ok(cfg)
211}
212
213/// A binding of `path`, as `host` names it, at `guest` (the same path by default).
214fn bind(host: &dyn Host, path: &[u8], guest: Option<&[u8]>) -> Binding {
215    let path = trim_slashes(path.to_vec());
216    let guest = trim_slashes(guest.map(|g| g.to_vec()).unwrap_or_else(|| path.clone()));
217    Binding {
218        host: trim_slashes(host.host_path(&path)),
219        guest,
220        read_only: false,
221    }
222}