Skip to main content

tracexec_core/
copy.rs

1use std::{
2  borrow::Cow,
3  ops::Range,
4};
5
6use itertools::Itertools;
7use nix::fcntl::OFlag;
8
9use crate::{
10  cli::args::ModifierArgs,
11  event::{
12    ExecEvent,
13    OutputMsg,
14    RuntimeModifier,
15    TracerEventDetails,
16  },
17  proc::{
18    BaselineInfo,
19    FileDescriptorInfoCollection,
20  },
21};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CopyTarget {
25  Line,
26  Commandline(SupportedShell),
27  CommandlineWithFullEnv(SupportedShell),
28  CommandlineWithStdio(SupportedShell),
29  CommandlineWithFds(SupportedShell),
30  Env,
31  Argv,
32  ArgvJoined,
33  Filename,
34  SyscallResult,
35  EnvDiff,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SupportedShell {
40  Bash,
41  Sh,
42  Fish,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CommandlinePartKind {
47  Plain,
48  TracerEvent,
49  Arg0,
50  Cwd,
51  DeletedEnvVar,
52  AddedEnvVar,
53  ModifiedEnvVar,
54  UnchangedEnvKey,
55  UnchangedEnvVal,
56  Filename,
57  Argv,
58  InlineTracerError,
59  ModifiedFdInCommandline,
60  RemovedFdInCommandline,
61  CloexecFdInCommandline,
62  AddedFdInCommandline,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum OutputStatus {
67  Ok,
68  PartialOk,
69  Err,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct CommandlinePart {
74  pub text: String,
75  pub kind: CommandlinePartKind,
76  pub output_status: Option<OutputStatus>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Commandline {
81  pub parts: Vec<CommandlinePart>,
82  pub cwd_range: Option<Range<usize>>,
83  pub env_range: Option<Range<usize>>,
84}
85
86impl std::fmt::Display for Commandline {
87  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88    for part in &self.parts {
89      f.write_str(&part.text)?;
90    }
91    Ok(())
92  }
93}
94
95impl Commandline {
96  fn push(&mut self, text: impl Into<String>, kind: CommandlinePartKind) {
97    self.parts.push(CommandlinePart {
98      text: text.into(),
99      kind,
100      output_status: None,
101    });
102  }
103
104  fn push_bash_escaped(&mut self, msg: &OutputMsg, kind: CommandlinePartKind) {
105    self.parts.push(CommandlinePart {
106      text: msg.bash_escaped().into_owned(),
107      kind,
108      output_status: Some(output_status(msg)),
109    });
110  }
111}
112
113fn output_status(msg: &OutputMsg) -> OutputStatus {
114  match msg {
115    OutputMsg::Ok(_) => OutputStatus::Ok,
116    OutputMsg::PartialOk(_) => OutputStatus::PartialOk,
117    OutputMsg::Err(_) => OutputStatus::Err,
118  }
119}
120
121fn handle_stdio_fd(
122  fd: i32,
123  fdinfo_orig: &crate::proc::FileDescriptorInfo,
124  curr: &FileDescriptorInfoCollection,
125  commandline: &mut Commandline,
126) {
127  let (fdstr, redir) = match fd {
128    0 => (" 0", "<"),
129    1 => (" 1", ">"),
130    2 => (" 2", "2>"),
131    _ => unreachable!(),
132  };
133
134  if let Some(fdinfo) = curr.get(fd) {
135    if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
136      commandline.push(fdstr, CommandlinePartKind::CloexecFdInCommandline);
137      commandline.push(">&-", CommandlinePartKind::CloexecFdInCommandline);
138    } else if fdinfo.not_same_file_as(fdinfo_orig) {
139      commandline.push(" ", CommandlinePartKind::Plain);
140      commandline.push(redir, CommandlinePartKind::ModifiedFdInCommandline);
141      commandline.push_bash_escaped(&fdinfo.path, CommandlinePartKind::ModifiedFdInCommandline);
142    }
143  } else if curr.is_reliable() {
144    commandline.push(fdstr, CommandlinePartKind::CloexecFdInCommandline);
145    commandline.push(">&-", CommandlinePartKind::RemovedFdInCommandline);
146  }
147}
148
149pub fn exec_commandline(
150  event: &ExecEvent,
151  baseline: &BaselineInfo,
152  modifier: &ModifierArgs,
153  rt_modifier: RuntimeModifier,
154  full_env: bool,
155  _shell: SupportedShell,
156) -> Commandline {
157  let mut commandline = Commandline {
158    parts: Vec::new(),
159    cwd_range: None,
160    env_range: None,
161  };
162  commandline.push("env", CommandlinePartKind::TracerEvent);
163
164  let ExecEvent {
165    cwd,
166    filename,
167    argv,
168    env_diff,
169    fdinfo,
170    envp,
171    has_dash_env,
172    ..
173  } = event;
174
175  // Handle argv[0]
176  let _ = argv.as_deref().inspect(|v| {
177    v.first().inspect(|&arg0| {
178      if filename != arg0 {
179        commandline.push(" ", CommandlinePartKind::Plain);
180        commandline.push("-a ", CommandlinePartKind::Arg0);
181        commandline.push_bash_escaped(arg0, CommandlinePartKind::Arg0);
182      }
183    });
184  });
185
186  // Handle cwd
187  if cwd != &baseline.cwd && rt_modifier.show_cwd {
188    let start = commandline.parts.len();
189    commandline.push(" ", CommandlinePartKind::Plain);
190    commandline.push("-C ", CommandlinePartKind::Cwd);
191    commandline.push_bash_escaped(cwd, CommandlinePartKind::Cwd);
192    commandline.cwd_range = Some(start..commandline.parts.len());
193  }
194
195  // Handle env
196  if rt_modifier.show_env {
197    let start = commandline.parts.len();
198    if !full_env {
199      if let Ok(env_diff) = env_diff {
200        for k in env_diff.removed.iter() {
201          commandline.push(" ", CommandlinePartKind::Plain);
202          commandline.push("-u ", CommandlinePartKind::DeletedEnvVar);
203          commandline.push_bash_escaped(k, CommandlinePartKind::DeletedEnvVar);
204        }
205        if env_diff.need_env_argument_separator(filename) {
206          commandline.push(" ", CommandlinePartKind::Plain);
207          commandline.push("--", CommandlinePartKind::Plain);
208        }
209        for (k, v) in env_diff.added.iter() {
210          commandline.push(" ", CommandlinePartKind::Plain);
211          commandline.push_bash_escaped(k, CommandlinePartKind::AddedEnvVar);
212          commandline.push("=", CommandlinePartKind::AddedEnvVar);
213          commandline.push_bash_escaped(v, CommandlinePartKind::AddedEnvVar);
214        }
215        for (k, v) in env_diff.modified.iter() {
216          commandline.push(" ", CommandlinePartKind::Plain);
217          commandline.push_bash_escaped(k, CommandlinePartKind::ModifiedEnvVar);
218          commandline.push("=", CommandlinePartKind::ModifiedEnvVar);
219          commandline.push_bash_escaped(v, CommandlinePartKind::ModifiedEnvVar);
220        }
221      }
222    } else if let Ok(envp) = &**envp {
223      commandline.push(" ", CommandlinePartKind::Plain);
224      commandline.push("-i", CommandlinePartKind::Plain);
225      if *has_dash_env || (envp.is_empty() && filename.as_ref().starts_with('-')) {
226        commandline.push(" --", CommandlinePartKind::Plain);
227      }
228      for (k, v) in envp.iter() {
229        commandline.push(" ", CommandlinePartKind::Plain);
230        commandline.push_bash_escaped(k, CommandlinePartKind::UnchangedEnvKey);
231        commandline.push("=", CommandlinePartKind::UnchangedEnvKey);
232        commandline.push_bash_escaped(v, CommandlinePartKind::UnchangedEnvVal);
233      }
234    }
235    commandline.env_range = Some(start..commandline.parts.len());
236  }
237
238  commandline.push(" ", CommandlinePartKind::Plain);
239  // Filename
240  commandline.push_bash_escaped(filename, CommandlinePartKind::Filename);
241
242  // Argv[1..]
243  match argv.as_ref() {
244    Ok(argv) => {
245      for arg in argv.iter().skip(1) {
246        commandline.push(" ", CommandlinePartKind::Plain);
247        commandline.push_bash_escaped(arg, CommandlinePartKind::Argv);
248      }
249    }
250    Err(_) => {
251      commandline.push(" ", CommandlinePartKind::Plain);
252      commandline.push(
253        "[failed to read argv]",
254        CommandlinePartKind::InlineTracerError,
255      );
256    }
257  }
258
259  // FD
260  if modifier.stdio_in_cmdline {
261    for (fd, fdinfo_orig) in baseline.fdinfo.stdio() {
262      handle_stdio_fd(fd, fdinfo_orig, fdinfo, &mut commandline);
263    }
264  }
265
266  if modifier.fd_in_cmdline {
267    for (&fd, fdinfo) in fdinfo.fdinfo.iter() {
268      if fd < 3 {
269        continue;
270      }
271      if fdinfo.flags.ok().is_none() || fdinfo.flags.intersects(OFlag::O_CLOEXEC) {
272        // Skip fds that will be closed upon exec
273        continue;
274      }
275      commandline.push(" ", CommandlinePartKind::Plain);
276      commandline.push(fd.to_string(), CommandlinePartKind::AddedFdInCommandline);
277      commandline.push("<>", CommandlinePartKind::AddedFdInCommandline);
278      commandline.push_bash_escaped(&fdinfo.path, CommandlinePartKind::AddedFdInCommandline);
279    }
280  }
281
282  commandline
283}
284
285pub fn text_for_copy<'a>(
286  event_details: &'a TracerEventDetails,
287  baseline: &BaselineInfo,
288  target: CopyTarget,
289  _modifier_args: &ModifierArgs,
290  _rt_modifier: RuntimeModifier,
291) -> Cow<'a, str> {
292  let TracerEventDetails::Exec(event) = event_details else {
293    panic!("Copy target {target:?} is only available for Exec events");
294  };
295  let mut modifier_args = ModifierArgs::default();
296  match target {
297    CopyTarget::Commandline(shell) => exec_commandline(
298      event,
299      baseline,
300      &modifier_args,
301      RuntimeModifier::default(),
302      false,
303      shell,
304    )
305    .to_string()
306    .into(),
307    CopyTarget::CommandlineWithFullEnv(shell) => exec_commandline(
308      event,
309      baseline,
310      &modifier_args,
311      RuntimeModifier::default(),
312      true,
313      shell,
314    )
315    .to_string()
316    .into(),
317    CopyTarget::CommandlineWithStdio(shell) => {
318      modifier_args.stdio_in_cmdline = true;
319      exec_commandline(
320        event,
321        baseline,
322        &modifier_args,
323        RuntimeModifier::default(),
324        false,
325        shell,
326      )
327      .to_string()
328      .into()
329    }
330    CopyTarget::CommandlineWithFds(shell) => {
331      modifier_args.fd_in_cmdline = true;
332      modifier_args.stdio_in_cmdline = true;
333      exec_commandline(
334        event,
335        baseline,
336        &modifier_args,
337        RuntimeModifier::default(),
338        false,
339        shell,
340      )
341      .to_string()
342      .into()
343    }
344    CopyTarget::Env => match event.envp.as_ref() {
345      Ok(envp) => envp
346        .iter()
347        .map(|(k, v)| format!("{k}={v}"))
348        .join("\n")
349        .into(),
350      Err(e) => format!("[failed to read envp: {e}]").into(),
351    },
352    CopyTarget::EnvDiff => {
353      let Ok(env_diff) = event.env_diff.as_ref() else {
354        return "[failed to read envp]".into();
355      };
356      let mut result = String::new();
357      result.push_str("# Added:\n");
358      for (k, v) in env_diff.added.iter() {
359        result.push_str(&format!("{k}={v}\n"));
360      }
361      result.push_str("# Modified: (original first)\n");
362      for (k, original, current) in env_diff.modified_with_values(&baseline.env) {
363        result.push_str(&format!("{}={}\n{}={}\n", k, original, k, current));
364      }
365      result.push_str("# Removed:\n");
366      for (k, value) in env_diff.removed_with_values(&baseline.env) {
367        result.push_str(&format!("{k}={value}\n"));
368      }
369      result.into()
370    }
371    CopyTarget::Argv => TracerEventDetails::argv_to_string(&event.argv).into(),
372    CopyTarget::ArgvJoined => match event.argv.as_ref() {
373      Ok(argv) => argv.iter().map(AsRef::as_ref).join(" ").into(),
374      Err(_) => "[failed to read argv]".into(),
375    },
376    CopyTarget::Filename => Cow::Borrowed(event.filename.as_ref()),
377    CopyTarget::SyscallResult => event.result.to_string().into(),
378    CopyTarget::Line => panic!("CopyTarget::Line requires a presentation formatter"),
379  }
380}
381
382#[cfg(test)]
383mod tests {
384  use std::{
385    collections::BTreeMap,
386    sync::Arc,
387  };
388
389  use nix::unistd::Pid;
390
391  use super::*;
392  use crate::{
393    event::ExecSyscall,
394    proc::{
395      CgroupInfo,
396      Cred,
397      diff_env,
398    },
399  };
400
401  fn msg(value: &str) -> OutputMsg {
402    OutputMsg::Ok(value.into())
403  }
404
405  fn filename_event(
406    baseline: &BaselineInfo,
407    envp: BTreeMap<OutputMsg, OutputMsg>,
408    filename: &str,
409  ) -> ExecEvent {
410    let has_dash_env = envp.keys().any(|key| key.as_ref().starts_with('-'));
411    let filename = msg(filename);
412    ExecEvent {
413      syscall: ExecSyscall::Execve,
414      exec_pid: Pid::from_raw(1),
415      pid: Pid::from_raw(1),
416      cwd: baseline.cwd.clone(),
417      comm: "false".into(),
418      filename: filename.clone(),
419      argv: Arc::new(Ok(vec![filename])),
420      env_diff: Ok(diff_env(&baseline.env, &envp)),
421      envp: Arc::new(Ok(envp)),
422      has_dash_env,
423      cred: Ok(Cred::default()),
424      interpreter: None,
425      fdinfo: Arc::new(FileDescriptorInfoCollection::default()),
426      result: 0,
427      timestamp: chrono::Local::now(),
428      parent: None,
429      cgroup: CgroupInfo::NotCollected,
430    }
431  }
432
433  fn baseline(env: BTreeMap<OutputMsg, OutputMsg>) -> BaselineInfo {
434    BaselineInfo {
435      cwd: msg("/work"),
436      env,
437      fdinfo: FileDescriptorInfoCollection::default(),
438    }
439  }
440
441  #[test]
442  fn diff_env_commandline_separates_dash_filename_when_env_is_unchanged() {
443    let env = BTreeMap::from([(msg("UNCHANGED"), msg("value"))]);
444    let baseline = baseline(env.clone());
445    let event = filename_event(&baseline, env, "--ignore-signal");
446
447    let commandline = exec_commandline(
448      &event,
449      &baseline,
450      &ModifierArgs::default(),
451      RuntimeModifier::default(),
452      false,
453      SupportedShell::Bash,
454    );
455
456    assert_eq!(commandline.to_string(), "env -- --ignore-signal");
457  }
458
459  #[test]
460  fn full_env_commandline_separates_dash_filename_when_env_is_empty() {
461    let baseline = baseline(BTreeMap::new());
462    let event = filename_event(&baseline, BTreeMap::new(), "--ignore-signal");
463
464    let commandline = exec_commandline(
465      &event,
466      &baseline,
467      &ModifierArgs::default(),
468      RuntimeModifier::default(),
469      true,
470      SupportedShell::Bash,
471    );
472
473    assert_eq!(commandline.to_string(), "env -i -- --ignore-signal");
474  }
475
476  #[test]
477  fn empty_env_commandlines_do_not_separate_non_dash_filename() {
478    let baseline = baseline(BTreeMap::new());
479    let event = filename_event(&baseline, BTreeMap::new(), "/bin/false");
480
481    let diff_env = exec_commandline(
482      &event,
483      &baseline,
484      &ModifierArgs::default(),
485      RuntimeModifier::default(),
486      false,
487      SupportedShell::Bash,
488    );
489    let full_env = exec_commandline(
490      &event,
491      &baseline,
492      &ModifierArgs::default(),
493      RuntimeModifier::default(),
494      true,
495      SupportedShell::Bash,
496    );
497
498    assert_eq!(diff_env.to_string(), "env /bin/false");
499    assert_eq!(full_env.to_string(), "env -i /bin/false");
500  }
501}