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