tracexec_core/
cmdbuilder.rs

1// MIT License
2
3// Copyright (c) 2018 Wez Furlong
4// Copyright (c) 2024 Levi Zim
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in all
14// copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22// SOFTWARE.
23
24//! Modified from https://github.com/wez/wezterm/tree/main/pty
25
26#![allow(unused)]
27
28use std::{
29  collections::BTreeMap,
30  env,
31  ffi::{
32    CString,
33    OsStr,
34    OsString,
35  },
36  os::unix::ffi::OsStringExt,
37  path::{
38    Path,
39    PathBuf,
40  },
41};
42
43use color_eyre::eyre::{
44  Context,
45  bail,
46};
47use nix::libc;
48use tracing::warn;
49
50fn get_shell() -> String {
51  use std::{
52    ffi::CStr,
53    path::Path,
54    str,
55  };
56
57  use nix::unistd::{
58    AccessFlags,
59    access,
60  };
61
62  let ent = unsafe { libc::getpwuid(libc::getuid()) };
63  if !ent.is_null() {
64    let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
65    match shell.to_str().map(str::to_owned) {
66      Err(err) => {
67        warn!(
68          "passwd database shell could not be \
69                     represented as utf-8: {err:#}, \
70                     falling back to /bin/sh"
71        );
72      }
73      Ok(shell) => {
74        if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) {
75          warn!(
76            "passwd database shell={shell:?} which is \
77                         not executable ({err:#}), falling back to /bin/sh"
78          );
79        } else {
80          return shell;
81        }
82      }
83    }
84  }
85  "/bin/sh".into()
86}
87
88/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
89/// The interface is intentionally similar to that of `std::process::Command`.
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct CommandBuilder {
92  args: Vec<OsString>,
93  cwd: Option<PathBuf>,
94  pub(crate) umask: Option<libc::mode_t>,
95  controlling_tty: bool,
96}
97
98impl CommandBuilder {
99  /// Create a new builder instance with argv[0] set to the specified
100  /// program.
101  pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
102    Self {
103      args: vec![program.as_ref().to_owned()],
104      cwd: None,
105      umask: None,
106      controlling_tty: true,
107    }
108  }
109
110  /// Create a new builder instance from a pre-built argument vector
111  pub fn from_argv(args: Vec<OsString>) -> Self {
112    Self {
113      args,
114      cwd: None,
115      umask: None,
116      controlling_tty: true,
117    }
118  }
119
120  /// Set whether we should set the pty as the controlling terminal.
121  /// The default is true, which is usually what you want, but you
122  /// may need to set this to false if you are crossing container
123  /// boundaries (eg: flatpak) to workaround issues like:
124  /// <https://github.com/flatpak/flatpak/issues/3697>
125  /// <https://github.com/flatpak/flatpak/issues/3285>
126  pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
127    self.controlling_tty = controlling_tty;
128  }
129
130  pub fn get_controlling_tty(&self) -> bool {
131    self.controlling_tty
132  }
133
134  /// Create a new builder instance that will run some idea of a default
135  /// program.  Such a builder will panic if `arg` is called on it.
136  pub fn new_default_prog() -> Self {
137    Self {
138      args: vec![],
139      cwd: None,
140      umask: None,
141      controlling_tty: true,
142    }
143  }
144
145  /// Returns true if this builder was created via `new_default_prog`
146  pub fn is_default_prog(&self) -> bool {
147    self.args.is_empty()
148  }
149
150  /// Append an argument to the current command line.
151  /// Will panic if called on a builder created via `new_default_prog`.
152  pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
153    if self.is_default_prog() {
154      panic!("attempted to add args to a default_prog builder");
155    }
156    self.args.push(arg.as_ref().to_owned());
157  }
158
159  /// Append a sequence of arguments to the current command line
160  pub fn args<I, S>(&mut self, args: I)
161  where
162    I: IntoIterator<Item = S>,
163    S: AsRef<OsStr>,
164  {
165    for arg in args {
166      self.arg(arg);
167    }
168  }
169
170  pub fn get_argv(&self) -> &Vec<OsString> {
171    &self.args
172  }
173
174  pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
175    &mut self.args
176  }
177
178  pub fn cwd<D>(&mut self, dir: D)
179  where
180    D: AsRef<Path>,
181  {
182    self.cwd = Some(dir.as_ref().to_owned());
183  }
184
185  pub fn clear_cwd(&mut self) {
186    self.cwd.take();
187  }
188
189  pub fn get_cwd(&self) -> Option<&Path> {
190    self.cwd.as_deref()
191  }
192}
193
194impl CommandBuilder {
195  pub fn umask(&mut self, mask: Option<libc::mode_t>) {
196    self.umask = mask;
197  }
198
199  fn resolve_path(&self) -> Option<OsString> {
200    env::var_os("PATH")
201  }
202
203  fn search_path(&self, exe: &OsStr, cwd: &Path) -> color_eyre::Result<PathBuf> {
204    use std::path::Path;
205
206    use nix::unistd::{
207      AccessFlags,
208      access,
209    };
210
211    let exe_path: &Path = exe.as_ref();
212    if exe_path.is_relative() {
213      let abs_path = cwd.join(exe_path);
214      if abs_path.exists() {
215        return Ok(abs_path);
216      }
217
218      if let Some(path) = self.resolve_path() {
219        for path in std::env::split_paths(&path) {
220          let candidate = path.join(exe);
221          if access(&candidate, AccessFlags::X_OK).is_ok() {
222            return Ok(candidate);
223          }
224        }
225      }
226      bail!(
227        "Unable to spawn {} because it doesn't exist on the filesystem \
228                and was not found in PATH",
229        exe_path.display()
230      );
231    } else {
232      if let Err(err) = access(exe_path, AccessFlags::X_OK) {
233        bail!(
234          "Unable to spawn {} because it doesn't exist on the filesystem \
235                    or is not executable ({err:#})",
236          exe_path.display()
237        );
238      }
239
240      Ok(PathBuf::from(exe))
241    }
242  }
243
244  /// Convert the CommandBuilder to a `Command` instance.
245  pub(crate) fn build(self) -> color_eyre::Result<Command> {
246    use std::os::unix::process::CommandExt;
247    let cwd = env::current_dir()?;
248    let dir = if let Some(dir) = self.cwd.as_deref() {
249      dir.to_owned()
250    } else {
251      cwd
252    };
253    let resolved = self.search_path(&self.args[0], &dir)?;
254    tracing::trace!("resolved path to {:?}", resolved);
255
256    Ok(Command {
257      program: resolved,
258      args: self
259        .args
260        .into_iter()
261        .map(|a| CString::new(a.into_vec()))
262        .collect::<Result<_, _>>()?,
263      cwd: dir,
264    })
265  }
266}
267
268pub struct Command {
269  pub program: PathBuf,
270  pub args: Vec<CString>,
271  pub cwd: PathBuf,
272}
273
274#[cfg(test)]
275mod tests {
276  use std::{
277    ffi::{
278      OsStr,
279      OsString,
280    },
281    fs::{
282      self,
283      File,
284    },
285    os::unix::fs::PermissionsExt,
286    path::PathBuf,
287  };
288
289  use rusty_fork::rusty_fork_test;
290  use tempfile::TempDir;
291
292  use super::*;
293
294  fn make_executable(dir: &TempDir, name: &str) -> PathBuf {
295    let path = dir.path().join(name);
296    File::create(&path).unwrap();
297    let mut perms = fs::metadata(&path).unwrap().permissions();
298    perms.set_mode(0o755);
299    fs::set_permissions(&path, perms).unwrap();
300    path
301  }
302
303  #[test]
304  fn test_new_builder() {
305    let b = CommandBuilder::new("echo");
306    assert_eq!(b.get_argv(), &vec![OsString::from("echo")]);
307    assert!(b.get_cwd().is_none());
308    assert!(b.get_controlling_tty());
309  }
310
311  #[test]
312  fn test_from_argv() {
313    let argv = vec![OsString::from("ls"), OsString::from("-l")];
314    let b = CommandBuilder::from_argv(argv.clone());
315    assert_eq!(b.get_argv(), &argv);
316  }
317
318  #[test]
319  fn test_default_prog() {
320    let b = CommandBuilder::new_default_prog();
321    assert!(b.is_default_prog());
322  }
323
324  #[test]
325  #[should_panic(expected = "attempted to add args to a default_prog builder")]
326  fn test_default_prog_panics_on_arg() {
327    let mut b = CommandBuilder::new_default_prog();
328    b.arg("ls");
329  }
330
331  #[test]
332  fn test_arg_and_args() {
333    let mut b = CommandBuilder::new("cmd");
334    b.arg("a");
335    b.args(["b", "c"]);
336    let argv: Vec<&OsStr> = b.get_argv().iter().map(|s| s.as_os_str()).collect();
337    assert_eq!(argv, ["cmd", "a", "b", "c"]);
338  }
339
340  #[test]
341  fn test_cwd_set_and_clear() {
342    let mut b = CommandBuilder::new("cmd");
343    let tmp = TempDir::new().unwrap();
344
345    b.cwd(tmp.path());
346    assert_eq!(b.get_cwd(), Some(tmp.path()));
347
348    b.clear_cwd();
349    assert!(b.get_cwd().is_none());
350  }
351
352  #[test]
353  fn test_controlling_tty_flag() {
354    let mut b = CommandBuilder::new("cmd");
355    assert!(b.get_controlling_tty());
356
357    b.set_controlling_tty(false);
358    assert!(!b.get_controlling_tty());
359  }
360
361  rusty_fork_test! {
362    #[test]
363    fn test_search_path_finds_executable_in_path() {
364      let dir = TempDir::new().unwrap();
365      let exe = make_executable(&dir, "mycmd");
366
367      unsafe {
368        // SAFETY: we do this in a separate subprocess.
369        std::env::set_var("PATH", dir.path());
370      }
371
372      let b = CommandBuilder::new("mycmd");
373      let resolved = b.search_path(OsStr::new("mycmd"), dir.path()).unwrap();
374
375      assert_eq!(resolved, exe);
376    }
377  }
378
379  #[test]
380  fn test_search_path_relative_to_cwd() {
381    let dir = TempDir::new().unwrap();
382    let exe = make_executable(&dir, "tool");
383
384    let b = CommandBuilder::new("./tool");
385    let resolved = b.search_path(OsStr::new("./tool"), dir.path()).unwrap();
386
387    assert_eq!(resolved, exe);
388  }
389
390  #[test]
391  fn test_search_path_missing_binary_fails() {
392    let dir = TempDir::new().unwrap();
393    let b = CommandBuilder::new("does_not_exist");
394
395    let err = b.search_path(OsStr::new("does_not_exist"), dir.path());
396    assert!(err.is_err());
397  }
398
399  rusty_fork_test! {
400
401    #[test]
402    fn test_build_sets_program_args_and_cwd() {
403      let dir = TempDir::new().unwrap();
404      let exe = make_executable(&dir, "echo");
405
406      unsafe {
407        // SAFETY: we do this in a separate subprocess.
408        std::env::set_var("PATH", dir.path());
409      }
410
411      let mut b = CommandBuilder::new("echo");
412      b.arg("hello");
413      b.cwd(dir.path());
414
415      let cmd = b.build().unwrap();
416
417      assert_eq!(cmd.program, exe);
418      assert_eq!(cmd.cwd, dir.path());
419
420      let args: Vec<&str> = cmd.args.iter().map(|c| c.to_str().unwrap()).collect();
421
422      assert_eq!(args, ["echo", "hello"]);
423      dir.close().unwrap()
424    }
425
426    #[test]
427    fn test_build_uses_current_dir_when_cwd_not_set() {
428      let dir = TempDir::new().unwrap();
429      let exe = make_executable(&dir, "cmd");
430
431      // SAFETY: we do this in a separate subprocess.
432      unsafe { std::env::set_var("PATH", dir.path()); }
433
434      let b = CommandBuilder::new("cmd");
435      let cmd = b.build().unwrap();
436
437      assert_eq!(cmd.program, exe);
438      assert_eq!(cmd.cwd, std::env::current_dir().unwrap());
439      dir.close().unwrap()
440    }
441  }
442}