Skip to main content

tracexec_core/
tracer.rs

1use std::{
2  collections::BTreeMap,
3  fmt::Display,
4  sync::Arc,
5};
6
7use chrono::{
8  DateTime,
9  Local,
10};
11use enumflags2::BitFlags;
12use nix::{
13  errno::Errno,
14  libc::{
15    SIGRTMIN,
16    c_int,
17  },
18  unistd::{
19    Pid,
20    User,
21  },
22};
23use tokio::sync::mpsc::UnboundedSender;
24
25use crate::{
26  cli::{
27    args::{
28      LogModeArgs,
29      ModifierArgs,
30      PtraceArgs,
31    },
32    options::SeccompBpf,
33  },
34  elevate::EnvVars,
35  event::{
36    OutputMsg,
37    TracerEventDetailsKind,
38    TracerMessage,
39  },
40  printer::{
41    Printer,
42    PrinterArgs,
43  },
44  proc::{
45    BaselineInfo,
46    CgroupInfo,
47    Cred,
48    CredInspectError,
49    FileDescriptorInfoCollection,
50    Interpreter,
51  },
52  pty::UnixSlavePty,
53};
54
55pub type InspectError = Errno;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Signal {
59  Standard(nix::sys::signal::Signal),
60  Realtime(u8), // u8 is enough for Linux
61}
62
63impl Signal {
64  pub fn from_raw(raw: c_int) -> Self {
65    match nix::sys::signal::Signal::try_from(raw) {
66      Ok(sig) => Self::Standard(sig),
67      // libc might reserve some RT signals for itself.
68      // But from a tracer's perspective we don't need to care about it.
69      // So here no validation is done for the RT signal value.
70      Err(_) => Self::Realtime(raw as u8),
71    }
72  }
73
74  pub fn as_raw(self) -> i32 {
75    match self {
76      Self::Standard(signal) => signal as i32,
77      Self::Realtime(raw) => raw as i32,
78    }
79  }
80}
81
82impl From<nix::sys::signal::Signal> for Signal {
83  fn from(value: nix::sys::signal::Signal) -> Self {
84    Self::Standard(value)
85  }
86}
87
88impl Display for Signal {
89  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90    match self {
91      Self::Standard(signal) => signal.fmt(f),
92      Self::Realtime(sig) => {
93        let min = SIGRTMIN();
94        let delta = *sig as i32 - min;
95        match delta.signum() {
96          0 => write!(f, "SIGRTMIN"),
97          1 => write!(f, "SIGRTMIN+{delta}"),
98          -1 => write!(f, "SIGRTMIN{delta}"),
99          _ => unreachable!(),
100        }
101      }
102    }
103  }
104}
105
106#[derive(Default)]
107#[non_exhaustive]
108pub struct TracerBuilder {
109  pub user: Option<User>,
110  pub modifier: ModifierArgs,
111  pub mode: Option<TracerMode>,
112  pub filter: Option<BitFlags<TracerEventDetailsKind>>,
113  pub tx: Option<UnboundedSender<TracerMessage>>,
114  // TODO: remove this.
115  pub printer: Option<Printer>,
116  pub baseline: Option<Arc<BaselineInfo>>,
117  // --- ptrace specific ---
118  pub seccomp_bpf: SeccompBpf,
119  pub ptrace_polling_delay: Option<u64>,
120  pub ptrace_blocking: Option<bool>,
121  pub tracee_env: Option<EnvVars>,
122  pub tracexec_override_env: Option<EnvVars>,
123}
124
125#[allow(clippy::unwrap_used)]
126impl TracerBuilder {
127  /// Initialize a new [`TracerBuilder`]
128  pub fn new() -> Self {
129    Default::default()
130  }
131
132  /// Use blocking waitpid calls instead of polling.
133  ///
134  /// This mode conflicts with ptrace polling delay option
135  /// This option is not used in eBPF tracer.
136  pub fn ptrace_blocking(mut self, enable: bool) -> Self {
137    if self.ptrace_polling_delay.is_some() && enable {
138      panic!(
139        "Cannot enable blocking mode when ptrace polling delay implicitly specifys polling mode"
140      );
141    }
142    self.ptrace_blocking = Some(enable);
143    self
144  }
145
146  /// Sets ptrace polling delay (in microseconds)
147  /// This options conflicts with ptrace blocking mode.
148  ///
149  /// This option is not used in eBPF tracer.
150  pub fn ptrace_polling_delay(mut self, ptrace_polling_delay: Option<u64>) -> Self {
151    if Some(true) == self.ptrace_blocking && ptrace_polling_delay.is_some() {
152      panic!("Cannot set ptrace_polling_delay when operating in blocking mode")
153    }
154    self.ptrace_polling_delay = ptrace_polling_delay;
155    self
156  }
157
158  /// Applies the ptrace-specific CLI options to this builder.
159  pub fn ptrace_options(self, args: &PtraceArgs) -> Self {
160    self
161      .seccomp_bpf(args.seccomp_bpf)
162      .ptrace_blocking(args.polling_interval.is_none_or(|value| value < 0))
163      .ptrace_polling_delay(
164        args
165          .polling_interval
166          .filter(|&value| value > 0)
167          .map(|value| value as u64),
168      )
169  }
170
171  /// Sets seccomp-bpf mode for ptrace tracer
172  ///
173  /// Default to auto.
174  /// This option is not used in eBPF tracer.
175  pub fn seccomp_bpf(mut self, seccomp_bpf: SeccompBpf) -> Self {
176    self.seccomp_bpf = seccomp_bpf;
177    self
178  }
179
180  /// Sets the `User` used when spawning the command.
181  ///
182  /// Default to current user.
183  pub fn user(mut self, user: Option<User>) -> Self {
184    self.user = user;
185    self
186  }
187
188  /// Sets the environment passed to the root tracee at exec time.
189  ///
190  /// When unset, the tracee inherits tracexec's current process environment.
191  pub fn tracee_env(mut self, env: Option<EnvVars>) -> Self {
192    self.tracee_env = env;
193    self
194  }
195
196  /// Sets the environment variables tracexec should consult for before using process env vars
197  ///
198  /// Or put it simply, this overrides std::env::vars for tracexec itself only.
199  pub fn tracexec_override_env(mut self, env: Option<EnvVars>) -> Self {
200    self.tracexec_override_env = env;
201    self
202  }
203
204  pub fn modifier(mut self, modifier: ModifierArgs) -> Self {
205    self.modifier = modifier;
206    self
207  }
208
209  /// Sets the mode for the trace e.g. TUI or Log
210  pub fn mode(mut self, mode: TracerMode) -> Self {
211    self.mode = Some(mode);
212    self
213  }
214
215  /// Sets a filter for wanted tracer events.
216  pub fn filter(mut self, filter: BitFlags<TracerEventDetailsKind>) -> Self {
217    self.filter = Some(filter);
218    self
219  }
220
221  /// Passes the tx part of tracer event channel
222  ///
223  /// By default this is not set and tracer will not send events.
224  pub fn tracer_tx(mut self, tx: UnboundedSender<TracerMessage>) -> Self {
225    self.tx = Some(tx);
226    self
227  }
228
229  pub fn printer(mut self, printer: Printer) -> Self {
230    self.printer = Some(printer);
231    self
232  }
233
234  /// Create a printer from CLI options,
235  ///
236  /// Requires `modifier` and `baseline` to be set before calling.
237  pub fn printer_from_cli(mut self, tracing_args: &LogModeArgs) -> Self {
238    self.printer = Some(Printer::new(
239      PrinterArgs::from_cli(tracing_args, &self.modifier),
240      self.baseline.clone().unwrap(),
241    ));
242    self
243  }
244
245  pub fn baseline(mut self, baseline: Arc<BaselineInfo>) -> Self {
246    self.baseline = Some(baseline);
247    self
248  }
249}
250
251#[derive(Debug)]
252pub struct ExecData {
253  pub exec_pid: Pid,
254  pub filename: OutputMsg,
255  pub argv: Arc<Result<Vec<OutputMsg>, InspectError>>,
256  pub envp: Arc<Result<BTreeMap<OutputMsg, OutputMsg>, InspectError>>,
257  pub has_dash_env: bool,
258  pub cred: Result<Cred, CredInspectError>,
259  pub cwd: OutputMsg,
260  pub interpreters: Option<Vec<Interpreter>>,
261  pub fdinfo: Arc<FileDescriptorInfoCollection>,
262  pub timestamp: DateTime<Local>,
263  pub cgroup: CgroupInfo,
264}
265
266impl ExecData {
267  #[allow(clippy::too_many_arguments)]
268  pub fn new(
269    exec_pid: Pid,
270    filename: OutputMsg,
271    argv: Result<Vec<OutputMsg>, InspectError>,
272    envp: Result<BTreeMap<OutputMsg, OutputMsg>, InspectError>,
273    has_dash_env: bool,
274    cred: Result<Cred, CredInspectError>,
275    cwd: OutputMsg,
276    interpreters: Option<Vec<Interpreter>>,
277    fdinfo: FileDescriptorInfoCollection,
278    timestamp: DateTime<Local>,
279    cgroup: CgroupInfo,
280  ) -> Self {
281    Self {
282      exec_pid,
283      filename,
284      argv: Arc::new(argv),
285      envp: Arc::new(envp),
286      has_dash_env,
287      cred,
288      cwd,
289      interpreters,
290      fdinfo: Arc::new(fdinfo),
291      timestamp,
292      cgroup,
293    }
294  }
295}
296
297#[derive(Debug)]
298pub enum TracerMode {
299  Tui(Option<UnixSlavePty>),
300  Log { foreground: bool },
301}
302
303impl PartialEq for TracerMode {
304  fn eq(&self, other: &Self) -> bool {
305    // I think a plain match is more readable here
306    #[allow(clippy::match_like_matches_macro)]
307    match (self, other) {
308      (Self::Log { foreground: a }, Self::Log { foreground: b }) => a == b,
309      _ => false,
310    }
311  }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum ProcessExit {
316  Code(i32),
317  Signal(Signal),
318}
319
320#[cfg(test)]
321mod tests {
322  use std::{
323    collections::BTreeMap,
324    sync::Arc,
325  };
326
327  use chrono::Local;
328  use nix::sys::signal::Signal as NixSignal;
329  use test_that::prelude::*;
330
331  use super::*;
332  use crate::event::OutputMsg;
333
334  /* ---------------- Signal ---------------- */
335
336  #[test]
337  fn signal_from_raw_standard() {
338    let sig = Signal::from_raw(NixSignal::SIGINT as i32);
339    assert_eq!(sig, Signal::Standard(NixSignal::SIGINT));
340    assert_eq!(sig.as_raw(), NixSignal::SIGINT as i32);
341  }
342
343  #[test]
344  fn signal_from_raw_realtime() {
345    let raw = SIGRTMIN() + 3;
346    let sig = Signal::from_raw(raw);
347    assert_eq!(sig, Signal::Realtime(raw as u8));
348    assert_eq!(sig.as_raw(), raw);
349  }
350
351  #[test]
352  fn signal_display_standard() {
353    let sig = Signal::Standard(NixSignal::SIGTERM);
354    assert_eq!(sig.to_string(), "SIGTERM");
355  }
356
357  #[test]
358  fn signal_display_realtime_variants() {
359    let min = SIGRTMIN();
360
361    let sig_min = Signal::Realtime(min as u8);
362    assert_eq!(sig_min.to_string(), "SIGRTMIN");
363
364    let sig_plus = Signal::Realtime((min + 2) as u8);
365    assert_eq!(sig_plus.to_string(), "SIGRTMIN+2");
366
367    let sig_minus = Signal::Realtime((min - 1) as u8);
368    assert_eq!(sig_minus.to_string(), "SIGRTMIN-1");
369  }
370
371  /* ---------------- TracerBuilder ---------------- */
372  #[test]
373  #[should_panic(expected = "Cannot enable blocking mode")]
374  fn tracer_builder_blocking_conflict_panics() {
375    TracerBuilder::new()
376      .ptrace_polling_delay(Some(10))
377      .ptrace_blocking(true);
378  }
379
380  #[test]
381  #[should_panic(expected = "Cannot set ptrace_polling_delay")]
382  fn tracer_builder_polling_conflict_panics() {
383    TracerBuilder::new()
384      .ptrace_blocking(true)
385      .ptrace_polling_delay(Some(10));
386  }
387
388  #[test]
389  fn tracer_builder_chaining_works() {
390    let builder = TracerBuilder::new()
391      .ptrace_blocking(false)
392      .ptrace_polling_delay(None)
393      .seccomp_bpf(SeccompBpf::Auto);
394
395    assert_eq!(builder.ptrace_blocking, Some(false));
396    assert_eq!(builder.ptrace_polling_delay, None);
397  }
398
399  #[test]
400  fn tracer_builder_applies_ptrace_cli_options() {
401    let blocking = TracerBuilder::new().ptrace_options(&PtraceArgs::default());
402    assert_eq!(blocking.ptrace_blocking, Some(true));
403    assert_eq!(blocking.ptrace_polling_delay, None);
404
405    let polling = TracerBuilder::new().ptrace_options(&PtraceArgs {
406      seccomp_bpf: SeccompBpf::Off,
407      polling_interval: Some(250),
408    });
409    assert_eq!(polling.seccomp_bpf, SeccompBpf::Off);
410    assert_eq!(polling.ptrace_blocking, Some(false));
411    assert_eq!(polling.ptrace_polling_delay, Some(250));
412
413    let no_delay = TracerBuilder::new().ptrace_options(&PtraceArgs {
414      polling_interval: Some(0),
415      ..Default::default()
416    });
417    assert_eq!(no_delay.ptrace_blocking, Some(false));
418    assert_eq!(no_delay.ptrace_polling_delay, None);
419  }
420
421  /* ---------------- ExecData ---------------- */
422
423  #[test]
424  fn exec_data_new_populates_fields() {
425    let filename = OutputMsg::Ok("bin".into());
426    let argv = Ok(vec![
427      OutputMsg::Ok("bin".into()),
428      OutputMsg::Ok("-h".into()),
429    ]);
430
431    let mut envp_map = BTreeMap::new();
432    envp_map.insert(OutputMsg::Ok("A".into()), OutputMsg::Ok("B".into()));
433    let envp = Ok(envp_map);
434
435    let cwd = OutputMsg::Ok("/".into());
436    let fdinfo = FileDescriptorInfoCollection::default();
437    let timestamp = Local::now();
438
439    let exec = ExecData::new(
440      Pid::from_raw(1234),
441      filename.clone(),
442      argv,
443      envp,
444      false,
445      Err(CredInspectError::Inspect),
446      cwd.clone(),
447      None,
448      fdinfo,
449      timestamp,
450      CgroupInfo::V2 {
451        path: "/".to_string(),
452      },
453    );
454
455    assert_eq!(exec.exec_pid, Pid::from_raw(1234));
456    assert_eq!(exec.filename, filename);
457    assert_eq!(exec.cwd, cwd);
458    assert_that!(exec.argv, points_to(ok(anything())));
459    assert_that!(exec.envp, points_to(ok(anything())));
460    assert!(!exec.has_dash_env);
461    assert_that!(exec.interpreters, none());
462    assert_that!(Arc::strong_count(&exec.argv), ge(1));
463    assert_that!(Arc::strong_count(&exec.envp), ge(1));
464    assert_that!(Arc::strong_count(&exec.fdinfo), ge(1));
465  }
466
467  /* ---------------- ProcessExit ---------------- */
468
469  #[test]
470  fn process_exit_equality() {
471    let a = ProcessExit::Code(0);
472    let b = ProcessExit::Code(0);
473    let c = ProcessExit::Code(1);
474
475    assert_eq!(a, b);
476    assert_ne!(a, c);
477
478    let s1 = ProcessExit::Signal(Signal::Standard(NixSignal::SIGKILL));
479    let s2 = ProcessExit::Signal(Signal::Standard(NixSignal::SIGKILL));
480    let s3 = ProcessExit::Signal(Signal::Standard(NixSignal::SIGTERM));
481
482    assert_eq!(s1, s2);
483    assert_ne!(s1, s3);
484  }
485}