1use std::collections::BTreeMap;
39use std::collections::BTreeSet;
40use std::fmt;
41use std::str::FromStr;
42
43use reverie_syscalls::Sysno;
44use serde::Deserialize;
45use serde::Serialize;
46
47use crate::pid::DetTid;
48use crate::schedule::SyscallPhase;
49
50pub const HAPPENS_BEFORE_VERSION: u32 = 2;
56
57pub const HAPPENS_BEFORE_VERSION_1: u32 = 1;
60
61pub const DEFAULT_MAX_HOLD_NS: u64 = 10_000_000_000;
64
65#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
73pub struct HappensBeforeSpec {
74 pub version: u32,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub max_hold_ns: Option<u64>,
83
84 #[serde(default)]
87 pub threads: BTreeMap<String, ThreadSpec>,
88
89 #[serde(default)]
92 pub events: BTreeMap<String, EventSpec>,
93
94 #[serde(default)]
96 pub edges: Vec<EdgeSpec>,
97}
98
99#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
103pub struct ThreadSpec {
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub label: Option<String>,
107
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub dettid: Option<i32>,
111
112 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub spawn_ordinal: Option<u32>,
116}
117
118#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
125pub struct EventSpec {
126 pub thread: String,
129
130 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub syscalls: Option<u64>,
134
135 #[serde(default, alias = "rcb", skip_serializing_if = "Option::is_none")]
138 pub rcbs: Option<u64>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub func: Option<String>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub file: Option<String>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub line: Option<u32>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub syscall: Option<String>,
159
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub phase: Option<PhaseSpec>,
163
164 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub fd: Option<i32>,
174
175 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub futex_op: Option<i64>,
181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub from: Option<String>,
189
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub rip: Option<String>,
193
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub mark: Option<String>,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub nth: Option<u64>,
203}
204
205#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)]
207#[serde(rename_all = "lowercase")]
208pub enum PhaseSpec {
209 #[serde(alias = "pre")]
211 Prehook,
212 Polling,
214 #[serde(alias = "post")]
216 Posthook,
217}
218
219impl From<PhaseSpec> for SyscallPhase {
220 fn from(p: PhaseSpec) -> Self {
221 match p {
222 PhaseSpec::Prehook => SyscallPhase::Prehook,
223 PhaseSpec::Polling => SyscallPhase::Polling,
224 PhaseSpec::Posthook => SyscallPhase::Posthook,
225 }
226 }
227}
228
229#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
231pub struct EdgeSpec {
232 pub before: String,
234 pub after: String,
236 #[serde(default)]
238 pub strength: Strength,
239}
240
241#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
243#[serde(rename_all = "lowercase")]
244pub enum Strength {
245 #[default]
248 Hard,
249 Soft,
252}
253
254#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
260pub struct ThreadRef {
261 pub label: String,
263 pub dettid: Option<DetTid>,
265 pub spawn_ordinal: Option<u32>,
267}
268
269#[derive(PartialEq, Eq, Debug, Clone, Default)]
271pub struct CodeLocation {
272 pub function: Option<String>,
274 pub file: Option<String>,
276 pub line: Option<u32>,
278}
279
280impl CodeLocation {
281 pub fn is_empty(&self) -> bool {
283 self.function.is_none() && self.file.is_none() && self.line.is_none()
284 }
285}
286
287impl fmt::Display for CodeLocation {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 match (&self.function, &self.file, self.line) {
290 (Some(func), _, Some(line)) => write!(f, "{}:{}", func, line),
291 (Some(func), _, None) => write!(f, "{}", func),
292 (None, Some(file), Some(line)) => write!(f, "{}:{}", file, line),
293 (None, Some(file), None) => write!(f, "{}", file),
294 (None, None, Some(line)) => write!(f, "line {}", line),
295 (None, None, None) => write!(f, "<unlocated>"),
296 }
297 }
298}
299
300#[derive(PartialEq, Eq, Debug, Clone)]
307pub enum Position {
308 SyscallCount(u64),
310
311 Rcb(u64),
313
314 Syscall {
352 sysno: Sysno,
354 phase: Option<SyscallPhase>,
356 nth: u64,
358 fd: Option<i32>,
360 futex_op: Option<i32>,
363 },
364
365 Rip {
368 addr: Option<u64>,
371 nth: u64,
373 },
374
375 Marker {
377 name: String,
379 nth: u64,
381 },
382}
383
384impl fmt::Display for Position {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 match self {
387 Position::SyscallCount(n) => write!(f, "after {} syscalls", n),
388 Position::Rcb(m) => write!(f, "at RCB {}", m),
389 Position::Syscall {
390 sysno,
391 phase,
392 nth,
393 fd,
394 futex_op,
395 } => {
396 write!(f, "{}", sysno.name())?;
397 if let Some(fd) = fd {
398 write!(f, "(fd={})", fd)?;
399 }
400 if let Some(op) = futex_op {
401 write!(f, "(op={})", op)?;
402 }
403 if let Some(p) = phase {
404 write!(f, "@{:?}", p)?;
405 }
406 write!(f, "#{}", nth)
407 }
408 Position::Rip { addr, nth } => match addr {
409 Some(a) => write!(f, "@{:#x}#{}", a, nth),
410 None => write!(f, "@<unresolved>#{}", nth),
411 },
412 Position::Marker { name, nth } => write!(f, "mark:{}#{}", name, nth),
413 }
414 }
415}
416
417#[derive(PartialEq, Eq, Debug, Clone)]
419pub struct Anchor {
420 pub name: String,
422 pub thread: ThreadRef,
424 pub position: Position,
426 pub location: CodeLocation,
428 pub from: Option<String>,
431}
432
433impl fmt::Display for Anchor {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 write!(f, "{}[{}: {}", self.name, self.thread.label, self.position)?;
436 if let Some(base) = &self.from {
437 write!(f, " after {}", base)?;
438 }
439 if !self.location.is_empty() {
440 write!(f, " ({})", self.location)?;
441 }
442 write!(f, "]")
443 }
444}
445
446impl Anchor {
447 pub fn is_syscall_occurrence(&self) -> bool {
454 matches!(
455 self.position,
456 Position::Syscall {
457 phase: None | Some(SyscallPhase::Prehook) | Some(SyscallPhase::Posthook),
458 ..
459 }
460 )
461 }
462
463 pub fn fires_at_posthook(&self) -> bool {
469 matches!(
470 self.position,
471 Position::Syscall {
472 phase: Some(SyscallPhase::Posthook),
473 ..
474 }
475 )
476 }
477
478 pub fn occurrence_key(&self) -> Option<OccurrenceKey> {
482 match &self.position {
483 Position::Syscall {
484 sysno,
485 fd,
486 futex_op,
487 ..
488 } if self.is_syscall_occurrence() => Some((*sysno as usize, *fd, *futex_op)),
489 _ => None,
490 }
491 }
492
493 pub fn syscall_occurrence_matches(&self, sysno: Sysno, args: [usize; 2]) -> bool {
496 match &self.position {
497 Position::Syscall {
498 sysno: want,
499 fd,
500 futex_op,
501 ..
502 } if self.is_syscall_occurrence() && *want == sysno => {
503 fd.is_none_or(|fd| args[0] as u32 as i32 == fd)
506 && futex_op.is_none_or(|op| args[1] as u32 as i32 == op)
507 }
508 _ => false,
509 }
510 }
511}
512
513pub fn syscall_never_returns_on_success(sysno: Sysno) -> bool {
517 matches!(
518 sysno,
519 Sysno::execve | Sysno::execveat | Sysno::exit | Sysno::exit_group | Sysno::rt_sigreturn
520 )
521}
522
523pub fn syscall_never_reaches_tracer(sysno: Sysno) -> bool {
528 sysno == Sysno::rt_sigreturn
529}
530
531pub type OccurrenceKey = (usize, Option<i32>, Option<i32>);
534
535pub type OccurrenceCounters = BTreeMap<OccurrenceKey, u64>;
538
539#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
545pub struct HbThreadCounters {
546 pub counts: OccurrenceCounters,
548 pub origins: BTreeMap<String, u64>,
551}
552
553pub fn syscall_takes_fd_first(sysno: Sysno) -> bool {
558 match sysno {
559 Sysno::read
560 | Sysno::write
561 | Sysno::readv
562 | Sysno::writev
563 | Sysno::pread64
564 | Sysno::pwrite64
565 | Sysno::preadv
566 | Sysno::pwritev
567 | Sysno::preadv2
568 | Sysno::pwritev2
569 | Sysno::close
570 | Sysno::fsync
571 | Sysno::fdatasync
572 | Sysno::ioctl
573 | Sysno::fcntl
574 | Sysno::lseek
575 | Sysno::fstat
576 | Sysno::ftruncate
577 | Sysno::fallocate
578 | Sysno::flock
579 | Sysno::getdents64
580 | Sysno::sendto
581 | Sysno::recvfrom
582 | Sysno::sendmsg
583 | Sysno::recvmsg
584 | Sysno::sendmmsg
585 | Sysno::recvmmsg
586 | Sysno::accept
587 | Sysno::accept4
588 | Sysno::connect
589 | Sysno::bind
590 | Sysno::listen
591 | Sysno::shutdown
592 | Sysno::epoll_pwait
593 | Sysno::epoll_ctl
594 | Sysno::dup
595 | Sysno::dup3 => true,
596 #[cfg(target_arch = "x86_64")]
599 Sysno::epoll_wait | Sysno::dup2 => true,
600 _ => false,
601 }
602}
603
604#[derive(PartialEq, Eq, Debug, Clone)]
606pub struct HappensBeforeEdge {
607 pub before: String,
609 pub after: String,
611 pub strength: Strength,
613}
614
615#[derive(PartialEq, Eq, Debug, Clone)]
618pub struct HappensBeforeProgram {
619 pub version: u32,
621 pub anchors: BTreeMap<String, Anchor>,
623 pub edges: Vec<HappensBeforeEdge>,
625 pub max_hold_ns: Option<u64>,
629}
630
631impl HappensBeforeProgram {
632 pub fn unresolved_locations(&self) -> impl Iterator<Item = &Anchor> {
636 self.anchors.values().filter(|a| {
637 matches!(a.position, Position::Rip { addr: None, .. }) && !a.location.is_empty()
638 })
639 }
640
641 pub fn may_have_syscall_count_anchor_at(&self, dettid: DetTid, count: u64) -> bool {
653 self.anchors.values().any(|a| {
654 matches!(a.position, Position::SyscallCount(n) if n == count)
655 && a.thread.dettid.is_none_or(|d| d == dettid)
656 })
657 }
658
659 pub fn has_syscall_occurrence_anchors(&self) -> bool {
662 self.anchors.values().any(Anchor::is_syscall_occurrence)
663 }
664
665 pub fn count_syscall_occurrences(
689 &self,
690 counters: &mut HbThreadCounters,
691 dettid: DetTid,
692 sysno: Sysno,
693 args: [usize; 2],
694 entry_count: u64,
695 ) -> Vec<String> {
696 let on_thread = |a: &&Anchor| a.thread.dettid.is_none_or(|d| d == dettid);
697 let keys: BTreeSet<OccurrenceKey> = self
698 .anchors
699 .values()
700 .filter(|a| a.syscall_occurrence_matches(sysno, args))
701 .filter_map(Anchor::occurrence_key)
702 .collect();
703 for key in &keys {
704 *counters.counts.entry(*key).or_insert(0) += 1;
705 }
706 for relative in self.anchors.values().filter(on_thread) {
707 let Some(base) = relative.from.as_ref().and_then(|b| self.anchors.get(b)) else {
708 continue;
709 };
710 if counters.origins.contains_key(&relative.name) {
711 continue;
712 }
713 let reached = match &base.position {
714 Position::SyscallCount(n) => *n == entry_count,
715 Position::Syscall { nth, .. } => {
716 base.syscall_occurrence_matches(sysno, args)
717 && base
718 .occurrence_key()
719 .is_some_and(|key| counters.counts.get(&key) == Some(nth))
720 }
721 _ => false,
722 };
723 if reached {
724 let origin = relative
725 .occurrence_key()
726 .and_then(|key| counters.counts.get(&key).copied())
727 .unwrap_or(0);
728 counters.origins.insert(relative.name.clone(), origin);
729 }
730 }
731 if keys.is_empty() {
732 return Vec::new();
733 }
734 self.anchors
735 .values()
736 .filter(on_thread)
737 .filter(|a| a.syscall_occurrence_matches(sysno, args))
738 .filter(|a| match (&a.position, a.occurrence_key()) {
739 (Position::Syscall { nth, .. }, Some(key)) => {
740 let count = counters.counts.get(&key).copied().unwrap_or(0);
741 match &a.from {
742 None => count == *nth,
743 Some(_) => counters
744 .origins
745 .get(&a.name)
746 .is_some_and(|origin| count - origin == *nth),
747 }
748 }
749 _ => false,
750 })
751 .map(|a| a.name.clone())
752 .collect()
753 }
754
755 pub fn is_version_2(&self) -> bool {
758 self.version == HAPPENS_BEFORE_VERSION
759 }
760
761 pub fn relative_anchors(&self) -> impl Iterator<Item = (&Anchor, &Anchor)> {
763 self.anchors.values().filter_map(|a| {
764 a.from
765 .as_ref()
766 .and_then(|b| self.anchors.get(b))
767 .map(|base| (a, base))
768 })
769 }
770
771 pub fn unenforced_positions(&self) -> impl Iterator<Item = &Anchor> {
776 self.anchors.values().filter(|a| {
777 !matches!(a.position, Position::SyscallCount(_)) && !a.is_syscall_occurrence()
778 })
779 }
780}
781
782#[derive(PartialEq, Eq, Debug, Clone)]
788pub enum HappensBeforeError {
789 UnsupportedVersion(u32),
791 AmbiguousPosition {
793 event: String,
795 found: Vec<String>,
797 },
798 UnknownSyscall {
800 event: String,
802 name: String,
804 },
805 NthZero {
808 event: String,
810 },
811 FdWithoutSyscall {
813 event: String,
815 },
816 FdNotFirstArgument {
819 event: String,
821 syscall: String,
823 },
824 BadRip {
826 event: String,
828 text: String,
830 },
831 UnknownEvent {
833 which: String,
835 name: String,
837 },
838 UnknownThread {
841 event: String,
843 thread: String,
845 },
846 Cycle(Vec<String>),
848 NewFieldInVersion1 {
850 event: Option<String>,
852 field: String,
854 },
855 UnknownField {
857 place: String,
859 field: String,
861 },
862 FutexOpWithoutFutex {
865 event: String,
867 },
868 FutexOpOutOfRange {
870 event: String,
872 value: i64,
874 },
875 FromUnknownEvent {
877 event: String,
879 base: String,
881 },
882 FromOnNonOccurrence {
884 event: String,
886 },
887 FromOtherThread {
891 event: String,
893 base: String,
895 },
896 FromUnenforcedPosition {
899 event: String,
901 base: String,
903 },
904 FromRelativeBase {
906 event: String,
908 base: String,
910 },
911 FromCycle(Vec<String>),
914 PosthookOnNoReturnSyscall {
917 event: String,
919 syscall: String,
921 },
922 DslSyntax {
924 line: usize,
926 message: String,
928 },
929}
930
931impl fmt::Display for HappensBeforeError {
932 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
933 match self {
934 HappensBeforeError::UnsupportedVersion(v) => write!(
935 f,
936 "unsupported happens-before schema version {} (this build understands {} and {})",
937 v, HAPPENS_BEFORE_VERSION_1, HAPPENS_BEFORE_VERSION
938 ),
939 HappensBeforeError::AmbiguousPosition { event, found } => {
940 if found.is_empty() {
941 write!(
942 f,
943 "event '{}' must specify a position: a count (syscalls/rcbs), a syscall, a \
944 rip, a mark, or a code location (func/file/line)",
945 event
946 )
947 } else {
948 write!(
949 f,
950 "event '{}' names conflicting positions {:?}; use at most one explicit \
951 position selector (a code location may accompany it)",
952 event, found
953 )
954 }
955 }
956 HappensBeforeError::UnknownSyscall { event, name } => {
957 write!(f, "event '{}' names unknown syscall '{}'", event, name)
958 }
959 HappensBeforeError::NthZero { event } => write!(
960 f,
961 "event '{}' sets 'nth' to 0; occurrences are counted from 1",
962 event
963 ),
964 HappensBeforeError::FdWithoutSyscall { event } => write!(
965 f,
966 "event '{}' sets 'fd' without 'syscall'; 'fd' restricts a named syscall",
967 event
968 ),
969 HappensBeforeError::FdNotFirstArgument { event, syscall } => write!(
970 f,
971 "event '{}' sets 'fd' on syscall '{}', whose first argument is not a file \
972 descriptor",
973 event, syscall
974 ),
975 HappensBeforeError::BadRip { event, text } => {
976 write!(f, "event '{}' has unparseable rip '{}'", event, text)
977 }
978 HappensBeforeError::UnknownEvent { which, name } => {
979 write!(f, "edge '{}' references unknown event '{}'", which, name)
980 }
981 HappensBeforeError::UnknownThread { event, thread } => write!(
982 f,
983 "event '{}' references unknown thread '{}'",
984 event, thread
985 ),
986 HappensBeforeError::Cycle(names) => {
987 write!(
988 f,
989 "happens-before edges contain a cycle: {}",
990 names.join(" -> ")
991 )
992 }
993 HappensBeforeError::DslSyntax { line, message } => {
994 write!(f, "DSL parse error on line {}: {}", line, message)
995 }
996 HappensBeforeError::NewFieldInVersion1 { event, field } => match event {
997 Some(event) => write!(
998 f,
999 "event '{}' sets '{}', which needs \"version\": 2",
1000 event, field
1001 ),
1002 None => write!(f, "the spec sets '{}', which needs \"version\": 2", field),
1003 },
1004 HappensBeforeError::UnknownField { place, field } => write!(
1005 f,
1006 "{} has unknown field '{}' (version 2 refuses unknown fields)",
1007 place, field
1008 ),
1009 HappensBeforeError::FutexOpWithoutFutex { event } => write!(
1010 f,
1011 "event '{}' sets 'futex_op', which applies only to \"syscall\": \"futex\"",
1012 event
1013 ),
1014 HappensBeforeError::FutexOpOutOfRange { event, value } => write!(
1015 f,
1016 "event '{}' sets 'futex_op' to {}, which does not fit the kernel's int",
1017 event, value
1018 ),
1019 HappensBeforeError::FromUnknownEvent { event, base } => {
1020 write!(f, "event '{}' counts from unknown event '{}'", event, base)
1021 }
1022 HappensBeforeError::FromOnNonOccurrence { event } => write!(
1023 f,
1024 "event '{}' sets 'from', which applies only to a syscall anchor at the prehook",
1025 event
1026 ),
1027 HappensBeforeError::FromOtherThread { event, base } => write!(
1028 f,
1029 "event '{}' counts from '{}', which cannot be shown to be on the same thread; a \
1030 base must name the same dettid, or the same spawn ordinal",
1031 event, base
1032 ),
1033 HappensBeforeError::FromUnenforcedPosition { event, base } => write!(
1034 f,
1035 "event '{}' counts from '{}', which is neither a count anchor nor a syscall anchor \
1036 at the prehook, so it is never reached",
1037 event, base
1038 ),
1039 HappensBeforeError::FromRelativeBase { event, base } => write!(
1040 f,
1041 "event '{}' counts from '{}', which itself uses 'from'; relative chains are not \
1042 supported",
1043 event, base
1044 ),
1045 HappensBeforeError::PosthookOnNoReturnSyscall { event, syscall } => write!(
1046 f,
1047 "event '{}' fires at the posthook of {}, which does not return to its caller when \
1048 it succeeds, so the anchor could only fire on a failed call",
1049 event, syscall
1050 ),
1051 HappensBeforeError::FromCycle(names) => write!(
1052 f,
1053 "relative bases and hard edges together contain a cycle: {}",
1054 names.join(" -> ")
1055 ),
1056 }
1057 }
1058}
1059
1060impl std::error::Error for HappensBeforeError {}
1061
1062impl HappensBeforeSpec {
1067 pub fn from_json(s: &str) -> anyhow::Result<HappensBeforeSpec> {
1077 let value: serde_json::Value = serde_json::from_str(s)?;
1078 if value.get("version").and_then(serde_json::Value::as_u64)
1079 == Some(u64::from(HAPPENS_BEFORE_VERSION))
1080 {
1081 check_known_fields(&value)?;
1082 }
1083 Ok(serde_json::from_str(s)?)
1084 }
1085
1086 pub fn to_json(&self) -> anyhow::Result<String> {
1088 Ok(serde_json::to_string_pretty(self)?)
1089 }
1090
1091 pub fn normalize(&self) -> Result<HappensBeforeProgram, HappensBeforeError> {
1095 if self.version != HAPPENS_BEFORE_VERSION && self.version != HAPPENS_BEFORE_VERSION_1 {
1096 return Err(HappensBeforeError::UnsupportedVersion(self.version));
1097 }
1098 if self.version == HAPPENS_BEFORE_VERSION_1 {
1099 self.refuse_version_2_fields()?;
1100 }
1101
1102 let mut anchors = BTreeMap::new();
1103 for (name, ev) in &self.events {
1104 anchors.insert(name.clone(), self.normalize_event(name, ev)?);
1105 }
1106 check_relative_bases(&anchors)?;
1107
1108 let mut edges = Vec::with_capacity(self.edges.len());
1110 for e in &self.edges {
1111 if !anchors.contains_key(&e.before) {
1112 return Err(HappensBeforeError::UnknownEvent {
1113 which: "before".to_string(),
1114 name: e.before.clone(),
1115 });
1116 }
1117 if !anchors.contains_key(&e.after) {
1118 return Err(HappensBeforeError::UnknownEvent {
1119 which: "after".to_string(),
1120 name: e.after.clone(),
1121 });
1122 }
1123 edges.push(HappensBeforeEdge {
1124 before: e.before.clone(),
1125 after: e.after.clone(),
1126 strength: e.strength,
1127 });
1128 }
1129
1130 detect_cycle(&anchors, &edges)?;
1131 let mut with_activation = edges.clone();
1135 with_activation.extend(
1136 anchors
1137 .values()
1138 .filter_map(|a| a.from.as_ref().map(|base| (base, a)))
1139 .map(|(base, a)| HappensBeforeEdge {
1140 before: base.clone(),
1141 after: a.name.clone(),
1142 strength: Strength::Hard,
1143 }),
1144 );
1145 with_activation.retain(|e| e.strength == Strength::Hard);
1146 if let Err(HappensBeforeError::Cycle(names)) = detect_cycle(&anchors, &with_activation) {
1147 return Err(HappensBeforeError::FromCycle(names));
1148 }
1149
1150 let max_hold_ns = (self.version == HAPPENS_BEFORE_VERSION)
1151 .then(|| self.max_hold_ns.unwrap_or(DEFAULT_MAX_HOLD_NS));
1152 Ok(HappensBeforeProgram {
1153 version: self.version,
1154 anchors,
1155 edges,
1156 max_hold_ns,
1157 })
1158 }
1159
1160 fn refuse_version_2_fields(&self) -> Result<(), HappensBeforeError> {
1162 if self.max_hold_ns.is_some() {
1163 return Err(HappensBeforeError::NewFieldInVersion1 {
1164 event: None,
1165 field: "max_hold_ns".to_owned(),
1166 });
1167 }
1168 for (name, ev) in &self.events {
1169 for (field, set) in [
1170 ("from", ev.from.is_some()),
1171 ("futex_op", ev.futex_op.is_some()),
1172 ] {
1173 if set {
1174 return Err(HappensBeforeError::NewFieldInVersion1 {
1175 event: Some(name.clone()),
1176 field: field.to_owned(),
1177 });
1178 }
1179 }
1180 }
1181 Ok(())
1182 }
1183
1184 fn normalize_event(&self, name: &str, ev: &EventSpec) -> Result<Anchor, HappensBeforeError> {
1186 let thread = self.resolve_thread(name, &ev.thread)?;
1187
1188 let location = CodeLocation {
1191 function: ev.func.clone(),
1192 file: ev.file.clone(),
1193 line: ev.line,
1194 };
1195
1196 let mut found: Vec<&str> = Vec::new();
1203 if ev.syscalls.is_some() {
1204 found.push("syscalls");
1205 }
1206 if ev.rcbs.is_some() {
1207 found.push("rcbs");
1208 }
1209 if ev.syscall.is_some() {
1210 found.push("syscall");
1211 }
1212 if ev.rip.is_some() {
1213 found.push("rip");
1214 }
1215 if ev.mark.is_some() {
1216 found.push("mark");
1217 }
1218 let has_code_location = !location.is_empty();
1219
1220 if found.len() > 1 {
1224 return Err(HappensBeforeError::AmbiguousPosition {
1225 event: name.to_string(),
1226 found: found.iter().map(|s| s.to_string()).collect(),
1227 });
1228 }
1229 if found.is_empty() && !has_code_location {
1230 return Err(HappensBeforeError::AmbiguousPosition {
1231 event: name.to_string(),
1232 found: Vec::new(),
1233 });
1234 }
1235
1236 if ev.fd.is_some() && ev.syscall.is_none() {
1237 return Err(HappensBeforeError::FdWithoutSyscall {
1238 event: name.to_string(),
1239 });
1240 }
1241 if ev.futex_op.is_some() && ev.syscall.is_none() {
1242 return Err(HappensBeforeError::FutexOpWithoutFutex {
1243 event: name.to_string(),
1244 });
1245 }
1246
1247 if ev.nth == Some(0) && ev.syscalls.is_none() && ev.rcbs.is_none() {
1248 return Err(HappensBeforeError::NthZero {
1249 event: name.to_string(),
1250 });
1251 }
1252
1253 let nth = ev.nth.unwrap_or(1);
1254 let position = if let Some(n) = ev.syscalls {
1255 Position::SyscallCount(n)
1256 } else if let Some(m) = ev.rcbs {
1257 Position::Rcb(m)
1258 } else if let Some(sc) = &ev.syscall {
1259 let sysno = Sysno::from_str(sc).map_err(|_| HappensBeforeError::UnknownSyscall {
1260 event: name.to_string(),
1261 name: sc.clone(),
1262 })?;
1263 if ev.fd.is_some() && !syscall_takes_fd_first(sysno) {
1264 return Err(HappensBeforeError::FdNotFirstArgument {
1265 event: name.to_string(),
1266 syscall: sc.clone(),
1267 });
1268 }
1269 let futex_op = match ev.futex_op {
1270 None => None,
1271 Some(_) if sysno != Sysno::futex => {
1272 return Err(HappensBeforeError::FutexOpWithoutFutex {
1273 event: name.to_string(),
1274 });
1275 }
1276 Some(value) => Some(i32::try_from(value).map_err(|_| {
1277 HappensBeforeError::FutexOpOutOfRange {
1278 event: name.to_string(),
1279 value,
1280 }
1281 })?),
1282 };
1283 if ev.phase == Some(PhaseSpec::Posthook) && syscall_never_returns_on_success(sysno) {
1284 return Err(HappensBeforeError::PosthookOnNoReturnSyscall {
1285 event: name.to_string(),
1286 syscall: sc.clone(),
1287 });
1288 }
1289 Position::Syscall {
1290 sysno,
1291 phase: ev.phase.map(Into::into),
1292 nth,
1293 fd: ev.fd,
1294 futex_op,
1295 }
1296 } else if let Some(rip) = &ev.rip {
1297 let addr = parse_rip(rip).ok_or_else(|| HappensBeforeError::BadRip {
1298 event: name.to_string(),
1299 text: rip.clone(),
1300 })?;
1301 Position::Rip {
1302 addr: Some(addr),
1303 nth,
1304 }
1305 } else if let Some(mark) = &ev.mark {
1306 Position::Marker {
1307 name: mark.clone(),
1308 nth,
1309 }
1310 } else {
1311 debug_assert!(has_code_location);
1313 Position::Rip { addr: None, nth }
1314 };
1315
1316 Ok(Anchor {
1317 name: name.to_string(),
1318 thread,
1319 position,
1320 location,
1321 from: ev.from.clone(),
1322 })
1323 }
1324
1325 fn resolve_thread(&self, event: &str, thread: &str) -> Result<ThreadRef, HappensBeforeError> {
1328 if let Some(spec) = self.threads.get(thread) {
1329 Ok(ThreadRef {
1330 label: spec.label.clone().unwrap_or_else(|| thread.to_string()),
1331 dettid: spec.dettid.map(DetTid::from_raw),
1332 spawn_ordinal: spec.spawn_ordinal,
1333 })
1334 } else if let Ok(raw) = thread.parse::<i32>() {
1335 Ok(ThreadRef {
1336 label: thread.to_string(),
1337 dettid: Some(DetTid::from_raw(raw)),
1338 spawn_ordinal: None,
1339 })
1340 } else {
1341 Err(HappensBeforeError::UnknownThread {
1342 event: event.to_string(),
1343 thread: thread.to_string(),
1344 })
1345 }
1346 }
1347}
1348
1349fn check_relative_bases(anchors: &BTreeMap<String, Anchor>) -> Result<(), HappensBeforeError> {
1353 for anchor in anchors.values() {
1354 let Some(base_name) = &anchor.from else {
1355 continue;
1356 };
1357 let event = anchor.name.clone();
1358 let base = base_name.clone();
1359 let Some(base_anchor) = anchors.get(base_name) else {
1360 return Err(HappensBeforeError::FromUnknownEvent { event, base });
1361 };
1362 if !anchor.is_syscall_occurrence() {
1363 return Err(HappensBeforeError::FromOnNonOccurrence { event });
1364 }
1365 if !same_thread(&base_anchor.thread, &anchor.thread) {
1366 return Err(HappensBeforeError::FromOtherThread { event, base });
1367 }
1368 if base_anchor.from.is_some() {
1369 return Err(HappensBeforeError::FromRelativeBase { event, base });
1370 }
1371 if !matches!(base_anchor.position, Position::SyscallCount(_))
1372 && !base_anchor.is_syscall_occurrence()
1373 {
1374 return Err(HappensBeforeError::FromUnenforcedPosition { event, base });
1375 }
1376 }
1377 Ok(())
1378}
1379
1380fn same_thread(a: &ThreadRef, b: &ThreadRef) -> bool {
1385 match (a.dettid, b.dettid) {
1386 (Some(x), Some(y)) => x == y,
1387 (None, None) => a.spawn_ordinal.is_some() && a.spawn_ordinal == b.spawn_ordinal,
1388 _ => false,
1389 }
1390}
1391
1392fn check_known_fields(value: &serde_json::Value) -> Result<(), HappensBeforeError> {
1395 const SPEC: &[&str] = &["version", "max_hold_ns", "threads", "events", "edges"];
1396 const THREAD: &[&str] = &["label", "dettid", "spawn_ordinal"];
1397 const EVENT: &[&str] = &[
1398 "thread", "syscalls", "rcbs", "rcb", "func", "file", "line", "syscall", "phase", "fd",
1399 "futex_op", "from", "rip", "mark", "nth",
1400 ];
1401 const EDGE: &[&str] = &["before", "after", "strength"];
1402 fn check(
1403 object: Option<&serde_json::Map<String, serde_json::Value>>,
1404 known: &[&str],
1405 place: impl Fn() -> String,
1406 ) -> Result<(), HappensBeforeError> {
1407 for field in object.into_iter().flat_map(|o| o.keys()) {
1408 if !known.contains(&field.as_str()) {
1409 return Err(HappensBeforeError::UnknownField {
1410 place: place(),
1411 field: field.clone(),
1412 });
1413 }
1414 }
1415 Ok(())
1416 }
1417 check(value.as_object(), SPEC, || "the spec".to_owned())?;
1418 for (name, thread) in value
1419 .get("threads")
1420 .and_then(serde_json::Value::as_object)
1421 .into_iter()
1422 .flatten()
1423 {
1424 check(thread.as_object(), THREAD, || format!("thread '{name}'"))?;
1425 }
1426 for (name, event) in value
1427 .get("events")
1428 .and_then(serde_json::Value::as_object)
1429 .into_iter()
1430 .flatten()
1431 {
1432 check(event.as_object(), EVENT, || format!("event '{name}'"))?;
1433 }
1434 for (index, edge) in value
1435 .get("edges")
1436 .and_then(serde_json::Value::as_array)
1437 .into_iter()
1438 .flatten()
1439 .enumerate()
1440 {
1441 check(edge.as_object(), EDGE, || format!("edge {index}"))?;
1442 }
1443 Ok(())
1444}
1445
1446fn parse_rip(s: &str) -> Option<u64> {
1448 let s = s.trim();
1449 if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
1450 u64::from_str_radix(hex, 16).ok()
1451 } else {
1452 s.parse::<u64>().ok()
1453 }
1454}
1455
1456fn detect_cycle(
1459 anchors: &BTreeMap<String, Anchor>,
1460 edges: &[HappensBeforeEdge],
1461) -> Result<(), HappensBeforeError> {
1462 let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1464 for name in anchors.keys() {
1465 adj.entry(name.as_str()).or_default();
1466 }
1467 for e in edges {
1468 adj.entry(e.before.as_str())
1469 .or_default()
1470 .push(e.after.as_str());
1471 }
1472
1473 #[derive(Clone, Copy, PartialEq)]
1474 enum Mark {
1475 Visiting,
1476 Done,
1477 }
1478 let mut state: BTreeMap<&str, Mark> = BTreeMap::new();
1479
1480 for root in adj.keys().copied() {
1483 if state.contains_key(root) {
1484 continue;
1485 }
1486 let mut stack: Vec<(&str, usize)> = vec![(root, 0)];
1488 let mut path: Vec<&str> = vec![root];
1489 state.insert(root, Mark::Visiting);
1490
1491 while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
1492 let neighbors = &adj[node];
1493 if *idx < neighbors.len() {
1494 let next = neighbors[*idx];
1495 *idx += 1;
1496 match state.get(next) {
1497 Some(Mark::Visiting) => {
1498 let start = path.iter().position(|&n| n == next).unwrap_or(0);
1500 let mut cycle: Vec<String> =
1501 path[start..].iter().map(|s| s.to_string()).collect();
1502 cycle.push(next.to_string());
1503 return Err(HappensBeforeError::Cycle(cycle));
1504 }
1505 Some(Mark::Done) => {}
1506 None => {
1507 state.insert(next, Mark::Visiting);
1508 path.push(next);
1509 stack.push((next, 0));
1510 }
1511 }
1512 } else {
1513 state.insert(node, Mark::Done);
1514 stack.pop();
1515 path.pop();
1516 }
1517 }
1518 }
1519 Ok(())
1520}
1521
1522impl HappensBeforeSpec {
1542 pub fn from_dsl(input: &str) -> Result<HappensBeforeSpec, HappensBeforeError> {
1545 let mut spec = HappensBeforeSpec {
1547 version: HAPPENS_BEFORE_VERSION_1,
1548 max_hold_ns: None,
1549 threads: BTreeMap::new(),
1550 events: BTreeMap::new(),
1551 edges: Vec::new(),
1552 };
1553 let mut seen_names: BTreeSet<String> = BTreeSet::new();
1554
1555 for (i, raw_line) in input.lines().enumerate() {
1556 let lineno = i + 1;
1557 let line = strip_comment(raw_line).trim();
1558 if line.is_empty() {
1559 continue;
1560 }
1561
1562 let (body, strength) = if let Some(b) = line.strip_suffix("!soft") {
1564 (b.trim(), Strength::Soft)
1565 } else if let Some(b) = line.strip_suffix("!hard") {
1566 (b.trim(), Strength::Hard)
1567 } else {
1568 (line, Strength::Hard)
1569 };
1570
1571 let (lhs, rhs) = body
1572 .split_once('<')
1573 .ok_or_else(|| HappensBeforeError::DslSyntax {
1574 line: lineno,
1575 message: "expected '<' separating two events".to_string(),
1576 })?;
1577
1578 let before = parse_dsl_side(lhs.trim(), lineno, &mut spec, &mut seen_names)?;
1579 let after = parse_dsl_side(rhs.trim(), lineno, &mut spec, &mut seen_names)?;
1580 spec.edges.push(EdgeSpec {
1581 before,
1582 after,
1583 strength,
1584 });
1585 }
1586 Ok(spec)
1587 }
1588}
1589
1590fn strip_comment(line: &str) -> &str {
1593 match line.find("//") {
1594 Some(idx) => &line[..idx],
1595 None => line,
1596 }
1597}
1598
1599fn parse_dsl_side(
1602 token: &str,
1603 lineno: usize,
1604 spec: &mut HappensBeforeSpec,
1605 seen_names: &mut BTreeSet<String>,
1606) -> Result<String, HappensBeforeError> {
1607 let (thread, anchor) = token
1608 .split_once(':')
1609 .ok_or_else(|| HappensBeforeError::DslSyntax {
1610 line: lineno,
1611 message: format!("expected 'thread:anchor' in '{}'", token),
1612 })?;
1613 let thread = thread.trim();
1614 let anchor = anchor.trim();
1615 if thread.is_empty() || anchor.is_empty() {
1616 return Err(HappensBeforeError::DslSyntax {
1617 line: lineno,
1618 message: format!("empty thread or anchor in '{}'", token),
1619 });
1620 }
1621
1622 let (anchor_body, nth) = match anchor.split_once('#') {
1624 Some((a, n)) => {
1625 let parsed = n
1626 .parse::<u64>()
1627 .map_err(|_| HappensBeforeError::DslSyntax {
1628 line: lineno,
1629 message: format!("bad ordinal '#{}'", n),
1630 })?;
1631 (a.trim(), Some(parsed))
1632 }
1633 None => (anchor, None),
1634 };
1635
1636 let mut ev = EventSpec {
1637 thread: thread.to_string(),
1638 nth,
1639 ..Default::default()
1640 };
1641
1642 if let Some(rest) = anchor_body.strip_prefix('@') {
1643 ev.rip = Some(rest.to_string());
1645 } else if let Some(m) = anchor_body.strip_prefix("rcb=") {
1646 ev.rcbs = Some(
1647 m.parse::<u64>()
1648 .map_err(|_| HappensBeforeError::DslSyntax {
1649 line: lineno,
1650 message: format!("bad rcb count '{}'", m),
1651 })?,
1652 );
1653 ev.nth = None;
1654 } else if let Some(n) = anchor_body.strip_prefix("sc=") {
1655 ev.syscalls = Some(
1656 n.parse::<u64>()
1657 .map_err(|_| HappensBeforeError::DslSyntax {
1658 line: lineno,
1659 message: format!("bad syscall count '{}'", n),
1660 })?,
1661 );
1662 ev.nth = None;
1663 } else if let Some((sc, phase)) = anchor_body.split_once('@') {
1664 ev.syscall = Some(sc.to_string());
1666 ev.phase = Some(parse_dsl_phase(phase, lineno)?);
1667 } else if is_syscall_name(anchor_body) {
1668 ev.syscall = Some(anchor_body.to_string());
1670 } else {
1671 ev.func = Some(anchor_body.to_string());
1673 }
1674
1675 let base = sanitize_name(token);
1677 let mut ev_name = base.clone();
1678 let mut suffix = 1;
1679 while seen_names.contains(&ev_name) && spec.events.get(&ev_name) != Some(&ev) {
1680 suffix += 1;
1681 ev_name = format!("{}_{}", base, suffix);
1682 }
1683 seen_names.insert(ev_name.clone());
1684 spec.events.entry(ev_name.clone()).or_insert(ev);
1685
1686 if thread.parse::<i32>().is_err() {
1688 spec.threads
1689 .entry(thread.to_string())
1690 .or_insert(ThreadSpec {
1691 label: Some(thread.to_string()),
1692 dettid: None,
1693 spawn_ordinal: None,
1694 });
1695 }
1696
1697 Ok(ev_name)
1698}
1699
1700fn parse_dsl_phase(phase: &str, lineno: usize) -> Result<PhaseSpec, HappensBeforeError> {
1701 match phase.trim().to_ascii_lowercase().as_str() {
1702 "pre" | "prehook" => Ok(PhaseSpec::Prehook),
1703 "post" | "posthook" => Ok(PhaseSpec::Posthook),
1704 "poll" | "polling" => Ok(PhaseSpec::Polling),
1705 other => Err(HappensBeforeError::DslSyntax {
1706 line: lineno,
1707 message: format!("unknown syscall phase '{}'", other),
1708 }),
1709 }
1710}
1711
1712fn is_syscall_name(s: &str) -> bool {
1714 Sysno::from_str(s).is_ok()
1715}
1716
1717fn sanitize_name(token: &str) -> String {
1719 let mut out = String::with_capacity(token.len());
1720 for ch in token.chars() {
1721 if ch.is_ascii_alphanumeric() || ch == '_' {
1722 out.push(ch);
1723 } else {
1724 out.push('_');
1725 }
1726 }
1727 if out.is_empty() {
1728 out.push('e');
1729 }
1730 out
1731}
1732
1733#[cfg(test)]
1734mod tests {
1735 use super::*;
1736
1737 fn spec_json() -> &'static str {
1738 r#"{
1739 "version": 1,
1740 "threads": { "writer": {"label": "writer"}, "reader": {"label": "reader"} },
1741 "events": {
1742 "X_342": {"thread": "writer", "func": "free_buffer", "line": 120, "nth": 342},
1743 "Y_97": {"thread": "reader", "func": "read_buffer", "nth": 97},
1744 "lockA": {"thread": "writer", "syscall": "futex", "phase": "posthook", "nth": 5},
1745 "storeB": {"thread": "reader", "rip": "0x401f3c", "nth": 1},
1746 "scA": {"thread": "writer", "syscalls": 10},
1747 "rcbB": {"thread": "reader", "rcbs": 123456}
1748 },
1749 "edges": [
1750 {"before": "X_342", "after": "Y_97", "strength": "hard"},
1751 {"before": "lockA", "after": "storeB"},
1752 {"before": "scA", "after": "rcbB", "strength": "soft"}
1753 ]
1754 }"#
1755 }
1756
1757 #[test]
1764 fn may_have_syscall_count_anchor_at_is_exact_per_thread() {
1765 let prog = HappensBeforeSpec::from_json(
1766 r#"{"version": 1,
1767 "threads": {"T7": {"dettid": 7}, "S2": {"spawn_ordinal": 2}},
1768 "events": {"a": {"thread": "T7", "syscalls": 10},
1769 "b": {"thread": "S2", "syscalls": 20},
1770 "c": {"thread": "T7", "rcbs": 30},
1771 "d": {"thread": "T7", "syscall": "futex", "phase": "pre", "nth": 40}},
1772 "edges": [{"before": "a", "after": "b"}]}"#,
1773 )
1774 .unwrap()
1775 .normalize()
1776 .unwrap();
1777 assert!(prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 10));
1778 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(8), 10));
1779 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 9));
1780 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 11));
1781 assert!(prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 20));
1782 assert!(prog.may_have_syscall_count_anchor_at(DetTid::from_raw(8), 20));
1783 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(8), 21));
1784 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 30));
1785 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 0));
1786 assert!(matches!(
1787 prog.anchors["d"].position,
1788 Position::Syscall { nth: 40, .. }
1789 ));
1790 assert!(!prog.may_have_syscall_count_anchor_at(DetTid::from_raw(7), 40));
1791 }
1792
1793 #[test]
1796 fn syscall_fd_anchor_normalizes_and_displays() {
1797 let prog = HappensBeforeSpec::from_json(
1798 r#"{"version": 1,
1799 "events": {"m": {"thread": "7", "syscall": "writev", "fd": 9, "nth": 3}}}"#,
1800 )
1801 .unwrap()
1802 .normalize()
1803 .unwrap();
1804 let anchor = &prog.anchors["m"];
1805 assert_eq!(
1806 anchor.position,
1807 Position::Syscall {
1808 sysno: Sysno::writev,
1809 phase: None,
1810 nth: 3,
1811 fd: Some(9),
1812 futex_op: None,
1813 }
1814 );
1815 assert_eq!(anchor.position.to_string(), "writev(fd=9)#3");
1816 assert!(anchor.is_syscall_occurrence());
1817 assert_eq!(prog.unenforced_positions().count(), 0);
1818 }
1819
1820 #[test]
1824 fn syscall_fd_anchor_is_refused_where_it_cannot_apply() {
1825 let err = HappensBeforeSpec::from_json(
1826 r#"{"version": 1, "events": {"x": {"thread": "7", "syscalls": 5, "fd": 9}}}"#,
1827 )
1828 .unwrap()
1829 .normalize()
1830 .unwrap_err();
1831 assert_eq!(
1832 err,
1833 HappensBeforeError::FdWithoutSyscall {
1834 event: "x".to_string()
1835 }
1836 );
1837 let err = HappensBeforeSpec::from_json(
1838 r#"{"version": 1, "events": {"y": {"thread": "7", "syscall": "getpid", "fd": 9}}}"#,
1839 )
1840 .unwrap()
1841 .normalize()
1842 .unwrap_err();
1843 assert_eq!(
1844 err,
1845 HappensBeforeError::FdNotFirstArgument {
1846 event: "y".to_string(),
1847 syscall: "getpid".to_string()
1848 }
1849 );
1850 assert!(err.to_string().contains("'y'"), "{err}");
1851 let err = HappensBeforeSpec::from_json(
1852 r#"{"version": 1, "events": {"z": {"thread": "7", "syscall": "write", "fd": 1, "nth": 0}}}"#,
1853 )
1854 .unwrap()
1855 .normalize()
1856 .unwrap_err();
1857 assert_eq!(
1858 err,
1859 HappensBeforeError::NthZero {
1860 event: "z".to_string()
1861 }
1862 );
1863 }
1864
1865 #[test]
1873 fn count_syscall_occurrences_is_exact_per_thread() {
1874 let prog = HappensBeforeSpec::from_json(
1875 r#"{"version": 1,
1876 "threads": {"S1": {"spawn_ordinal": 1}},
1877 "events": {"fd9": {"thread": "7", "syscall": "writev", "fd": 9, "nth": 2},
1878 "anyfd": {"thread": "7", "syscall": "writev", "nth": 3},
1879 "spawned": {"thread": "S1", "syscall": "write", "fd": 1},
1880 "polling": {"thread": "7", "syscall": "close", "phase": "polling"}}}"#,
1881 )
1882 .unwrap()
1883 .normalize()
1884 .unwrap();
1885 assert!(prog.has_syscall_occurrence_anchors());
1886 let t7 = DetTid::from_raw(7);
1887 let t8 = DetTid::from_raw(8);
1888 let mut c7 = HbThreadCounters::default();
1889 let count = |c: &mut HbThreadCounters, t, sysno, arg0: usize| {
1890 prog.count_syscall_occurrences(c, t, sysno, [arg0, 0], 1)
1891 };
1892 assert!(count(&mut c7, t7, Sysno::writev, 9).is_empty());
1893 assert!(count(&mut c7, t7, Sysno::writev, 8).is_empty());
1895 assert!(count(&mut c7, t7, Sysno::write, 9).is_empty());
1896 assert_eq!(
1898 count(&mut c7, t7, Sysno::writev, 0x1_0000_0009),
1899 ["anyfd", "fd9"]
1900 );
1901 assert!(count(&mut c7, t7, Sysno::writev, 9).is_empty());
1902 let mut c8 = HbThreadCounters::default();
1904 assert!(count(&mut c8, t8, Sysno::writev, 9).is_empty());
1905 assert!(count(&mut c8, t8, Sysno::writev, 9).is_empty());
1906 assert_eq!(count(&mut c8, t8, Sysno::write, 1), ["spawned"]);
1908 assert!(count(&mut c8, t8, Sysno::write, 2).is_empty());
1909 assert!(count(&mut c7, t7, Sysno::close, 3).is_empty());
1911 assert!(!c7.counts.contains_key(&(Sysno::close as usize, None, None)));
1912 let unenforced: Vec<&str> = prog
1913 .unenforced_positions()
1914 .map(|a| a.name.as_str())
1915 .collect();
1916 assert_eq!(unenforced, ["polling"]);
1917 assert!(!prog.may_have_syscall_count_anchor_at(t7, 2));
1919 let count_only = HappensBeforeSpec::from_json(
1920 r#"{"version": 1, "events": {"a": {"thread": "7", "syscalls": 2}}}"#,
1921 )
1922 .unwrap()
1923 .normalize()
1924 .unwrap();
1925 assert!(!count_only.has_syscall_occurrence_anchors());
1926 }
1927
1928 #[test]
1929 fn parse_and_normalize_rfc_example() {
1930 let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
1931 let prog = spec.normalize().unwrap();
1932 assert_eq!(prog.anchors.len(), 6);
1933 assert_eq!(prog.edges.len(), 3);
1934
1935 assert_eq!(prog.anchors["scA"].position, Position::SyscallCount(10));
1937 assert_eq!(prog.anchors["rcbB"].position, Position::Rcb(123456));
1938
1939 match &prog.anchors["X_342"].position {
1941 Position::Rip { addr: None, nth } => assert_eq!(*nth, 342),
1942 other => panic!("expected unresolved RIP, got {:?}", other),
1943 }
1944 assert_eq!(
1945 prog.anchors["X_342"].location.function.as_deref(),
1946 Some("free_buffer")
1947 );
1948 assert_eq!(prog.anchors["X_342"].location.line, Some(120));
1949
1950 match &prog.anchors["lockA"].position {
1952 Position::Syscall {
1953 sysno,
1954 phase,
1955 nth,
1956 fd: None,
1957 futex_op: None,
1958 } => {
1959 assert_eq!(*sysno, Sysno::futex);
1960 assert_eq!(*phase, Some(SyscallPhase::Posthook));
1961 assert_eq!(*nth, 5);
1962 }
1963 other => panic!("expected syscall, got {:?}", other),
1964 }
1965
1966 assert_eq!(
1968 prog.anchors["storeB"].position,
1969 Position::Rip {
1970 addr: Some(0x401f3c),
1971 nth: 1
1972 }
1973 );
1974
1975 assert_eq!(prog.edges[2].strength, Strength::Soft);
1977 assert_eq!(prog.edges[1].strength, Strength::Hard);
1979
1980 assert_eq!(prog.unresolved_locations().count(), 2);
1982 }
1983
1984 #[test]
1985 fn round_trip_json() {
1986 let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
1987 let json = spec.to_json().unwrap();
1988 let spec2 = HappensBeforeSpec::from_json(&json).unwrap();
1989 assert_eq!(spec, spec2);
1990 }
1991
1992 #[test]
1993 fn rejects_wrong_version() {
1994 let spec = HappensBeforeSpec {
1995 version: 999,
1996 ..HappensBeforeSpec::from_json(spec_json()).unwrap()
1997 };
1998 assert_eq!(
1999 spec.normalize().unwrap_err(),
2000 HappensBeforeError::UnsupportedVersion(999)
2001 );
2002 }
2003
2004 #[test]
2005 fn rejects_ambiguous_position() {
2006 let json = r#"{
2007 "version": 1,
2008 "events": { "bad": {"thread": "1", "syscalls": 3, "rcbs": 5} },
2009 "edges": []
2010 }"#;
2011 let spec = HappensBeforeSpec::from_json(json).unwrap();
2012 match spec.normalize().unwrap_err() {
2013 HappensBeforeError::AmbiguousPosition { event, found } => {
2014 assert_eq!(event, "bad");
2015 assert_eq!(found.len(), 2);
2016 }
2017 other => panic!("expected AmbiguousPosition, got {:?}", other),
2018 }
2019 }
2020
2021 #[test]
2022 fn rejects_no_position() {
2023 let json = r#"{
2024 "version": 1,
2025 "events": { "bad": {"thread": "1"} },
2026 "edges": []
2027 }"#;
2028 let spec = HappensBeforeSpec::from_json(json).unwrap();
2029 assert!(matches!(
2030 spec.normalize().unwrap_err(),
2031 HappensBeforeError::AmbiguousPosition { .. }
2032 ));
2033 }
2034
2035 #[test]
2036 fn code_location_accompanies_count() {
2037 let json = r#"{
2041 "version": 1,
2042 "events": {
2043 "w": {"thread": "1", "func": "free_buffer", "line": 342, "syscalls": 7},
2044 "r": {"thread": "1", "func": "read_buffer", "rcbs": 900}
2045 },
2046 "edges": [ {"before": "w", "after": "r"} ]
2047 }"#;
2048 let prog = HappensBeforeSpec::from_json(json)
2049 .unwrap()
2050 .normalize()
2051 .unwrap();
2052
2053 assert_eq!(prog.anchors["w"].position, Position::SyscallCount(7));
2055 assert_eq!(
2056 prog.anchors["w"].location.function.as_deref(),
2057 Some("free_buffer")
2058 );
2059 assert_eq!(prog.anchors["w"].location.line, Some(342));
2060
2061 assert_eq!(prog.anchors["r"].position, Position::Rcb(900));
2062 assert_eq!(
2063 prog.anchors["r"].location.function.as_deref(),
2064 Some("read_buffer")
2065 );
2066
2067 assert_eq!(prog.unresolved_locations().count(), 0);
2069 }
2070
2071 #[test]
2072 fn raw_dettid_thread() {
2073 let json = r#"{
2074 "version": 1,
2075 "events": { "e": {"thread": "42", "rcbs": 7} },
2076 "edges": []
2077 }"#;
2078 let prog = HappensBeforeSpec::from_json(json)
2079 .unwrap()
2080 .normalize()
2081 .unwrap();
2082 assert_eq!(prog.anchors["e"].thread.dettid, Some(DetTid::from_raw(42)));
2083 }
2084
2085 #[test]
2086 fn rejects_unknown_thread() {
2087 let json = r#"{
2088 "version": 1,
2089 "events": { "e": {"thread": "ghost", "rcbs": 7} },
2090 "edges": []
2091 }"#;
2092 let spec = HappensBeforeSpec::from_json(json).unwrap();
2093 assert!(matches!(
2094 spec.normalize().unwrap_err(),
2095 HappensBeforeError::UnknownThread { .. }
2096 ));
2097 }
2098
2099 #[test]
2100 fn rejects_unknown_event_in_edge() {
2101 let json = r#"{
2102 "version": 1,
2103 "events": { "a": {"thread": "1", "rcbs": 7} },
2104 "edges": [ {"before": "a", "after": "missing"} ]
2105 }"#;
2106 let spec = HappensBeforeSpec::from_json(json).unwrap();
2107 assert!(matches!(
2108 spec.normalize().unwrap_err(),
2109 HappensBeforeError::UnknownEvent { .. }
2110 ));
2111 }
2112
2113 #[test]
2114 fn rejects_unknown_syscall() {
2115 let json = r#"{
2116 "version": 1,
2117 "events": { "a": {"thread": "1", "syscall": "not_a_syscall"} },
2118 "edges": []
2119 }"#;
2120 let spec = HappensBeforeSpec::from_json(json).unwrap();
2121 assert!(matches!(
2122 spec.normalize().unwrap_err(),
2123 HappensBeforeError::UnknownSyscall { .. }
2124 ));
2125 }
2126
2127 #[test]
2128 fn detects_cycle() {
2129 let json = r#"{
2130 "version": 1,
2131 "events": {
2132 "a": {"thread": "1", "rcbs": 1},
2133 "b": {"thread": "1", "rcbs": 2},
2134 "c": {"thread": "1", "rcbs": 3}
2135 },
2136 "edges": [
2137 {"before": "a", "after": "b"},
2138 {"before": "b", "after": "c"},
2139 {"before": "c", "after": "a"}
2140 ]
2141 }"#;
2142 let spec = HappensBeforeSpec::from_json(json).unwrap();
2143 match spec.normalize().unwrap_err() {
2144 HappensBeforeError::Cycle(path) => {
2145 assert_eq!(path.first(), path.last());
2147 assert!(path.len() >= 4);
2148 }
2149 other => panic!("expected Cycle, got {:?}", other),
2150 }
2151 }
2152
2153 #[test]
2154 fn accepts_dag() {
2155 let json = r#"{
2156 "version": 1,
2157 "events": {
2158 "a": {"thread": "1", "rcbs": 1},
2159 "b": {"thread": "1", "rcbs": 2},
2160 "c": {"thread": "1", "rcbs": 3}
2161 },
2162 "edges": [
2163 {"before": "a", "after": "c"},
2164 {"before": "b", "after": "c"}
2165 ]
2166 }"#;
2167 let spec = HappensBeforeSpec::from_json(json).unwrap();
2168 assert!(spec.normalize().is_ok());
2169 }
2170
2171 #[test]
2172 fn dsl_desugars() {
2173 let dsl = "\
2174 // btrfs race: erase-by-key must precede the re-insert
2175 writer:free_buffer#342 < reader:read_buffer#97
2176 writer:futex@post#5 < reader:@0x401f3c#1
2177 A:rcb=123456 < B:sc=97 !soft
2178 ";
2179 let spec = HappensBeforeSpec::from_dsl(dsl).unwrap();
2180 let prog = spec.normalize().unwrap();
2181 assert_eq!(prog.edges.len(), 3);
2182 assert_eq!(prog.anchors.len(), 6);
2183
2184 let soft = &prog.edges[2];
2186 assert_eq!(soft.strength, Strength::Soft);
2187 assert_eq!(prog.anchors[&soft.before].position, Position::Rcb(123456));
2188 assert_eq!(
2189 prog.anchors[&soft.after].position,
2190 Position::SyscallCount(97)
2191 );
2192
2193 let futex = prog
2195 .anchors
2196 .values()
2197 .find(
2198 |a| matches!(a.position, Position::Syscall { sysno, .. } if sysno == Sysno::futex),
2199 )
2200 .unwrap();
2201 match &futex.position {
2202 Position::Syscall { phase, nth, .. } => {
2203 assert_eq!(*phase, Some(SyscallPhase::Posthook));
2204 assert_eq!(*nth, 5);
2205 }
2206 _ => unreachable!(),
2207 }
2208 }
2209
2210 #[test]
2211 fn dsl_rejects_missing_arrow() {
2212 let err = HappensBeforeSpec::from_dsl("writer:foo reader:bar").unwrap_err();
2213 assert!(matches!(err, HappensBeforeError::DslSyntax { line: 1, .. }));
2214 }
2215
2216 #[test]
2217 fn parse_rip_forms() {
2218 assert_eq!(parse_rip("0x401f3c"), Some(0x401f3c));
2219 assert_eq!(parse_rip("4201276"), Some(4201276));
2220 assert_eq!(parse_rip("nonsense"), None);
2221 }
2222
2223 fn normalize_json(json: &str) -> Result<HappensBeforeProgram, HappensBeforeError> {
2224 HappensBeforeSpec::from_json(json).unwrap().normalize()
2225 }
2226
2227 #[test]
2232 fn version_2_fields_need_version_2() {
2233 let v1 = normalize_json(
2234 r#"{"version": 1, "events": {"w": {"thread": "7", "syscall": "write", "fd": 1}}}"#,
2235 )
2236 .unwrap();
2237 assert_eq!((v1.version, v1.max_hold_ns), (1, None));
2238 assert!(!v1.is_version_2());
2239 let v2 = normalize_json(
2240 r#"{"version": 2, "events": {"w": {"thread": "7", "syscall": "write", "fd": 1}}}"#,
2241 )
2242 .unwrap();
2243 assert_eq!(v2.max_hold_ns, Some(DEFAULT_MAX_HOLD_NS));
2244 assert!(v2.is_version_2());
2245 let budget = normalize_json(
2246 r#"{"version": 2, "max_hold_ns": 5000,
2247 "events": {"w": {"thread": "7", "syscall": "write", "fd": 1}}}"#,
2248 )
2249 .unwrap();
2250 assert_eq!(budget.max_hold_ns, Some(5000));
2251
2252 for (json, event, field) in [
2253 (
2254 r#"{"version": 1, "events": {
2255 "a": {"thread": "7", "syscall": "write", "fd": 1},
2256 "r": {"thread": "7", "syscall": "getppid", "from": "a"}}}"#,
2257 Some("r"),
2258 "from",
2259 ),
2260 (
2261 r#"{"version": 1, "events": {
2262 "f": {"thread": "7", "syscall": "futex", "futex_op": 393}}}"#,
2263 Some("f"),
2264 "futex_op",
2265 ),
2266 (
2267 r#"{"version": 1, "max_hold_ns": 5,
2268 "events": {"w": {"thread": "7", "syscall": "write"}}}"#,
2269 None,
2270 "max_hold_ns",
2271 ),
2272 ] {
2273 assert_eq!(
2274 normalize_json(json).unwrap_err(),
2275 HappensBeforeError::NewFieldInVersion1 {
2276 event: event.map(str::to_owned),
2277 field: field.to_owned(),
2278 }
2279 );
2280 }
2281 assert_eq!(
2282 normalize_json(r#"{"version": 3, "events": {}}"#).unwrap_err(),
2283 HappensBeforeError::UnsupportedVersion(3)
2284 );
2285 let dsl = HappensBeforeSpec::from_dsl("A:sc=3 < B:sc=4").unwrap();
2286 assert_eq!(dsl.version, HAPPENS_BEFORE_VERSION_1);
2287 }
2288
2289 #[test]
2293 fn version_2_refuses_unknown_fields() {
2294 for (json, place, field) in [
2295 (r#"{"version": 2, "evnets": {}}"#, "the spec", "evnets"),
2296 (
2297 r#"{"version": 2, "events": {"w": {"thread": "7", "syscal": "write"}}}"#,
2298 "event 'w'",
2299 "syscal",
2300 ),
2301 (
2302 r#"{"version": 2, "threads": {"t": {"dettid": 7, "tid": 7}}}"#,
2303 "thread 't'",
2304 "tid",
2305 ),
2306 (
2307 r#"{"version": 2, "events": {}, "edges": [{"before": "a", "after": "b", "kind": 1}]}"#,
2308 "edge 0",
2309 "kind",
2310 ),
2311 ] {
2312 let error = HappensBeforeSpec::from_json(json).unwrap_err();
2313 assert_eq!(
2314 error.downcast_ref::<HappensBeforeError>(),
2315 Some(&HappensBeforeError::UnknownField {
2316 place: place.to_owned(),
2317 field: field.to_owned(),
2318 }),
2319 "{json}"
2320 );
2321 }
2322 normalize_json(r#"{"version": 2, "events": {"r": {"thread": "7", "rcb": 5}}}"#).unwrap();
2324 normalize_json(r#"{"version": 1, "events": {}, "evnets": {}}"#).unwrap();
2326 }
2327
2328 #[test]
2332 fn a_duplicated_field_is_refused() {
2333 for json in [
2334 r#"{"version": 1, "events": {"w": {"thread": "7", "syscall": "write", "nth": 1, "nth": 2}}}"#,
2335 r#"{"version": 2, "events": {"w": {"thread": "7", "syscall": "write", "nth": 1, "nth": 2}}}"#,
2336 r#"{"version": 2, "max_hold_ns": 5, "max_hold_ns": 6, "events": {}}"#,
2337 ] {
2338 let error = HappensBeforeSpec::from_json(json).unwrap_err().to_string();
2339 assert!(error.contains("duplicate field"), "{json}: {error}");
2340 }
2341 }
2342
2343 #[test]
2348 fn a_posthook_anchor_is_counted_at_entry_and_refused_on_no_return_syscalls() {
2349 let prog = normalize_json(
2350 r#"{"version": 1, "events": {
2351 "w": {"thread": "7", "syscall": "write", "fd": 1, "phase": "posthook", "nth": 2}}}"#,
2352 )
2353 .unwrap();
2354 let w = &prog.anchors["w"];
2355 assert!(w.is_syscall_occurrence() && w.fires_at_posthook());
2356 assert_eq!(prog.unenforced_positions().count(), 0);
2357 let t7 = DetTid::from_raw(7);
2358 let mut c = HbThreadCounters::default();
2359 assert!(
2360 prog.count_syscall_occurrences(&mut c, t7, Sysno::write, [1, 0], 1)
2361 .is_empty()
2362 );
2363 assert_eq!(
2364 prog.count_syscall_occurrences(&mut c, t7, Sysno::write, [1, 0], 2),
2365 ["w"]
2366 );
2367 for syscall in ["execve", "execveat", "exit", "exit_group", "rt_sigreturn"] {
2368 assert_eq!(
2369 normalize_json(&format!(
2370 r#"{{"version": 1, "events": {{"x": {{"thread": "7", "syscall": "{syscall}", "phase": "posthook"}}}}}}"#
2371 ))
2372 .unwrap_err(),
2373 HappensBeforeError::PosthookOnNoReturnSyscall {
2374 event: "x".to_owned(),
2375 syscall: syscall.to_owned()
2376 }
2377 );
2378 }
2379 normalize_json(r#"{"version": 1, "events": {"x": {"thread": "7", "syscall": "execve"}}}"#)
2381 .unwrap();
2382 }
2383
2384 #[test]
2388 fn futex_op_matches_the_low_32_bits_of_the_operation() {
2389 let prog = normalize_json(
2390 r#"{"version": 2, "events": {
2391 "halt": {"thread": "7", "syscall": "futex", "futex_op": 393, "nth": 2},
2392 "wait": {"thread": "7", "syscall": "futex", "futex_op": 0}}}"#,
2393 )
2394 .unwrap();
2395 assert_eq!(prog.anchors["halt"].position.to_string(), "futex(op=393)#2");
2396 let t7 = DetTid::from_raw(7);
2397 let mut c = HbThreadCounters::default();
2398 let mut entry = 0;
2399 let mut call = |c: &mut HbThreadCounters, op: usize| {
2400 entry += 1;
2401 prog.count_syscall_occurrences(c, t7, Sysno::futex, [0x1000, op], entry)
2402 };
2403 assert_eq!(call(&mut c, 393), Vec::<String>::new());
2404 assert_eq!(call(&mut c, 0), ["wait"]);
2406 assert_eq!(call(&mut c, 0xdead_0000_0000_0189), ["halt"]);
2408 assert_eq!(c.counts[&(Sysno::futex as usize, None, Some(393))], 2);
2409 assert_eq!(c.counts[&(Sysno::futex as usize, None, Some(0))], 1);
2410
2411 assert_eq!(
2412 normalize_json(
2413 r#"{"version": 2, "events": {"w": {"thread": "7", "syscall": "write", "futex_op": 1}}}"#,
2414 )
2415 .unwrap_err(),
2416 HappensBeforeError::FutexOpWithoutFutex {
2417 event: "w".to_owned()
2418 }
2419 );
2420 assert_eq!(
2421 normalize_json(
2422 r#"{"version": 2, "events": {"w": {"thread": "7", "futex_op": 1, "syscalls": 3}}}"#
2423 )
2424 .unwrap_err(),
2425 HappensBeforeError::FutexOpWithoutFutex {
2426 event: "w".to_owned()
2427 }
2428 );
2429 assert_eq!(
2430 normalize_json(
2431 r#"{"version": 2, "events": {"f": {"thread": "7", "syscall": "futex", "futex_op": 2147483648}}}"#,
2432 )
2433 .unwrap_err(),
2434 HappensBeforeError::FutexOpOutOfRange {
2435 event: "f".to_owned(),
2436 value: 2147483648
2437 }
2438 );
2439 }
2440
2441 #[test]
2443 fn from_refusals_name_the_event() {
2444 let refused = |events: &str, edges: &str| {
2445 normalize_json(&format!(
2446 r#"{{"version": 2, "events": {{{events}}}, "edges": [{edges}]}}"#
2447 ))
2448 .unwrap_err()
2449 };
2450 let names = |e: &str, b: &str| (e.to_owned(), b.to_owned());
2451 let (event, base) = names("r", "nope");
2452 assert_eq!(
2453 refused(
2454 r#""r": {"thread": "7", "syscall": "getppid", "from": "nope"}"#,
2455 ""
2456 ),
2457 HappensBeforeError::FromUnknownEvent { event, base }
2458 );
2459 let (event, base) = names("r", "a");
2460 assert_eq!(
2461 refused(
2462 r#""a": {"thread": "8", "syscall": "write", "fd": 1},
2463 "r": {"thread": "7", "syscall": "getppid", "from": "a"}"#,
2464 ""
2465 ),
2466 HappensBeforeError::FromOtherThread { event, base }
2467 );
2468 let (event, base) = names("r", "a");
2469 assert_eq!(
2470 refused(
2471 r#""a": {"thread": "7", "rcbs": 100},
2472 "r": {"thread": "7", "syscall": "getppid", "from": "a"}"#,
2473 ""
2474 ),
2475 HappensBeforeError::FromUnenforcedPosition { event, base }
2476 );
2477 let (event, base) = names("r", "a");
2478 assert_eq!(
2479 refused(
2480 r#""a": {"thread": "7", "syscall": "write", "phase": "polling"},
2481 "r": {"thread": "7", "syscall": "getppid", "from": "a"}"#,
2482 ""
2483 ),
2484 HappensBeforeError::FromUnenforcedPosition { event, base }
2485 );
2486 let (event, base) = names("r2", "r1");
2487 assert_eq!(
2488 refused(
2489 r#""a": {"thread": "7", "syscalls": 3},
2490 "r1": {"thread": "7", "syscall": "getppid", "from": "a"},
2491 "r2": {"thread": "7", "syscall": "getppid", "from": "r1"}"#,
2492 ""
2493 ),
2494 HappensBeforeError::FromRelativeBase { event, base }
2495 );
2496 assert_eq!(
2497 refused(
2498 r#""a": {"thread": "7", "syscalls": 3},
2499 "r": {"thread": "7", "syscalls": 9, "from": "a"}"#,
2500 ""
2501 ),
2502 HappensBeforeError::FromOnNonOccurrence {
2503 event: "r".to_owned()
2504 }
2505 );
2506 assert_eq!(
2509 refused(
2510 r#""a": {"thread": "7", "syscalls": 3},
2511 "r": {"thread": "7", "syscall": "getppid", "from": "a"}"#,
2512 r#"{"before": "r", "after": "a"}"#
2513 ),
2514 HappensBeforeError::FromCycle(vec!["a".to_owned(), "r".to_owned(), "a".to_owned()])
2515 );
2516 normalize_json(
2519 r#"{"version": 2, "threads": {"main": {"dettid": 3}, "s1": {"spawn_ordinal": 1}},
2520 "events": {"a": {"thread": "main", "syscall": "write", "fd": 1},
2521 "r": {"thread": "3", "syscall": "getppid", "from": "a"},
2522 "b": {"thread": "s1", "syscalls": 4},
2523 "q": {"thread": "s1", "syscall": "getppid", "from": "b"}}}"#,
2524 )
2525 .unwrap();
2526 let (event, base) = names("r", "a");
2527 assert_eq!(
2528 normalize_json(
2529 r#"{"version": 2, "threads": {"s1": {"spawn_ordinal": 1}},
2530 "events": {"a": {"thread": "7", "syscall": "write", "fd": 1},
2531 "r": {"thread": "s1", "syscall": "getppid", "from": "a"}}}"#,
2532 )
2533 .unwrap_err(),
2534 HappensBeforeError::FromOtherThread { event, base }
2535 );
2536 normalize_json(
2538 r#"{"version": 2, "events": {
2539 "a": {"thread": "7", "syscalls": 3},
2540 "r": {"thread": "7", "syscall": "getppid", "from": "a"}},
2541 "edges": [{"before": "r", "after": "a", "strength": "soft"}]}"#,
2542 )
2543 .unwrap();
2544 }
2545
2546 #[test]
2552 fn relative_anchors_count_strictly_after_their_base() {
2553 let prog = normalize_json(
2554 r#"{"version": 2, "events": {
2555 "m2": {"thread": "7", "syscall": "write", "fd": 1, "nth": 2},
2556 "abs3": {"thread": "7", "syscall": "getppid", "nth": 3},
2557 "after_m2": {"thread": "7", "syscall": "getppid", "from": "m2", "nth": 1},
2558 "c4": {"thread": "7", "syscalls": 4},
2559 "after_c4": {"thread": "7", "syscall": "getppid", "from": "c4", "nth": 2},
2560 "gp1": {"thread": "7", "syscall": "getppid", "nth": 1},
2561 "after_gp1": {"thread": "7", "syscall": "getppid", "from": "gp1", "nth": 1}}}"#,
2562 )
2563 .unwrap();
2564 assert_eq!(
2565 prog.anchors["after_m2"].to_string(),
2566 "after_m2[7: getppid#1 after m2]"
2567 );
2568 let relative: Vec<(&str, &str)> = prog
2569 .relative_anchors()
2570 .map(|(r, b)| (r.name.as_str(), b.name.as_str()))
2571 .collect();
2572 assert_eq!(
2573 relative,
2574 [("after_c4", "c4"), ("after_gp1", "gp1"), ("after_m2", "m2")]
2575 );
2576 let t7 = DetTid::from_raw(7);
2577 let mut c = HbThreadCounters::default();
2578 let mut entry = 0;
2579 let mut call = |c: &mut HbThreadCounters, sysno: Sysno, arg0: usize| {
2580 entry += 1;
2581 prog.count_syscall_occurrences(c, t7, sysno, [arg0, 0], entry)
2582 };
2583 assert_eq!(call(&mut c, Sysno::getppid, 0), ["gp1"]);
2586 assert_eq!(c.origins["after_gp1"], 1);
2587 assert!(call(&mut c, Sysno::write, 1).is_empty());
2589 assert_eq!(call(&mut c, Sysno::getppid, 0), ["after_gp1"]);
2591 assert!(call(&mut c, Sysno::getpid, 0).is_empty());
2594 assert_eq!(c.origins["after_c4"], 2);
2595 assert_eq!(call(&mut c, Sysno::write, 1), ["m2"]);
2597 assert_eq!(c.origins["after_m2"], 2);
2598 assert_eq!(call(&mut c, Sysno::getppid, 0), ["abs3", "after_m2"]);
2601 assert_eq!(call(&mut c, Sysno::getppid, 0), ["after_c4"]);
2603 assert!(call(&mut c, Sysno::getppid, 0).is_empty());
2604 assert_eq!(c.counts[&(Sysno::getppid as usize, None, None)], 5);
2605
2606 let mut c8 = HbThreadCounters::default();
2609 assert!(
2610 prog.count_syscall_occurrences(&mut c8, DetTid::from_raw(8), Sysno::getppid, [0, 0], 1)
2611 .is_empty()
2612 );
2613 assert!(c8.origins.is_empty());
2614 }
2615
2616 #[test]
2622 fn a_posthook_anchor_shares_its_counter_and_starts_windows_at_its_entry() {
2623 let prog = normalize_json(
2624 r#"{"version": 2, "events": {
2625 "pre2": {"thread": "7", "syscall": "write", "fd": 1, "nth": 2},
2626 "post2": {"thread": "7", "syscall": "write", "fd": 1, "nth": 2,
2627 "phase": "posthook"},
2628 "post3": {"thread": "7", "syscall": "write", "fd": 1, "nth": 3,
2629 "phase": "posthook"},
2630 "base": {"thread": "7", "syscall": "getpid", "phase": "posthook"},
2631 "after_base": {"thread": "7", "syscall": "write", "fd": 1, "from": "base",
2632 "nth": 1}}}"#,
2633 )
2634 .unwrap();
2635 let t7 = DetTid::from_raw(7);
2636 let mut c = HbThreadCounters::default();
2637 let mut entry = 0;
2638 let mut call = |c: &mut HbThreadCounters, sysno: Sysno, arg0: usize| {
2639 entry += 1;
2640 prog.count_syscall_occurrences(c, t7, sysno, [arg0, 0], entry)
2641 };
2642 assert!(call(&mut c, Sysno::write, 1).is_empty());
2643 assert_eq!(call(&mut c, Sysno::write, 1), ["post2", "pre2"]);
2644 assert_eq!(call(&mut c, Sysno::getpid, 0), ["base"]);
2647 assert_eq!(c.origins["after_base"], 2);
2648 assert_eq!(call(&mut c, Sysno::write, 1), ["after_base", "post3"]);
2650 assert_eq!(c.counts[&(Sysno::write as usize, Some(1), None)], 3);
2651 }
2652}