Skip to main content

tracexec_core/
breakpoint.rs

1use std::borrow::Cow;
2
3use nix::unistd::Pid;
4use regex_cursor::engines::pikevm::{
5  self,
6  PikeVM,
7};
8use strum::IntoStaticStr;
9
10use crate::{
11  event::OutputMsg,
12  primitives::regex::{
13    ArgvCursor,
14    SPACE,
15  },
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct BreakPointHit {
20  pub bid: u32,
21  pub pid: Pid,
22  pub stop: BreakPointStop,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)]
26pub enum BreakPointStop {
27  SyscallEnter,
28  SyscallExit,
29}
30
31impl BreakPointStop {
32  pub fn toggle(&mut self) {
33    *self = match self {
34      Self::SyscallEnter => Self::SyscallExit,
35      Self::SyscallExit => Self::SyscallEnter,
36    }
37  }
38}
39
40#[derive(Debug, Clone)]
41pub struct BreakPointRegex {
42  regex: PikeVM,
43  editable: String,
44}
45
46#[derive(Debug, Clone)]
47pub enum BreakPointPattern {
48  /// A regular expression that matches the cmdline of the process. The cmdline is the argv
49  /// concatenated with spaces without any escaping.
50  ArgvRegex(BreakPointRegex),
51  // CmdlineRegex(BreakPointRegex),
52  InFilename(String),
53  ExactFilename(String),
54}
55
56#[derive(Debug, Clone)]
57pub enum BreakPointType {
58  /// The breakpoint will be hit once and then deactivated.
59  Once,
60  /// The breakpoint will be hit every time it is encountered.
61  Permanent,
62}
63
64#[derive(Debug, Clone)]
65pub struct BreakPoint {
66  pub pattern: BreakPointPattern,
67  pub ty: BreakPointType,
68  pub activated: bool,
69  pub stop: BreakPointStop,
70}
71
72impl BreakPointPattern {
73  pub fn pattern(&self) -> &str {
74    match self {
75      Self::ArgvRegex(regex) => regex.editable.as_str(),
76      Self::InFilename(filename) => filename,
77      // Unwrap is fine since user inputs the filename as str
78      Self::ExactFilename(filename) => filename,
79    }
80  }
81
82  pub fn to_editable(&self) -> String {
83    match self {
84      Self::ArgvRegex(regex) => format!("argv-regex:{}", regex.editable),
85      Self::InFilename(filename) => format!("in-filename:{filename}"),
86      Self::ExactFilename(filename) => {
87        format!("exact-filename:{filename}")
88      }
89    }
90  }
91
92  pub fn from_editable(editable: &str) -> Result<Self, String> {
93    if let Some((prefix, rest)) = editable.split_once(':') {
94      match prefix {
95        "in-filename" => Ok(Self::InFilename(rest.to_string())),
96        "exact-filename" => Ok(Self::ExactFilename(rest.to_string())),
97        "argv-regex" => Ok(Self::ArgvRegex(BreakPointRegex {
98          regex: PikeVM::new(rest).map_err(|e| e.to_string())?,
99          editable: rest.to_string(),
100        })),
101        _ => Err(format!("Invalid breakpoint pattern type: {prefix}!")),
102      }
103    } else {
104      Err("No valid breakpoint pattern found!".to_string())
105    }
106  }
107
108  pub fn matches(&self, argv: Option<&[OutputMsg]>, filename: &OutputMsg) -> bool {
109    match self {
110      Self::ArgvRegex(regex) => {
111        let Some(argv) = argv else {
112          return false;
113        };
114        let space = &SPACE;
115        let argv = ArgvCursor::new(argv, space);
116        pikevm::is_match(
117          &regex.regex,
118          &mut pikevm::Cache::new(&regex.regex),
119          &mut regex_cursor::Input::new(argv),
120        )
121      }
122      Self::InFilename(pattern) => {
123        let OutputMsg::Ok(filename) = filename else {
124          return false;
125        };
126        filename.contains(pattern)
127      }
128      Self::ExactFilename(path) => {
129        let OutputMsg::Ok(filename) = filename else {
130          return false;
131        };
132        filename.as_str() == path
133      }
134    }
135  }
136}
137
138impl TryFrom<&str> for BreakPoint {
139  type Error = Cow<'static, str>;
140
141  fn try_from(value: &str) -> Result<Self, Self::Error> {
142    let Some((stop, rest)) = value.split_once(':') else {
143      return Err("No valid syscall stop found! The breakpoint should start with \"sysenter:\" or \"sysexit:\".".into());
144    };
145    let stop = match stop {
146      "sysenter" => BreakPointStop::SyscallEnter,
147      "sysexit" => BreakPointStop::SyscallExit,
148      _ => {
149        return Err(
150          format!("Invalid syscall stop {stop:?}! The breakpoint should start with \"sysenter:\" or \"sysexit:\".")
151            .into(),
152        )
153      }
154    };
155    let Some((pattern_kind, pattern)) = rest.split_once(':') else {
156      return Err("No valid pattern kind found! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\".".into());
157    };
158    let pattern = match pattern_kind {
159      "argv-regex" => BreakPointPattern::ArgvRegex(BreakPointRegex {
160        regex: PikeVM::new(pattern).map_err(|e| format!("\n{e}"))?,
161        editable: pattern.to_string(),
162      }),
163      "exact-filename" => BreakPointPattern::ExactFilename(pattern.to_string()),
164      "in-filename" => BreakPointPattern::InFilename(pattern.to_string()),
165      _ => {
166        return Err(
167          format!(
168            "Invalid pattern kind {pattern_kind:?}! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\"."
169          )
170          .into(),
171        )
172      }
173    };
174    Ok(Self {
175      ty: BreakPointType::Permanent,
176      stop,
177      pattern,
178      activated: true,
179    })
180  }
181}
182
183#[cfg(test)]
184mod tests {
185  use nix::errno::Errno;
186  use test_that::prelude::*;
187
188  use super::*;
189  use crate::cache::ArcStr;
190
191  #[test]
192  fn test_breakpoint_stop_toggle() {
193    let mut s = BreakPointStop::SyscallEnter;
194    s.toggle();
195    assert_eq!(s, BreakPointStop::SyscallExit);
196    s.toggle();
197    assert_eq!(s, BreakPointStop::SyscallEnter);
198  }
199
200  #[test]
201  fn test_from_editable_and_to_editable_and_pattern() {
202    // argv-regex
203    let bp = BreakPointPattern::from_editable("argv-regex:foo").expect("argv-regex");
204    assert_eq!(bp.pattern(), "foo");
205    assert_eq!(bp.to_editable(), "argv-regex:foo");
206
207    // in-filename
208    let bp2 = BreakPointPattern::from_editable("in-filename:/tmp/test").expect("in-filename");
209    assert_eq!(bp2.pattern(), "/tmp/test");
210    assert_eq!(bp2.to_editable(), "in-filename:/tmp/test");
211
212    // exact-filename
213    let bp3 = BreakPointPattern::from_editable("exact-filename:/bin/sh").expect("exact-filename");
214    assert_eq!(bp3.pattern(), "/bin/sh");
215    assert_eq!(bp3.to_editable(), "exact-filename:/bin/sh");
216
217    // invalid prefix
218    assert_that!(
219      BreakPointPattern::from_editable("unknown:abc"),
220      err(anything())
221    );
222    // missing colon
223    assert_that!(
224      BreakPointPattern::from_editable("no-colon"),
225      err(anything())
226    );
227  }
228
229  #[test]
230  fn test_matches_argv_regex() {
231    // pattern "arg1" should match when argv contains "arg1"
232    let pat = BreakPointPattern::from_editable("argv-regex:arg1").unwrap();
233
234    let argv = [
235      OutputMsg::Ok(ArcStr::from("arg0")),
236      OutputMsg::Ok(ArcStr::from("arg1")),
237      OutputMsg::Ok(ArcStr::from("arg2")),
238    ];
239    let filename = OutputMsg::Ok(ArcStr::from("/bin/prog"));
240
241    assert!(pat.matches(Some(&argv), &filename));
242
243    // If argv is None, ArgvRegex cannot match
244    assert!(!pat.matches(None, &filename));
245  }
246
247  #[test]
248  fn test_matches_in_and_exact_filename() {
249    let in_pat = BreakPointPattern::from_editable("in-filename:log").unwrap();
250    let exact_pat = BreakPointPattern::from_editable("exact-filename:/var/log/app").unwrap();
251
252    let ok_filename = OutputMsg::Ok(ArcStr::from("/var/log/app"));
253    let other_filename = OutputMsg::Ok(ArcStr::from("/tmp/file"));
254    let partial_filename = OutputMsg::PartialOk(ArcStr::from("something"));
255    let err_filename = OutputMsg::Err(crate::event::FriendlyError::InspectError(Errno::EINVAL));
256
257    // in-filename: substring match only when filename is Ok
258    assert!(in_pat.matches(Some(&[]), &ok_filename));
259    assert!(!in_pat.matches(Some(&[]), &other_filename));
260    assert!(!in_pat.matches(Some(&[]), &partial_filename));
261    assert!(!in_pat.matches(Some(&[]), &err_filename));
262
263    // exact-filename: equality only when filename is Ok
264    assert!(exact_pat.matches(Some(&[]), &ok_filename));
265    assert!(!exact_pat.matches(Some(&[]), &other_filename));
266    assert!(!exact_pat.matches(Some(&[]), &partial_filename));
267    assert!(!exact_pat.matches(Some(&[]), &err_filename));
268  }
269
270  #[test]
271  fn test_try_from_breakpoint_valid_and_invalid() {
272    // valid sysenter argv regex
273    let bp = BreakPoint::try_from("sysenter:argv-regex:foo").expect("valid breakpoint");
274    assert_eq!(bp.stop, BreakPointStop::SyscallEnter);
275    match bp.pattern {
276      BreakPointPattern::ArgvRegex(r) => assert_eq!(r.editable, "foo"),
277      _ => panic!("expected ArgvRegex"),
278    }
279
280    // valid sysexit exact filename
281    let bp2 =
282      BreakPoint::try_from("sysexit:exact-filename:/bin/ls").expect("valid exact breakpoint");
283    assert_eq!(bp2.stop, BreakPointStop::SyscallExit);
284    match bp2.pattern {
285      BreakPointPattern::ExactFilename(s) => assert_eq!(s, "/bin/ls"),
286      _ => panic!("expected ExactFilename"),
287    }
288
289    // missing stop
290    assert_that!(BreakPoint::try_from("no-colon-here"), err(anything()));
291
292    // invalid stop
293    assert_that!(
294      BreakPoint::try_from("badstop:argv-regex:foo"),
295      err(anything())
296    );
297
298    // missing pattern kind
299    assert_that!(BreakPoint::try_from("sysenter:badformat"), err(anything()));
300
301    // invalid pattern kind
302    assert_that!(
303      BreakPoint::try_from("sysenter:unknown-kind:xyz"),
304      err(anything())
305    );
306  }
307}