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}