Skip to main content

detcore_model/
schedule.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
9use std::fmt;
10use std::num::NonZeroUsize;
11use std::str::FromStr;
12
13use nix::sys::signal::Signal;
14use reverie_syscalls::Sysno;
15use serde::Deserialize;
16use serde::Serialize;
17use serde::Serializer;
18use serde::de;
19
20use crate::pid::DetTid;
21use crate::time::DetTime;
22use crate::time::LogicalDuration;
23use crate::time::LogicalTime;
24
25pub mod boundary;
26// Scheduler events
27//--------------------------------------------------------------------------------
28
29/// A scheduled action by one thread in the system.  This can be recorded, or replayed to guide the
30/// schedule.
31#[derive(PartialEq, Debug, Eq, Clone, Hash, Serialize, Deserialize)]
32pub struct SchedEvent {
33    /// The thread that originated the event.
34    pub dettid: DetTid,
35    /// The operation performed by the thread.
36    pub op: Op,
37    /// The consecutive count of that same operation (run length encoding).
38    pub count: u32,
39    /// The instruction pointer before this batch of operations.
40    pub start_rip: Option<InstructionPointer>,
41    /// The instruction pointer after this batch of operations.
42    pub end_rip: Option<InstructionPointer>,
43    /// An optional snapshot of the thread logical time at this point.
44    /// This includes time waiting on the global scheduler.
45    pub end_time: Option<LogicalTime>,
46}
47
48/// A more compact printing.
49impl fmt::Display for SchedEvent {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "(tid{}", self.dettid)?;
52        if self.count > 1 {
53            write!(f, " cnt={}", self.count)?;
54        }
55        if let Some(srip) = self.start_rip {
56            write!(f, " strt={:#x}", srip)?;
57        }
58        if let Some(erip) = self.end_rip {
59            write!(f, " end={:#x}", erip)?;
60        }
61        if let Some(time) = self.end_time {
62            write!(f, " time={}", time)?;
63        }
64        write!(f, " {:?})", self.op)?;
65        Ok(())
66    }
67}
68
69impl SchedEvent {
70    /// Add a syscall to the global scheduling history.  This takes the instruction pointer of the
71    /// syscall itself.
72    pub fn syscall(dettid: DetTid, sysno: Sysno, phase: SyscallPhase) -> SchedEvent {
73        SchedEvent {
74            dettid,
75            op: Op::Syscall(sysno, phase),
76            count: 1,
77            start_rip: None,
78            end_rip: None,
79            end_time: None,
80        }
81    }
82
83    /// Add a batch of branches to the global scheduling history.
84    pub fn branches(dettid: DetTid, count: u32) -> SchedEvent {
85        SchedEvent {
86            dettid,
87            op: Op::Branch,
88            count,
89            start_rip: None, // TODO: track the start of the interval as well.
90            end_rip: None,
91            end_time: None,
92        }
93    }
94
95    /// Set the logical time directly.
96    pub fn with_time(mut self, time: LogicalDuration) -> SchedEvent {
97        self.end_time = Some(time);
98        self
99    }
100
101    /// Correctly set logical time based on the threads current time.
102    pub fn with_dettime(mut self, dt: &DetTime) -> SchedEvent {
103        self.end_time = Some(dt.without_starting());
104        self
105    }
106
107    /// Set the start_rip field.  The instruction pointer before the event began executing.
108    pub fn with_start_rip(mut self, start_rip: InstructionPointer) -> Self {
109        self.start_rip = Some(start_rip);
110        self
111    }
112
113    /// Set the end_rip field.  The instruction pointer after the event completed.
114    pub fn with_end_rip(mut self, end_rip: InstructionPointer) -> Self {
115        self.end_rip = Some(end_rip);
116        self
117    }
118}
119
120/// The type of the RIP value.
121pub type InstructionPointer = NonZeroUsize;
122
123/// Which phase of the syscall did we observe on a given event: the prehook or the posthook.
124#[derive(PartialEq, Debug, Eq, Copy, Clone, Hash, Serialize, Deserialize)]
125pub enum SyscallPhase {
126    /// The event was recorded before physically beginning the syscall.
127    Prehook,
128
129    /// An internal (nonblocking) retry of the syscall to check if its done yet (but it wasn't).
130    Polling,
131
132    /// The event was recorded after the syscall logically completed.
133    Posthook,
134}
135
136/// A signal the scheduler can name, INCLUDING the realtime signals `nix` cannot.
137///
138/// ⚠️ WHY THIS IS A RAW `i32` AND NOT A `nix::Signal`. It used to be
139/// `SigWrapper(pub Signal)`, and `nix`'s `Signal` models only 1..=31. Every
140/// cross-task notification path gated on `Signal::try_from(raw)`, so a
141/// `tgkill`/`tkill`/`rt_tgsigqueueinfo`/`rt_sigqueueinfo` carrying
142/// `SIGRTMIN..SIGRTMAX` delivered the signal to the target and then SILENTLY
143/// skipped `NotifySignalPending`. Measured in-tree: the gate admitted exactly
144/// 1..=31 and ZERO of the 31 realtime signals. A thread parked on
145/// `ResourceID::WaitChild` was therefore never woken and the wait hung —
146/// permanently, until the child exited or the thread was killed.
147///
148/// The two `rt_*sigqueueinfo` sites are the sharp end: they are reached from
149/// `sigqueue()`/`pthread_sigqueue()`, which are used with realtime signals in
150/// essentially all real code, so those notification call sites were wired up
151/// and inert for their only normal use.
152///
153/// ⚠️ THE SERIALIZED FORM IS DELIBERATELY UNCHANGED FOR EVERY SIGNAL THAT COULD
154/// ALREADY BE REPRESENTED. 1..=31 still render as the `nix` name (`"SIGUSR1"`),
155/// byte-for-byte as before, so existing schedule files and DETLOG output do not
156/// move. Only the previously-impossible values gain a spelling, `"SIG<n>"`, and
157/// the deserializer accepts both. This is what keeps a model-type widening from
158/// becoming a schedule-compatibility break.
159#[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
160pub struct SigWrapper(pub i32);
161
162impl SigWrapper {
163    /// The raw signal number, always available.
164    pub fn raw(&self) -> i32 {
165        self.0
166    }
167
168    /// The `nix` signal, when one exists. `None` for realtime signals: they are
169    /// real, deliverable, and simply unnamed by `nix`.
170    pub fn signal(&self) -> Option<Signal> {
171        Signal::try_from(self.0).ok()
172    }
173
174    /// How this signal is spelled in schedule files and DETLOG.
175    pub fn as_string(&self) -> String {
176        match self.signal() {
177            Some(signal) => signal.as_str().to_string(),
178            None => format!("SIG{}", self.0),
179        }
180    }
181}
182
183impl std::fmt::Display for SigWrapper {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        write!(f, "{}", self.as_string())
186    }
187}
188
189impl Serialize for SigWrapper {
190    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
191    where
192        S: Serializer,
193    {
194        serializer.serialize_str(&self.as_string())
195    }
196}
197
198impl<'de> de::Deserialize<'de> for SigWrapper {
199    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200    where
201        D: de::Deserializer<'de>,
202    {
203        struct SignalVisitor;
204        impl<'de> de::Visitor<'de> for SignalVisitor {
205            type Value = SigWrapper;
206
207            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
208                write!(f, "string representing a signal")
209            }
210
211            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
212            where
213                E: serde::de::Error,
214            {
215                SigWrapper::from_str(v).map_err(serde::de::Error::custom)
216            }
217        }
218
219        deserializer.deserialize_str(SignalVisitor)
220    }
221}
222
223impl FromStr for SigWrapper {
224    type Err = anyhow::Error;
225
226    fn from_str(s: &str) -> anyhow::Result<Self> {
227        // The `nix` name first, so every previously-valid spelling keeps
228        // parsing exactly as it did.
229        if let Ok(signal) = Signal::from_str(s) {
230            return Ok(SigWrapper(signal as i32));
231        }
232        // Then the realtime spelling this type adds.
233        if let Some(rest) = s.strip_prefix("SIG")
234            && let Ok(raw) = rest.parse::<i32>()
235            && raw > 0
236        {
237            return Ok(SigWrapper(raw));
238        }
239        anyhow::bail!("not a signal: {s}")
240    }
241}
242
243impl From<Signal> for SigWrapper {
244    fn from(signal: Signal) -> Self {
245        Self(signal as i32)
246    }
247}
248
249#[cfg(test)]
250mod sigwrapper_tests {
251    use super::*;
252
253    /// ⚠️ THE COMPATIBILITY CLAIM, DEMONSTRATED RATHER THAN ASSERTED.
254    ///
255    /// Widening a serialized model type is only safe if every value that could
256    /// ALREADY be written still round-trips to the same bytes. This walks all 31
257    /// signals `nix` can name and requires the serialized form to be exactly the
258    /// `nix` name — which is what the old `serialize_str(self.0.as_str())`
259    /// produced — so no existing schedule file or DETLOG line moves.
260    #[test]
261    fn every_previously_representable_signal_serializes_byte_identically() {
262        for raw in 1..=31i32 {
263            let Ok(signal) = Signal::try_from(raw) else {
264                continue;
265            };
266            let wrapper = SigWrapper::from(signal);
267            let json = serde_json::to_string(&wrapper).expect("serialize");
268            // Exactly what the pre-widening implementation emitted.
269            let expected = serde_json::to_string(signal.as_str()).expect("serialize name");
270            assert_eq!(
271                json,
272                expected,
273                "signal {raw} ({}) changed its serialized form",
274                signal.as_str()
275            );
276        }
277    }
278
279    /// The previously-impossible values gain a spelling, and it does not collide
280    /// with any `nix` name.
281    #[test]
282    fn realtime_signals_gain_a_distinct_spelling() {
283        for raw in 32..=64i32 {
284            let wrapper = SigWrapper(raw);
285            assert_eq!(wrapper.as_string(), format!("SIG{raw}"));
286            assert_eq!(wrapper.signal(), None, "nix must still not name {raw}");
287        }
288    }
289
290    /// Round-trip in both directions, over the whole space this type now models.
291    #[test]
292    fn every_signal_round_trips_through_serde_and_fromstr() {
293        for raw in 1..=64i32 {
294            let wrapper = SigWrapper(raw);
295            let json = serde_json::to_string(&wrapper).expect("serialize");
296            let back: SigWrapper = serde_json::from_str(&json).expect("deserialize");
297            assert_eq!(back, wrapper, "serde round-trip lost signal {raw}");
298            let parsed = SigWrapper::from_str(&wrapper.as_string()).expect("from_str");
299            assert_eq!(parsed, wrapper, "FromStr round-trip lost signal {raw}");
300        }
301    }
302
303    /// A reader written before the widening emitted names; a reader after it may
304    /// emit either. Both spellings must parse, or an old schedule file stops
305    /// loading.
306    #[test]
307    fn both_spellings_deserialize() {
308        let by_name: SigWrapper = serde_json::from_str("\"SIGUSR1\"").expect("name");
309        assert_eq!(by_name, SigWrapper::from(Signal::SIGUSR1));
310        let by_number: SigWrapper = serde_json::from_str("\"SIG40\"").expect("number");
311        assert_eq!(by_number, SigWrapper(40));
312    }
313}
314
315/// NOTE [Event Semantics]
316///
317/// The observable operations that happen on a guest thread.
318///
319/// Each Op event has a beginning and an end, containing an interval of zero or more instructions
320/// inbetween. Each beginning and end point in time can be thought of as an imaginary marker between
321/// two instructions.  Start/end RIP values, if present in the containing `SchedEvent`, correspond
322/// to those beginning/end points and always point to the *next* instruction to execute.
323///
324/// If we speak of an event as an instantaneous thing, we're usually thinking of it as its end
325/// marker. Which is as follows for each:
326///
327/// - Branches: after the  branch instruction has retired
328/// - Syscall prehooks: just before the syscall instruction, after whatever came before
329/// - Syscall posthooks: just after the syscall instruction completes
330/// - Rdtsc/Cpuid: just after the designated instruction
331/// - OtherInstructions: just after the region of zero or more branch-free, non-interceptable
332///   instructions.
333/// - SignalReceived: just after the last regular, pre-signal guest instruction, and just before the
334///   first instruction of the signal handler.
335///
336/// If we view each event as a series of instructions contained between its start/end markers, then
337/// the pattern of instructions for each would be as follows.  Here we use simple regular
338/// expressions with "B" standing for branch instructions, "S" for syscall instructions, "R" for
339/// RDTSC, "C" for CPUID, and "O" for all other instructions.
340///
341/// - Branch "O*B"
342/// - Syscall prehook: ""
343/// - Syscall posthook: "S"
344/// - Rdtsc: "R"
345/// - Cpuid: "C"
346/// - OtherInstructions: "O*"
347/// - SignalReceived: ""
348///
349/// A few observations about the above:
350///
351/// - Some events always correspond to zero instructions.
352/// - OtherInstructions are omnipresent "dark matter" that we cannot intercept or count, so are
353///   implicitly present between other events.
354/// - Therefore the OtherInstructions event itself is only interesting insofar as it signals the
355///   absence of other events.
356/// - Branches include an implicit prefix of OtherInstructions.  This is because for a branch count
357///   greater than 1 to make sense, we need to include the full between-branches O's: "..BO*B..". We
358///   could change this design by going to either extreme. (1) removing implicit O's and changing the
359///   count mechanism to allow repetition of entire sequences "(O*B)^3" instead of "B^3".  Or (2),
360///   including implicit O's in all event types, and not recording them explicitly.
361#[derive(PartialEq, Debug, Eq, Copy, Clone, Hash, Serialize, Deserialize)]
362pub enum Op {
363    /// A single retired conditional branch, corresponding to one increment of the RCB counter.
364    Branch,
365
366    /// A nondeterministic rdtsc instruction.
367    Rdtsc,
368
369    /// A nondeterministic cpuid instruction.
370    Cpuid,
371
372    /// A system call performed by the thread.  The bool is set to true when this is a syscall
373    /// PREHOOKh event, which is recorded BEFORE the syscall instruction executes, rather than
374    /// after.
375    Syscall(Sysno, SyscallPhase),
376
377    /// An unknown number of other instructions that occured BETWEEN hermit-interceptable events.
378    /// The only way to preempt in between these is expensive single-stepping.
379    OtherInstructions,
380
381    /// The point a signal handler is received, just after whatever regular user instruction
382    /// preceeded it, and just before the first instruction of the signal handler.
383    SignalReceived(SigWrapper),
384}