tracexec_core/
breakpoint.rs

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