Skip to main content

detcore_model/
happens_before.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Happens-before edges: a sparse, authored partial order over dynamic events.
10//!
11//! Where `--replay-schedule-from` replays a *complete* total order captured from
12//! a prior run, a happens-before specification pins down only the *few* events
13//! that matter for a race and lets the deterministic scheduler fill in the rest.
14//! An agent (or human) that already knows a target race can therefore construct
15//! it deterministically instead of blind seed-search.
16//!
17//! # Model
18//!
19//! An [`Anchor`] names a precise, deterministic per-thread stop point. Following
20//! the owner's refinement of RFC #1146, the *primary* addressing is a
21//! [`Position`] — "after N syscalls" or "after M retired conditional branches
22//! (RCBs)" on a specific thread — optionally decorated with a [`CodeLocation`]
23//! (function and/or source line, resolved from debug info) for readability. The
24//! RFC's richer addressing modes (Nth occurrence of a named syscall, RIP hit, or
25//! function entry) remain expressible but are deliberately *not* the lead
26//! addressing scheme.
27//!
28//! A [`HappensBeforeEdge`] states that one anchor must be observed before another
29//! thread is allowed to proceed past its anchor. A [`HappensBeforeSpec`] is the
30//! whole authored partial order: a table of named threads, a table of named
31//! events (anchors), and the edge list connecting them.
32//!
33//! This module owns the *model* only: parsing (JSON and a terse DSL),
34//! normalization, and static validation (name resolution, exactly-one-position,
35//! and cycle detection). Resolving a [`CodeLocation`] to a concrete address via
36//! debug info, and enforcing edges in the scheduler, live in higher layers.
37
38use 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
50/// The newest schema version understood by this build. Version 2 adds
51/// relative anchors (`from`), the `futex_op` filter and the hold budget
52/// (`max_hold_ns`); version 1 files load unchanged, with their old semantics.
53/// A build that understands only version 1 refuses a version-2 file instead of
54/// silently dropping the fields it does not know.
55pub const HAPPENS_BEFORE_VERSION: u32 = 2;
56
57/// The original schema version, still accepted. It may not use any field that
58/// version 2 added ([`HappensBeforeError::NewFieldInVersion1`]).
59pub const HAPPENS_BEFORE_VERSION_1: u32 = 1;
60
61/// The hold budget of a version-2 spec that sets no `max_hold_ns`: 10 s of
62/// virtual time.
63pub const DEFAULT_MAX_HOLD_NS: u64 = 10_000_000_000;
64
65// ================================================================================
66// Declarative on-disk / on-wire format (serde)
67// ================================================================================
68
69/// The declarative happens-before specification, as read from a JSON file or
70/// desugared from the terse DSL. This mirrors the reviewed RFC #1146 file format
71/// verbatim so an authored file round-trips.
72#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
73pub struct HappensBeforeSpec {
74    /// Schema version: [`HAPPENS_BEFORE_VERSION_1`] or
75    /// [`HAPPENS_BEFORE_VERSION`].
76    pub version: u32,
77
78    /// Version 2 only: how long a thread may stay held at a gate, in committed
79    /// virtual nanoseconds, before the run is refused
80    /// (`HERMIT_HB_HOLD_BUDGET_EXCEEDED`). Defaults to [`DEFAULT_MAX_HOLD_NS`].
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub max_hold_ns: Option<u64>,
83
84    /// Symbolic thread labels mapped to a resolution rule, so authors need not
85    /// hard-code raw `DetTid`s.
86    #[serde(default)]
87    pub threads: BTreeMap<String, ThreadSpec>,
88
89    /// Named events (anchors). Naming events separately from edges lets one event
90    /// participate in several edges and keeps the edge list readable.
91    #[serde(default)]
92    pub events: BTreeMap<String, EventSpec>,
93
94    /// The partial order itself.
95    #[serde(default)]
96    pub edges: Vec<EdgeSpec>,
97}
98
99/// How a symbolic thread label resolves to a concrete `DetTid`.
100///
101/// Exactly one resolution rule should be provided; if `dettid` is present it wins.
102#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
103pub struct ThreadSpec {
104    /// A human-facing label (defaults to the map key).
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub label: Option<String>,
107
108    /// An explicit deterministic thread id, when the author knows it.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub dettid: Option<i32>,
111
112    /// Resolve to the thread created by the Nth `clone`/`fork`, 1-based. Thread
113    /// creation is deterministic under sequentialization, so this is stable.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub spawn_ordinal: Option<u32>,
116}
117
118/// A single named event (anchor) in the declarative format.
119///
120/// The addressing fields are flat and optional to match the RFC JSON; exactly one
121/// *position* selector must be set (see [`HappensBeforeSpec::normalize`]). The
122/// two owner-preferred primaries are [`syscalls`](Self::syscalls) ("after N
123/// syscalls") and [`rcbs`](Self::rcbs) ("after M RBCs").
124#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
125pub struct EventSpec {
126    /// The thread this event is on: a key into [`HappensBeforeSpec::threads`], or
127    /// a raw integer `DetTid`.
128    pub thread: String,
129
130    // ---- primary positions (owner's refinement) ----
131    /// After the thread has executed this many syscalls in total (any syscall).
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub syscalls: Option<u64>,
134
135    /// After the thread has retired this many conditional branches (its RCB
136    /// clock reaches this absolute value). Accepted as `rcbs` or `rcb`.
137    #[serde(default, alias = "rcb", skip_serializing_if = "Option::is_none")]
138    pub rcbs: Option<u64>,
139
140    // ---- code location (readability; resolves to a RIP via debug info) ----
141    /// A function name; with `line` this is "function+line", the preferred
142    /// human-legible location.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub func: Option<String>,
145
146    /// A source file name (optional companion to `line`).
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub file: Option<String>,
149
150    /// A source line number.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub line: Option<u32>,
153
154    // ---- RFC richer addressing (expressible, not led with) ----
155    /// A specific syscall by name (e.g. `"futex"`), the Nth occurrence of which
156    /// is the anchor. Combine with `phase` and `nth`.
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub syscall: Option<String>,
159
160    /// Which phase of the named `syscall` to anchor on.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub phase: Option<PhaseSpec>,
163
164    /// Restrict the named `syscall` to calls whose first argument is this file
165    /// descriptor, for example `{"syscall": "writev", "fd": 9, "nth": 3}`: the
166    /// thread's third `writev` to fd 9. The descriptor is matched by number at
167    /// the time of the call: a write to a `dup` of fd 9 under another number
168    /// does not count, and after fd 9 is closed and the number reused, calls
169    /// on the new fd 9 do. Only valid with `syscall`, and only for
170    /// a syscall whose first argument is a file descriptor
171    /// ([`syscall_takes_fd_first`]).
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub fd: Option<i32>,
174
175    /// Version 2 only: restrict `"syscall": "futex"` to calls whose operation
176    /// is this value. The kernel takes `int futex_op`, so the low 32 bits of
177    /// the second argument are compared, read as a signed `int`; the upper bits
178    /// of the register are ignored. The value must fit an `i32`.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub futex_op: Option<i64>,
181
182    /// Version 2 only: count this event's syscall occurrences strictly after
183    /// the entry at which the named event is reached on the same thread, so
184    /// `nth` is 1-based from there. The base entry itself never counts, even
185    /// when it matches. The base must be a count or syscall-occurrence anchor
186    /// on the same thread and may not itself use `from`.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub from: Option<String>,
189
190    /// A raw instruction pointer, as a hex string like `"0x401f3c"` or decimal.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub rip: Option<String>,
193
194    /// A cooperative marker name (guest-emitted). Reserved for a future backend.
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub mark: Option<String>,
197
198    /// Which occurrence of the addressed point (1-based). Defaults to 1. Only
199    /// meaningful for the occurrence-counted modes (`syscall`, `func`, `rip`,
200    /// `mark`); the absolute-count primaries (`syscalls`, `rcbs`) ignore it.
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub nth: Option<u64>,
203}
204
205/// Serializable mirror of [`SyscallPhase`] using lowercase author-friendly names.
206#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)]
207#[serde(rename_all = "lowercase")]
208pub enum PhaseSpec {
209    /// Before the syscall instruction executes.
210    #[serde(alias = "pre")]
211    Prehook,
212    /// A nonblocking poll retry.
213    Polling,
214    /// After the syscall logically completes.
215    #[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/// One ordering constraint: `before` happens-before `after`.
230#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
231pub struct EdgeSpec {
232    /// The name of the source event (must be observed first).
233    pub before: String,
234    /// The name of the sink event (blocked until the source fires).
235    pub after: String,
236    /// Enforcement strength; defaults to [`Strength::Hard`].
237    #[serde(default)]
238    pub strength: Strength,
239}
240
241/// How strictly an edge is enforced by the scheduler.
242#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
243#[serde(rename_all = "lowercase")]
244pub enum Strength {
245    /// Park the sink thread in a true gate until the source fires. The guarantee
246    /// wanted for constructed repros, and the default.
247    #[default]
248    Hard,
249    /// Merely bias scheduling (priority nudge); the sink may still run if it is
250    /// the only runnable thread.
251    Soft,
252}
253
254// ================================================================================
255// Normalized model
256// ================================================================================
257
258/// A resolved reference to a thread.
259#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
260pub struct ThreadRef {
261    /// The symbolic label (map key, or the raw id as text).
262    pub label: String,
263    /// The concrete `DetTid`, when statically known.
264    pub dettid: Option<DetTid>,
265    /// Resolve to the Nth spawned thread, when that is the rule.
266    pub spawn_ordinal: Option<u32>,
267}
268
269/// A source-level location, resolvable to/from an address via debug info.
270#[derive(PartialEq, Eq, Debug, Clone, Default)]
271pub struct CodeLocation {
272    /// Function name.
273    pub function: Option<String>,
274    /// Source file name.
275    pub file: Option<String>,
276    /// Source line number.
277    pub line: Option<u32>,
278}
279
280impl CodeLocation {
281    /// True when this location carries no information.
282    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/// The deterministic per-thread stop point that an anchor addresses.
301///
302/// All variants reduce to "a predicate over this thread's deterministic event
303/// stream plus an occurrence count," but the two leading variants
304/// ([`Position::SyscallCount`] and [`Position::Rcb`]) are absolute counts that
305/// need no per-anchor occurrence tracking.
306#[derive(PartialEq, Eq, Debug, Clone)]
307pub enum Position {
308    /// After the thread has executed exactly this many syscalls (any syscall).
309    SyscallCount(u64),
310
311    /// When the thread's RCB clock reaches this absolute value.
312    Rcb(u64),
313
314    /// The `nth` occurrence of a specific syscall on the thread, optionally
315    /// phase-qualified and restricted to one file descriptor. The occurrence is
316    /// counted per thread, over that thread's calls that match `sysno` (and
317    /// `fd`), so an anchor needs no calibration run to find a syscall count.
318    ///
319    /// Two properties to know when writing one:
320    /// - What is counted is syscall *entries*, the same unit as
321    ///   [`Position::SyscallCount`]. A matching call that a signal interrupts
322    ///   and the kernel restarts is entered, and counted, again.
323    /// - The counters belong to the thread, like its syscall count: a new
324    ///   thread starts at zero, an exec keeps them, and after a non-leader exec
325    ///   the calling thread's counters continue under the leader's thread id.
326    /// - By default it fires at the prehook: an edge then orders the BEFORE
327    ///   thread *reaching* its syscall, not that syscall's effect. The held
328    ///   thread is re-admitted at the next drain, so a BEFORE syscall that
329    ///   takes more scheduler turns than the AFTER thread needs (a `write` to
330    ///   stdout takes a checkpoint turn and a resource turn) can let the AFTER
331    ///   thread's effect land first, even under the default scheduler.
332    /// - With `phase` posthook it fires when the syscall's result is final:
333    ///   a value or an errno, `EINTR` included, decided before the guest
334    ///   resumes (<https://github.com/rrnewton/hermit/issues/3929>). An entry
335    ///   that returns an internal restart code is refused
336    ///   (`HERMIT_HB_POSTHOOK_INTERRUPTED`), because the kernel decides after
337    ///   that point whether it returns at all. A call that only a signal
338    ///   handler ends (`pause`, `rt_sigsuspend`) always returns one, so a
339    ///   posthook anchor on it always refuses. Occurrences are counted at entry
340    ///   either way, and the five syscalls that do not return on success are
341    ///   refused at load ([`syscall_never_returns_on_success`]).
342    /// - What a hard edge orders depends on both ends. A posthook BEFORE and a
343    ///   prehook AFTER order the BEFORE syscall's completed effect before the
344    ///   AFTER syscall runs. A posthook AFTER holds its thread only after its
345    ///   own call, so that call's effect may come before the BEFORE one's.
346    ///   With a prehook BEFORE, effects may land in either order.
347    /// - A posthook BEFORE on a call that only a held thread can complete
348    ///   never fires. A child wait or futex wait on the held thread ends in
349    ///   `HERMIT_HB_ANCHOR_NEVER_FIRED`; a read whose only writer is held is
350    ///   external input to the scheduler, and the run ends at `--timeout`.
351    Syscall {
352        /// The syscall number.
353        sysno: Sysno,
354        /// Restrict to a phase, or match any phase when `None`.
355        phase: Option<SyscallPhase>,
356        /// 1-based occurrence.
357        nth: u64,
358        /// Restrict to calls whose first argument is this file descriptor.
359        fd: Option<i32>,
360        /// Restrict `futex` to calls whose operation (the low 32 bits of the
361        /// second argument, as an `int`) is this value.
362        futex_op: Option<i32>,
363    },
364
365    /// The `nth` execution of the instruction at an absolute address. The address
366    /// is resolved later when it comes from a [`CodeLocation`].
367    Rip {
368        /// Absolute instruction pointer, or `None` until resolved from a
369        /// [`CodeLocation`].
370        addr: Option<u64>,
371        /// 1-based occurrence.
372        nth: u64,
373    },
374
375    /// A cooperative guest marker. Reserved for a future backend.
376    Marker {
377        /// Marker name.
378        name: String,
379        /// 1-based occurrence.
380        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/// A fully normalized anchor: a named, deterministic per-thread stop point.
418#[derive(PartialEq, Eq, Debug, Clone)]
419pub struct Anchor {
420    /// The event name (map key), for diagnostics and edge references.
421    pub name: String,
422    /// The thread this anchor is on.
423    pub thread: ThreadRef,
424    /// The deterministic position selector.
425    pub position: Position,
426    /// Optional human-legible / debug-info-resolved code location.
427    pub location: CodeLocation,
428    /// For a relative anchor, the event on the same thread after which its
429    /// occurrences are counted (`from`).
430    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    /// True for an enforced syscall-occurrence anchor: a [`Position::Syscall`]
448    /// with no phase, the prehook phase or the posthook phase. Its
449    /// occurrences are counted at the prehook of each matching syscall entry.
450    /// It fires at the prehook of the thread's `nth` matching entry, or, for
451    /// the posthook phase ([`Anchor::fires_at_posthook`]), when that entry
452    /// completes. The polling phase is not enforced.
453    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    /// True for a syscall-occurrence anchor that fires when its syscall
464    /// completes: after the call's effect and result, before the guest
465    /// resumes (<https://github.com/rrnewton/hermit/issues/3929>). An edge
466    /// whose BEFORE anchor fires at the posthook orders that syscall's
467    /// completed effect, not just its entry.
468    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    /// The per-thread counter this anchor's occurrences are counted in: its
479    /// syscall number, fd and futex operation, or `None` for any other kind of
480    /// anchor.
481    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    /// True when this is an enforced syscall-occurrence anchor and a call to
494    /// `sysno` with first two arguments `args` is one of the calls it counts.
495    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                // A file descriptor and a futex operation are each an `int`:
504                // the kernel reads the low 32 bits of the register.
505                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
513/// True for a syscall that does not return to its caller when it succeeds: the
514/// exec family replaces the image, and the exit family ends the thread. A
515/// posthook anchor on one is refused at load.
516pub 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
523/// True for a syscall that never reaches Hermit's tracer on the ptrace
524/// backend, whatever Detcore subscribes to: Reverie lets `rt_sigreturn` run
525/// untraced from its private page so a signal frame is restored safely. A
526/// syscall anchor on one could never fire, so the run refuses it.
527pub fn syscall_never_reaches_tracer(sysno: Sysno) -> bool {
528    sysno == Sysno::rt_sigreturn
529}
530
531/// The key of one per-thread occurrence counter: a syscall number, an optional
532/// file descriptor and an optional futex operation ([`Anchor::occurrence_key`]).
533pub type OccurrenceKey = (usize, Option<i32>, Option<i32>);
534
535/// One thread's syscall-occurrence counters
536/// ([`HappensBeforeProgram::count_syscall_occurrences`]).
537pub type OccurrenceCounters = BTreeMap<OccurrenceKey, u64>;
538
539/// One thread's happens-before counting state: its occurrence counters, and for
540/// each relative anchor whose base this thread has reached, the origin: the
541/// count of the relative anchor's counter just after the base entry. Belongs to
542/// the thread, like its syscall count: empty for a new thread, kept across the
543/// thread's own exec.
544#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
545pub struct HbThreadCounters {
546    /// Per occurrence key, how many of this thread's entries matched.
547    pub counts: OccurrenceCounters,
548    /// Per relative anchor whose base was reached, its origin. Each relative
549    /// anchor has its own origin, so the shared counters are never reset.
550    pub origins: BTreeMap<String, u64>,
551}
552
553/// True when `sysno`'s first argument is a file descriptor, so an anchor may
554/// restrict it with `fd`. The list is the fd-first syscalls a guest is likely to
555/// anchor on; an `fd` on any other syscall is refused when the spec is loaded
556/// rather than matched against an argument that is not a descriptor.
557pub 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        // aarch64, like every newer Linux port, has only the generic table,
597        // which leaves out these legacy calls.
598        #[cfg(target_arch = "x86_64")]
599        Sysno::epoll_wait | Sysno::dup2 => true,
600        _ => false,
601    }
602}
603
604/// A normalized happens-before edge between two anchors.
605#[derive(PartialEq, Eq, Debug, Clone)]
606pub struct HappensBeforeEdge {
607    /// The source anchor name (observed first).
608    pub before: String,
609    /// The sink anchor name (gated until the source fires).
610    pub after: String,
611    /// Enforcement strength.
612    pub strength: Strength,
613}
614
615/// A validated, normalized happens-before program: anchors indexed by name plus
616/// the edge list, guaranteed acyclic with all references resolved.
617#[derive(PartialEq, Eq, Debug, Clone)]
618pub struct HappensBeforeProgram {
619    /// The spec's schema version.
620    pub version: u32,
621    /// Normalized anchors, keyed by event name.
622    pub anchors: BTreeMap<String, Anchor>,
623    /// The validated, acyclic edge list.
624    pub edges: Vec<HappensBeforeEdge>,
625    /// The hold budget in committed virtual nanoseconds: `Some` for a
626    /// version-2 spec (its `max_hold_ns`, or [`DEFAULT_MAX_HOLD_NS`]), `None`
627    /// for version 1, which has no budget.
628    pub max_hold_ns: Option<u64>,
629}
630
631impl HappensBeforeProgram {
632    /// Anchors that still require debug-info resolution (an unresolved RIP from a
633    /// code location, i.e. a `func`/`line` that has not been turned into an
634    /// address yet).
635    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    /// True when thread `dettid` must check in with the scheduler at its
642    /// syscall number `count`: some anchor sits at exactly
643    /// [`Position::SyscallCount`]`(count)` and names `dettid`, or names its
644    /// thread by spawn ordinal (which only the scheduler can resolve, so the
645    /// guest side must ask). For any other (thread, count) the scheduler's
646    /// checkpoint finds no anchor and does nothing, so skipping it changes no
647    /// gate. It costs one scheduler round trip per anchor its thread reaches,
648    /// not one per syscall (<https://github.com/rrnewton/hermit/issues/3877>),
649    /// and threads a spec does not name keep their schedule: a count another
650    /// thread passes early (QEMU's main loop makes about a million syscalls a
651    /// run) no longer adds a scheduler turn there.
652    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    /// True when the program has any enforced syscall-occurrence anchor
660    /// ([`Anchor::is_syscall_occurrence`]), so the guest must count its calls.
661    pub fn has_syscall_occurrence_anchors(&self) -> bool {
662        self.anchors.values().any(Anchor::is_syscall_occurrence)
663    }
664
665    /// Count one syscall entry of thread `dettid` (`sysno`, first two
666    /// arguments `args`, the thread's `entry_count`th syscall) in that
667    /// thread's `counters`, and return the names of the syscall-occurrence
668    /// anchors this entry fires.
669    ///
670    /// `counters` belongs to one thread and is kept with its other per-thread
671    /// counts (the syscall count that count anchors use), so it starts empty
672    /// for every new thread. Per entry, in this order:
673    /// 1. Every distinct occurrence key (syscall, fd, futex operation) an
674    ///    anchor names that the entry matches is incremented once; an entry
675    ///    that matches both an fd-specific and an fd-agnostic anchor counts
676    ///    toward each.
677    /// 2. Each relative anchor whose base this entry reaches gets its origin:
678    ///    its own key's count after step 1, so the base entry is never counted
679    ///    in its window. A count base is reached when `entry_count` equals its
680    ///    count, whether or not the entry matches any key.
681    /// 3. An absolute anchor fires when its key's count equals `nth`; a
682    ///    relative anchor fires when its count minus its origin equals `nth`.
683    ///
684    /// An entry is counted exactly once, when the thread enters the syscall,
685    /// so a thread held at an anchor and re-admitted does not count again. The
686    /// result names anchors on `dettid` or on a spawn ordinal; the scheduler
687    /// resolves spawn ordinals and fires only the anchors on this thread.
688    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    /// True for a version-2 program, which carries a hold budget and needs an
756    /// effective preemption timer to charge it.
757    pub fn is_version_2(&self) -> bool {
758        self.version == HAPPENS_BEFORE_VERSION
759    }
760
761    /// The relative anchors and their bases, in name order.
762    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    /// Anchors whose position kind the current scheduler does not yet enforce:
772    /// everything other than [`Position::SyscallCount`] and the syscall
773    /// occurrence anchors ([`Anchor::is_syscall_occurrence`]). Reported so a run
774    /// does not silently ignore an authored ordering constraint it cannot honor.
775    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// ================================================================================
783// Errors
784// ================================================================================
785
786/// An error produced while parsing or validating a happens-before specification.
787#[derive(PartialEq, Eq, Debug, Clone)]
788pub enum HappensBeforeError {
789    /// The schema `version` is not understood by this build.
790    UnsupportedVersion(u32),
791    /// An event named more than one position selector, or none.
792    AmbiguousPosition {
793        /// The offending event name.
794        event: String,
795        /// The selectors that were set.
796        found: Vec<String>,
797    },
798    /// A syscall name could not be parsed.
799    UnknownSyscall {
800        /// The offending event name.
801        event: String,
802        /// The unparseable name.
803        name: String,
804    },
805    /// An occurrence-counted event (`syscall`, `rip`, `mark` or a code
806    /// location) set `nth` to 0; occurrences are 1-based.
807    NthZero {
808        /// The offending event name.
809        event: String,
810    },
811    /// An event set `fd` without `syscall`.
812    FdWithoutSyscall {
813        /// The offending event name.
814        event: String,
815    },
816    /// An event set `fd` on a syscall whose first argument is not a file
817    /// descriptor ([`syscall_takes_fd_first`]).
818    FdNotFirstArgument {
819        /// The offending event name.
820        event: String,
821        /// The syscall name.
822        syscall: String,
823    },
824    /// A RIP string could not be parsed as an address.
825    BadRip {
826        /// The offending event name.
827        event: String,
828        /// The unparseable text.
829        text: String,
830    },
831    /// An edge referenced an event that does not exist.
832    UnknownEvent {
833        /// `before` or `after`.
834        which: String,
835        /// The dangling name.
836        name: String,
837    },
838    /// An event referenced a thread label that is not in the `threads` table and
839    /// is not a raw integer id.
840    UnknownThread {
841        /// The offending event name.
842        event: String,
843        /// The dangling thread label.
844        thread: String,
845    },
846    /// The edge graph contains a cycle (listed in discovery order).
847    Cycle(Vec<String>),
848    /// A version-1 spec used a field that version 2 added.
849    NewFieldInVersion1 {
850        /// The offending event name, or `None` for a spec-level field.
851        event: Option<String>,
852        /// The field.
853        field: String,
854    },
855    /// A version-2 spec carried a field this build does not know.
856    UnknownField {
857        /// Where: `spec`, `event 'x'`, `thread 'x'` or `edge N`.
858        place: String,
859        /// The field.
860        field: String,
861    },
862    /// An event set `futex_op` on a syscall other than `futex`, or without
863    /// `syscall`.
864    FutexOpWithoutFutex {
865        /// The offending event name.
866        event: String,
867    },
868    /// An event's `futex_op` does not fit the kernel's `int`.
869    FutexOpOutOfRange {
870        /// The offending event name.
871        event: String,
872        /// The value given.
873        value: i64,
874    },
875    /// An event's `from` names an event that does not exist.
876    FromUnknownEvent {
877        /// The relative event.
878        event: String,
879        /// The missing base.
880        base: String,
881    },
882    /// An event that is not a prehook syscall-occurrence anchor set `from`.
883    FromOnNonOccurrence {
884        /// The offending event name.
885        event: String,
886    },
887    /// A relative event's base cannot be shown to be on the same thread: it
888    /// names another dettid or spawn ordinal, or a dettid where the relative
889    /// event names a spawn ordinal (or the reverse).
890    FromOtherThread {
891        /// The relative event.
892        event: String,
893        /// Its base.
894        base: String,
895    },
896    /// A relative event's base is neither a count anchor nor an enforced
897    /// syscall-occurrence anchor, so it can never be reached.
898    FromUnenforcedPosition {
899        /// The relative event.
900        event: String,
901        /// Its base.
902        base: String,
903    },
904    /// A relative event's base is itself relative; chains are not supported.
905    FromRelativeBase {
906        /// The relative event.
907        event: String,
908        /// Its base.
909        base: String,
910    },
911    /// The relative bases and the hard edges together form a cycle, so some
912    /// event on it could only be reached after itself.
913    FromCycle(Vec<String>),
914    /// A posthook anchor names a syscall that does not return to its caller
915    /// when it succeeds ([`syscall_never_returns_on_success`]).
916    PosthookOnNoReturnSyscall {
917        /// The offending event name.
918        event: String,
919        /// The syscall name.
920        syscall: String,
921    },
922    /// A DSL line could not be parsed.
923    DslSyntax {
924        /// 1-based line number.
925        line: usize,
926        /// What went wrong.
927        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
1062// ================================================================================
1063// Parsing & normalization
1064// ================================================================================
1065
1066impl HappensBeforeSpec {
1067    /// Parse a JSON specification. A version-2 spec is also checked for
1068    /// fields this build does not know ([`HappensBeforeError::UnknownField`]),
1069    /// so a misspelled field is refused instead of silently ignored; version 1
1070    /// keeps its old, lenient handling of unknown fields.
1071    ///
1072    /// The spec itself is deserialized from the original text, not from the
1073    /// `Value` used for that check: a `Value` keeps only the last of two equal
1074    /// keys, which would accept `"nth": 1, "nth": 2` as `nth` 2. Deserializing
1075    /// the text refuses a duplicated field, in either version.
1076    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    /// Serialize to pretty JSON.
1087    pub fn to_json(&self) -> anyhow::Result<String> {
1088        Ok(serde_json::to_string_pretty(self)?)
1089    }
1090
1091    /// Normalize and statically validate: check the version, resolve thread and
1092    /// event references, require exactly one position per event, parse syscalls
1093    /// and RIPs, and confirm the edge graph is acyclic.
1094    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        // Resolve edges against the anchor table.
1109        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        // A relative event is reached only after its base, on the same thread:
1132        // an activation edge base -> relative. With the hard edges, a cycle
1133        // means some event could only be reached after itself.
1134        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    /// Refuse, in a version-1 spec, every field that version 2 added.
1161    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    /// Resolve one event into a normalized [`Anchor`].
1185    fn normalize_event(&self, name: &str, ev: &EventSpec) -> Result<Anchor, HappensBeforeError> {
1186        let thread = self.resolve_thread(name, &ev.thread)?;
1187
1188        // A code location can accompany any position; it also *supplies* a RIP
1189        // position when no explicit position selector is present.
1190        let location = CodeLocation {
1191            function: ev.func.clone(),
1192            file: ev.file.clone(),
1193            line: ev.line,
1194        };
1195
1196        // Determine which *explicit* position selectors are present. A code
1197        // location (`func`/`file`/`line`) is descriptive and may accompany any
1198        // one of these — the owner's primary anchor is "function foo on thread T
1199        // after N syscalls / M RBCs", i.e. a code location *and* a count. The
1200        // code location only *becomes* the (deferred RIP) position when no
1201        // explicit selector is present at all.
1202        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        // Reject multiple explicit selectors outright. A single explicit
1221        // selector wins as the position (code location stays descriptive). Zero
1222        // explicit selectors is only valid when a code location supplies a RIP.
1223        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            // Code-location-only: a RIP to be resolved later from debug info.
1312            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    /// Resolve an event's `thread` field to a [`ThreadRef`], consulting the
1326    /// `threads` table and falling back to a raw integer id.
1327    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
1349/// Check every relative anchor's `from`: the base exists, is on the same
1350/// thread, is a count or syscall-occurrence anchor and is not itself relative;
1351/// the relative anchor is a syscall-occurrence anchor.
1352fn 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
1380/// True when two thread references resolve to the same thread whatever they
1381/// are called: the same `dettid`, or, with no `dettid` on either, the same
1382/// `spawn_ordinal`. A `dettid` and a `spawn_ordinal` cannot be shown equal
1383/// before the run, so they are not.
1384fn 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
1392/// Refuse a field a version-2 spec does not define. The field lists mirror
1393/// the serde structs above, including the `rcb` alias.
1394fn 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
1446/// Parse a RIP string: hex (`0x...`) or plain decimal.
1447fn 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
1456/// Detect a cycle in the edge graph via depth-first search, returning the cycle
1457/// path if one exists. Anchors are visited in name order for determinism.
1458fn detect_cycle(
1459    anchors: &BTreeMap<String, Anchor>,
1460    edges: &[HappensBeforeEdge],
1461) -> Result<(), HappensBeforeError> {
1462    // Adjacency: before -> [after...]
1463    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    // Iterative DFS to avoid stack overflow on deep chains, tracking the current
1481    // path so we can report a concrete cycle.
1482    for root in adj.keys().copied() {
1483        if state.contains_key(root) {
1484            continue;
1485        }
1486        // Stack of (node, index of next neighbor to visit).
1487        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                        // Found a back-edge: assemble the cycle from `path`.
1499                        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
1522// ================================================================================
1523// Terse DSL
1524// ================================================================================
1525//
1526// One edge per non-empty, non-comment line:
1527//
1528//     writer:free_buffer#342  <  reader:read_buffer#97
1529//     writer:futex@post#5     <  reader:@0x401f3c#1
1530//     A:rcb=123456            <  B:sc=97
1531//
1532// Each side is `thread:anchor[#ordinal]`. The anchor token is one of:
1533//   * `name`            -> function name (code location)
1534//   * `@0xADDR`         -> raw RIP
1535//   * `syscall@phase`   -> a named syscall, optional `@pre`/`@post`/`@polling`
1536//   * `rcb=M`           -> after M RBCs (owner primary)
1537//   * `sc=N`            -> after N syscalls (owner primary)
1538// A trailing `#N` sets the occurrence ordinal (ignored by `rcb=`/`sc=`).
1539// A `!soft` suffix on the line marks the edge soft; default is hard.
1540
1541impl HappensBeforeSpec {
1542    /// Parse the terse line-oriented DSL into a specification. Symbolic threads
1543    /// mentioned by name become entries in the `threads` table.
1544    pub fn from_dsl(input: &str) -> Result<HappensBeforeSpec, HappensBeforeError> {
1545        // The DSL has no version-2 syntax, so it builds a version-1 spec.
1546        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            // Optional trailing "!soft" / "!hard".
1563            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
1590/// Strip a `#`-or-`//` comment, but not a `#ordinal` that is part of a token.
1591/// We treat `//` as the only comment marker to avoid clashing with `#ordinal`.
1592fn strip_comment(line: &str) -> &str {
1593    match line.find("//") {
1594        Some(idx) => &line[..idx],
1595        None => line,
1596    }
1597}
1598
1599/// Parse one side of a DSL edge, registering the event and thread in `spec`, and
1600/// returning the generated event name.
1601fn 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    // Split a trailing "#ordinal".
1623    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        // raw rip: @0x...
1644        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        // syscall@phase
1665        ev.syscall = Some(sc.to_string());
1666        ev.phase = Some(parse_dsl_phase(phase, lineno)?);
1667    } else if is_syscall_name(anchor_body) {
1668        // bare syscall name
1669        ev.syscall = Some(anchor_body.to_string());
1670    } else {
1671        // function name (code location)
1672        ev.func = Some(anchor_body.to_string());
1673    }
1674
1675    // Generate a stable, unique event name from the token.
1676    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    // Register the thread label if not already present and not a raw id.
1687    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
1712/// True when the token parses as a known syscall name.
1713fn is_syscall_name(s: &str) -> bool {
1714    Sysno::from_str(s).is_ok()
1715}
1716
1717/// Turn a DSL token into a valid, readable event-name slug.
1718fn 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    /// The guest-side checkpoint filter is exact per thread: true for the named
1758    /// dettid at exactly its anchored count, false for another dettid or for
1759    /// count +/- 1 or at count 0, true for every thread at a spawn-ordinal
1760    /// anchor's count (the guest side cannot resolve ordinals), and false at
1761    /// numbers that belong to other position kinds (an RCB value, a
1762    /// syscall-by-name occurrence).
1763    #[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    /// `{"syscall": "writev", "fd": 9, "nth": 3}` is the thread's third
1794    /// `writev` to fd 9: a syscall-occurrence anchor that needs no calibration.
1795    #[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    /// `fd` restricts a named syscall whose first argument is a descriptor; set
1821    /// anywhere else it is refused by name when the spec is loaded, never
1822    /// matched against an argument that is not a descriptor.
1823    #[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    /// Occurrences are counted per thread over the calls that match: the
1866    /// anchor's syscall and, when it has one, its fd (the register's low 32
1867    /// bits). The second matching `writev` to fd 9 reaches an `nth: 2` anchor
1868    /// even with non-matching calls in between, an fd-agnostic anchor counts
1869    /// every fd, a spawn-ordinal anchor is returned on any thread (the
1870    /// scheduler resolves it), and a posthook anchor is never counted and is
1871    /// reported as unenforced.
1872    #[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        // Another fd and another syscall do not count toward fd9.
1894        assert!(count(&mut c7, t7, Sysno::writev, 8).is_empty());
1895        assert!(count(&mut c7, t7, Sysno::write, 9).is_empty());
1896        // Low 32 bits: this is fd 9, the second, and the third writev of any fd.
1897        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        // Thread 8 has its own counters; anchors on thread 7 are never its.
1903        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        // A spawn-ordinal anchor is returned on any thread at its nth.
1907        assert_eq!(count(&mut c8, t8, Sysno::write, 1), ["spawned"]);
1908        assert!(count(&mut c8, t8, Sysno::write, 2).is_empty());
1909        // A polling anchor is not enforced, never counted.
1910        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        // A count anchor is not a syscall-occurrence anchor and is unaffected.
1918        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        // Owner-primary positions.
1936        assert_eq!(prog.anchors["scA"].position, Position::SyscallCount(10));
1937        assert_eq!(prog.anchors["rcbB"].position, Position::Rcb(123456));
1938
1939        // Function+line becomes an unresolved RIP with a code location attached.
1940        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        // Syscall anchor parses the name and phase.
1951        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        // RIP anchor.
1967        assert_eq!(
1968            prog.anchors["storeB"].position,
1969            Position::Rip {
1970                addr: Some(0x401f3c),
1971                nth: 1
1972            }
1973        );
1974
1975        // Soft strength preserved.
1976        assert_eq!(prog.edges[2].strength, Strength::Soft);
1977        // Default strength is hard.
1978        assert_eq!(prog.edges[1].strength, Strength::Hard);
1979
1980        // One anchor needs debug-info resolution (X_342, Y_97).
1981        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        // The owner's primary anchor: "function foo (line L) on thread T after
2038        // N syscalls / M RBCs". The code location is descriptive and the count
2039        // is the enforced position; the two must coexist, not conflict.
2040        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        // The count wins as the position; the code location is retained.
2054        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        // A descriptive-only code location is not an unresolved RIP position.
2068        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                // Path forms a closed loop.
2146                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        // The rcb/sc line desugars to owner-primary positions and is soft.
2185        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        // futex@post#5 desugars to a phase-qualified syscall.
2194        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    /// Version 2 is a new schema: a version-1 spec loads with its old meaning
2228    /// and no hold budget, but may not use a version-2 field; a version-2 spec
2229    /// gets the default budget; any other version is refused. The DSL has no
2230    /// version-2 syntax and builds version 1.
2231    #[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    /// A version-2 spec refuses a field this build does not know, at every
2290    /// level, so a misspelling is not silently ignored; version 1 keeps its
2291    /// lenient parsing.
2292    #[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        // The `rcb` alias is a known field.
2323        normalize_json(r#"{"version": 2, "events": {"r": {"thread": "7", "rcb": 5}}}"#).unwrap();
2324        // Version 1 ignores an unknown field, as it always has.
2325        normalize_json(r#"{"version": 1, "events": {}, "evnets": {}}"#).unwrap();
2326    }
2327
2328    /// A field given twice is refused in both versions, rather than the last
2329    /// value silently winning (the Codex review of
2330    /// https://github.com/rrnewton/hermit/pull/4004).
2331    #[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    /// A posthook syscall anchor is enforced: it is counted at its entry like
2344    /// any occurrence anchor and named at that entry, and only its firing
2345    /// point differs (https://github.com/rrnewton/hermit/issues/3929). It is
2346    /// refused at load on syscalls that do not return on success.
2347    #[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        // At the prehook the same syscalls stay valid anchors.
2380        normalize_json(r#"{"version": 1, "events": {"x": {"thread": "7", "syscall": "execve"}}}"#)
2381            .unwrap();
2382    }
2383
2384    /// `futex_op` restricts `futex` to one operation, compared on the low 32
2385    /// bits of the second argument as the kernel's `int`; it is refused on any
2386    /// other syscall and outside `i32`.
2387    #[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        // Operation 0 counts only toward its own matcher.
2405        assert_eq!(call(&mut c, 0), ["wait"]);
2406        // The same operation with other upper register bits is operation 393.
2407        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    /// Each malformed `from` is refused by name when the spec is loaded.
2442    #[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        // R counts from A, so R comes after A on that thread; a hard edge
2507        // R before A would need A after R.
2508        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        // One thread named two ways (a label for dettid 3, and "3") is the
2517        // same thread; a dettid and a spawn ordinal cannot be shown equal.
2518        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        // A soft edge does not hold a thread, so it closes no cycle.
2537        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    /// The design's per-entry order: increment, then record origins, then
2547    /// fire. Absolute and relative anchors on one matcher share its counter;
2548    /// each relative anchor has its own origin; a base entry that matches the
2549    /// relative matcher is not in the window; a count base resolves on an
2550    /// entry that matches no key.
2551    #[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        // Entry 1: getppid #1 fires gp1, which is after_gp1's base; that
2584        // entry is not in after_gp1's window.
2585        assert_eq!(call(&mut c, Sysno::getppid, 0), ["gp1"]);
2586        assert_eq!(c.origins["after_gp1"], 1);
2587        // Entry 2: write #1 to fd 1.
2588        assert!(call(&mut c, Sysno::write, 1).is_empty());
2589        // Entry 3: getppid #2, the first after gp1.
2590        assert_eq!(call(&mut c, Sysno::getppid, 0), ["after_gp1"]);
2591        // Entry 4: an unwatched syscall that matches no key is c4's base
2592        // entry; after_c4's origin is getppid's count so far.
2593        assert!(call(&mut c, Sysno::getpid, 0).is_empty());
2594        assert_eq!(c.origins["after_c4"], 2);
2595        // Entry 5: write #2 to fd 1 fires m2 and starts after_m2's window.
2596        assert_eq!(call(&mut c, Sysno::write, 1), ["m2"]);
2597        assert_eq!(c.origins["after_m2"], 2);
2598        // Entry 6: getppid #3 is abs3, the first after m2, and the first
2599        // after c4; one shared counter, independent origins.
2600        assert_eq!(call(&mut c, Sysno::getppid, 0), ["abs3", "after_m2"]);
2601        // Entry 7: getppid #4 is the second after c4.
2602        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        // A new thread starts with no origins: the relative anchors on thread
2607        // 7 are never counted for thread 8.
2608        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    /// The phase is not part of an anchor's counting key: a prehook and a
2617    /// posthook anchor on one matcher share one counter, and both are named at
2618    /// the same entry; a posthook base starts its relative anchor's window at
2619    /// its entry, as a prehook base does
2620    /// (https://github.com/rrnewton/hermit/issues/3929).
2621    #[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        // Entry 3: the posthook base, named at its entry; its window starts
2645        // at the write count so far.
2646        assert_eq!(call(&mut c, Sysno::getpid, 0), ["base"]);
2647        assert_eq!(c.origins["after_base"], 2);
2648        // Entry 4: write #3 is post3 and the first write after the base.
2649        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}