1use detcore::Config;
20
21use crate::Backend;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct UnsupportedPreemption {
26 backend: &'static str,
27 setting: &'static str,
28 missing: MissingCapability,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum MissingCapability {
34 TimerEvents,
36 GuestBacktraces,
39}
40
41impl std::fmt::Display for UnsupportedPreemption {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 let reason = match self.missing {
44 MissingCapability::TimerEvents => {
45 "this runtime cannot deliver Detcore preemption timer events"
46 }
47 MissingCapability::GuestBacktraces => {
48 "this runtime cannot unwind guest stacks, so no preemption would have a \
49 stack to print"
50 }
51 };
52 write!(
53 f,
54 "unsupported_preemption: --backend={} refuses {}: {reason}; the guest has not started",
55 self.backend, self.setting,
56 )?;
57 if self.setting == "--max-timeslice" {
58 write!(
59 f,
60 "; to request execution without that bound, pass --max-timeslice=disabled"
61 )?;
62 } else if self.setting == "--chaos-timeslice-range" {
63 write!(
64 f,
65 "; use --backend=ptrace, or remove --chaos-timeslice-range and \
66 --chaos-timeslice-draw=run to use the inherited cooperative scheduling settings"
67 )?;
68 }
69 Ok(())
70 }
71}
72
73impl std::error::Error for UnsupportedPreemption {}
74
75pub fn refuse_unsupported_preemption(
92 config: &Config,
93 backend: Backend,
94 maximum_requested: bool,
95 skid_margin_requested: bool,
96) -> Result<(), UnsupportedPreemption> {
97 refuse_unsupported_preemption_with_capabilities(
98 config,
99 backend.as_str(),
100 crate::backend_capabilities(backend),
101 maximum_requested,
102 skid_margin_requested,
103 )
104}
105
106pub fn refuse_unsupported_preemption_with_capabilities(
110 config: &Config,
111 backend: &'static str,
112 capabilities: reverie::BackendCapabilities,
113 maximum_requested: bool,
114 skid_margin_requested: bool,
115) -> Result<(), UnsupportedPreemption> {
116 if capabilities.delivers_preemption_timer_events {
117 if capabilities.provides_guest_backtraces {
118 return Ok(());
119 }
120 let setting = if config.preemption_stacktrace {
121 "--preemption-stacktrace"
122 } else if config.preemption_stacktrace_log_file.is_some() {
123 "--preemption-stacktrace-log-file"
124 } else {
125 return Ok(());
126 };
127 return Err(UnsupportedPreemption {
128 backend,
129 setting,
130 missing: MissingCapability::GuestBacktraces,
131 });
132 }
133 let setting = if config.chaos_timeslice_range.is_some() {
134 "--chaos-timeslice-range"
136 } else if config.chaos_timeslice.is_some() {
137 "a replayed record's drawn --max-timeslice"
139 } else if maximum_requested && config.max_timeslice.is_some() {
140 "--max-timeslice"
141 } else if config.imprecise_timers {
142 "--imprecise-timers"
143 } else if !config.interrupt_at.is_empty() {
144 "--interrupt-at"
145 } else if config.preemption_stacktrace {
146 "--preemption-stacktrace"
147 } else if config.preemption_stacktrace_log_file.is_some() {
148 "--preemption-stacktrace-log-file"
149 } else if config.panic_on_rcb_overshoot {
150 "--panic-on-rbc-overshoot (--panic-on-rcb-overshoot)"
151 } else if skid_margin_requested {
152 "--skid-margin"
153 } else if config.replay_preemptions_from.is_some() {
154 "--replay-preemptions-from"
155 } else {
156 return Ok(());
157 };
158 Err(UnsupportedPreemption {
159 backend,
160 setting,
161 missing: MissingCapability::TimerEvents,
162 })
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct UnqualifiedAnnotatedReplay {
169 backend: Option<&'static str>,
172 setting: &'static str,
173}
174
175impl std::fmt::Display for UnqualifiedAnnotatedReplay {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 write!(
178 f,
179 "HERMIT_CALLBACK_CHECKPOINT_UNQUALIFIED_MODE: --replay-preemptions-from names a record \
180 with callback checkpoints, which replay only with precise PMU preemption timer \
181 events; "
182 )?;
183 match self.backend {
184 Some(backend) => write!(f, "--backend={} {}", backend, self.setting)?,
185 None => write!(f, "the launch configuration {}", self.setting)?,
186 }
187 write!(f, "; the guest has not started")
188 }
189}
190
191impl std::error::Error for UnqualifiedAnnotatedReplay {}
192
193pub fn refuse_unqualified_annotated_replay(
204 config: &Config,
205 backend: Backend,
206) -> Result<(), UnqualifiedAnnotatedReplay> {
207 refuse_unqualified_annotated_replay_with_capabilities(
208 config,
209 backend.as_str(),
210 crate::backend_capabilities(backend),
211 )
212}
213
214pub fn refuse_unqualified_annotated_replay_with_capabilities(
216 config: &Config,
217 backend: &'static str,
218 capabilities: reverie::BackendCapabilities,
219) -> Result<(), UnqualifiedAnnotatedReplay> {
220 let effective = Config {
221 backend: capabilities,
222 ..config.clone()
223 };
224 match effective.annotated_priority_replay_unqualified() {
225 None => Ok(()),
226 Some(setting) => Err(UnqualifiedAnnotatedReplay {
227 backend: Some(backend),
228 setting,
229 }),
230 }
231}
232
233pub fn refuse_unqualified_annotated_launch(
241 config: &Config,
242) -> Result<(), UnqualifiedAnnotatedReplay> {
243 if !config.annotated_priority_replay {
244 return Ok(());
245 }
246 match config.annotated_priority_replay_unqualified() {
247 None => Ok(()),
248 Some(setting) => Err(UnqualifiedAnnotatedReplay {
249 backend: None,
250 setting,
251 }),
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use clap::Parser;
258
259 use super::*;
260
261 const TIMERLESS: [(&str, reverie::BackendCapabilities); 4] = [
264 ("dbt", reverie::BackendCapabilities::DBT),
265 ("sabre", reverie::BackendCapabilities::SABRE),
266 ("liteinst", reverie::BackendCapabilities::LITEINST_IN_GUEST),
267 (
268 "in-guest-trap",
269 reverie::BackendCapabilities::LITEINST_IN_GUEST,
270 ),
271 ];
272
273 #[test]
274 fn explicit_active_maximum_is_refused_without_normalizing_the_request() {
275 for args in [
276 vec!["detcore"],
277 vec!["detcore", "--max-timeslice=1000"],
278 vec!["detcore", "--preemption-timeout=1000"],
279 vec!["detcore", "--no-rcb-time"],
280 ] {
281 let config = Config::parse_from(args);
282 let before = serde_json::to_value(&config).unwrap();
283 for (backend, capabilities) in TIMERLESS {
284 let error = refuse_unsupported_preemption_with_capabilities(
285 &config,
286 backend,
287 capabilities,
288 true,
289 false,
290 )
291 .unwrap_err();
292 assert_eq!(error.setting, "--max-timeslice");
293 let message = error.to_string();
294 assert!(message.contains(backend), "{message}");
295 assert!(message.contains("unsupported_preemption"), "{message}");
296 assert!(
297 message.contains("pass --max-timeslice=disabled"),
298 "{message}"
299 );
300 assert_eq!(serde_json::to_value(&config).unwrap(), before);
301 }
302 refuse_unsupported_preemption(&config, Backend::Ptrace, true, false).unwrap();
303 }
304 }
305
306 #[test]
307 fn explicit_timer_controls_still_refuse_after_disabling_the_maximum() {
308 for (args, setting) in [
309 (vec!["--imprecise-timers"], "--imprecise-timers"),
310 (vec!["--interrupt-at=3:1000"], "--interrupt-at"),
311 (vec!["--preemption-stacktrace"], "--preemption-stacktrace"),
312 (
313 vec!["--preemption-stacktrace-log-file=unused"],
314 "--preemption-stacktrace-log-file",
315 ),
316 (
317 vec!["--panic-on-rbc-overshoot"],
318 "--panic-on-rbc-overshoot (--panic-on-rcb-overshoot)",
319 ),
320 (
321 vec!["--panic-on-rcb-overshoot"],
322 "--panic-on-rbc-overshoot (--panic-on-rcb-overshoot)",
323 ),
324 (
325 vec!["--replay-preemptions-from=unused"],
326 "--replay-preemptions-from",
327 ),
328 (
329 vec!["--chaos", "--chaos-timeslice-range=1000..2000"],
330 "--chaos-timeslice-range",
331 ),
332 ] {
333 let config = Config::parse_from(
334 ["detcore", "--max-timeslice=disabled"]
335 .into_iter()
336 .chain(args),
337 );
338 for (backend, capabilities) in TIMERLESS {
339 let error = refuse_unsupported_preemption_with_capabilities(
340 &config,
341 backend,
342 capabilities,
343 true,
344 false,
345 )
346 .unwrap_err();
347 assert_eq!(error.setting, setting);
348 if setting == "--chaos-timeslice-range" {
349 let message = error.to_string();
350 assert!(message.contains("--chaos-timeslice-draw=run"), "{message}");
351 assert!(message.contains("--backend=ptrace"), "{message}");
352 }
353 }
354 refuse_unsupported_preemption(&config, Backend::Ptrace, true, false).unwrap();
355 }
356 let config = Config::parse_from(["detcore", "--max-timeslice=disabled"]);
357 for (backend, capabilities) in TIMERLESS {
358 assert_eq!(
359 refuse_unsupported_preemption_with_capabilities(
360 &config,
361 backend,
362 capabilities,
363 true,
364 true
365 )
366 .unwrap_err()
367 .setting,
368 "--skid-margin",
369 );
370 }
371 refuse_unsupported_preemption(&config, Backend::Ptrace, true, true).unwrap();
372 }
373
374 #[test]
378 fn an_annotated_priority_history_replays_only_with_precise_rcb_timers() {
379 let qualified = Config::parse_from([
380 "detcore",
381 "--chaos",
382 "--sequentialize-threads",
383 "--max-timeslice=1000000",
384 "--replay-preemptions-from=unused",
385 ]);
386 refuse_unqualified_annotated_replay(&qualified, Backend::Ptrace).unwrap();
387 refuse_unqualified_annotated_replay(&qualified, Backend::Kvm).unwrap();
389 for (backend, capabilities) in TIMERLESS {
390 let refusal = refuse_unqualified_annotated_replay_with_capabilities(
391 &qualified,
392 backend,
393 capabilities,
394 )
395 .unwrap_err();
396 assert_eq!(
397 refusal.setting,
398 "cannot deliver Detcore preemption timer events"
399 );
400 let message = refusal.to_string();
401 assert!(
402 message.starts_with("HERMIT_CALLBACK_CHECKPOINT_UNQUALIFIED_MODE"),
403 "{message}"
404 );
405 assert!(message.contains(backend), "{message}");
406 }
407 for (flags, setting) in [
408 (
409 ["--max-timeslice=1000000", "--chaos"],
410 "does not sequentialize threads (--no-sequentialize-threads)",
411 ),
412 (
413 ["--max-timeslice=disabled", "--sequentialize-threads"],
414 "has its preemption timer disabled (--max-timeslice=disabled)",
415 ),
416 (
417 ["--sequentialize-threads", "--imprecise-timers"],
418 "requests imprecise timers (--imprecise-timers)",
419 ),
420 (
421 ["--sequentialize-threads", "--no-rcb-time"],
422 "requests logical time without RCBs (--no-rcb-time)",
423 ),
424 ] {
425 let config = Config::parse_from(
426 ["detcore", "--replay-preemptions-from=unused"]
427 .into_iter()
428 .chain(flags),
429 );
430 let flag = flags[1];
431 let before = serde_json::to_value(&config).unwrap();
432 let refusal =
433 refuse_unqualified_annotated_replay(&config, Backend::Ptrace).unwrap_err();
434 assert_eq!(refusal.setting, setting, "{flag}");
435 assert_eq!(serde_json::to_value(&config).unwrap(), before);
436 }
437 }
438
439 #[test]
446 fn a_library_launch_of_an_annotated_history_is_judged_on_its_own_configuration() {
447 let unprepared = Config {
448 annotated_priority_replay: true,
449 ..Config::parse_from([
450 "detcore",
451 "--sequentialize-threads",
452 "--max-timeslice=1000000",
453 "--replay-preemptions-from=unused",
454 ])
455 };
456 let refusal = refuse_unqualified_annotated_launch(&unprepared).unwrap_err();
457 assert_eq!(
458 refusal.setting,
459 "cannot deliver Detcore preemption timer events"
460 );
461 let message = refusal.to_string();
462 assert!(
463 message.starts_with("HERMIT_CALLBACK_CHECKPOINT_UNQUALIFIED_MODE")
464 && message.contains("the launch configuration cannot deliver"),
465 "{message}"
466 );
467 let prepared = Config {
468 backend: crate::backend_capabilities(Backend::Ptrace),
469 ..unprepared.clone()
470 };
471 refuse_unqualified_annotated_launch(&prepared).unwrap();
472 let unsequentialized = Config {
473 sequentialize_threads: false,
474 ..prepared.clone()
475 };
476 let before = serde_json::to_value(&unsequentialized).unwrap();
477 assert_eq!(
478 refuse_unqualified_annotated_launch(&unsequentialized)
479 .unwrap_err()
480 .setting,
481 "does not sequentialize threads (--no-sequentialize-threads)"
482 );
483 assert_eq!(serde_json::to_value(&unsequentialized).unwrap(), before);
484 let unannotated = Config {
485 annotated_priority_replay: false,
486 ..unsequentialized
487 };
488 refuse_unqualified_annotated_launch(&unannotated).unwrap();
489 }
490
491 #[test]
498 fn preemption_stack_traces_need_guest_backtraces() {
499 for (args, setting) in [
500 (vec!["--preemption-stacktrace"], "--preemption-stacktrace"),
501 (
502 vec!["--preemption-stacktrace-log-file=unused"],
503 "--preemption-stacktrace-log-file",
504 ),
505 ] {
506 let config = Config::parse_from(["detcore", "--chaos"].into_iter().chain(args));
507 let before = serde_json::to_value(&config).unwrap();
508 let error =
509 refuse_unsupported_preemption(&config, Backend::Kvm, true, false).unwrap_err();
510 assert_eq!(error.setting, setting);
511 assert_eq!(error.missing, MissingCapability::GuestBacktraces);
512 let message = error.to_string();
513 assert!(
514 message.starts_with(&format!(
515 "unsupported_preemption: --backend=kvm refuses {setting}: this runtime \
516 cannot unwind guest stacks"
517 )),
518 "{message}"
519 );
520 assert!(
521 message.ends_with("; the guest has not started"),
522 "{message}"
523 );
524 assert_eq!(serde_json::to_value(&config).unwrap(), before);
525 refuse_unsupported_preemption(&config, Backend::Ptrace, true, false).unwrap();
526 refuse_unsupported_preemption_with_capabilities(
527 &config,
528 "e9patch",
529 reverie::BackendCapabilities::PTRACE,
530 true,
531 false,
532 )
533 .unwrap();
534 for (backend, capabilities) in TIMERLESS {
538 let error = refuse_unsupported_preemption_with_capabilities(
539 &config,
540 backend,
541 capabilities,
542 false,
543 false,
544 )
545 .unwrap_err();
546 assert_eq!(error.setting, setting, "{backend:?}");
547 assert_eq!(error.missing, MissingCapability::TimerEvents, "{backend:?}");
548 }
549 }
550 }
551
552 #[test]
556 fn kvm_accepts_every_timer_request_but_the_stack_traces() {
557 for args in [
558 vec!["--max-timeslice=1000"],
559 vec!["--preemption-timeout=1000"],
560 vec!["--max-timeslice=disabled", "--imprecise-timers"],
561 vec!["--max-timeslice=disabled", "--interrupt-at=3:1000"],
562 vec!["--panic-on-rcb-overshoot"],
563 vec!["--replay-preemptions-from=unused"],
564 vec!["--chaos", "--chaos-timeslice-range=1000..2000"],
565 ] {
566 let config = Config::parse_from(["detcore"].into_iter().chain(args.iter().copied()));
567 if let Err(error) = refuse_unsupported_preemption(&config, Backend::Kvm, true, true) {
568 panic!("{args:?}: {error}");
569 }
570 }
571 }
572
573 #[test]
574 fn cooperative_and_boundary_event_settings_do_not_require_a_timer() {
575 for disabled in [
576 "--max-timeslice=disabled",
577 "--max-timeslice=0",
578 "--preemption-timeout=0",
579 ] {
580 let config = Config::parse_from([
581 "detcore",
582 disabled,
583 "--chaos",
584 "--chaos-per-thread-slowdown",
585 "--target-timeslice=1000",
586 "--target-timeslice-syscalls-only",
587 "--record-preemptions-to=unused",
588 "--replay-schedule-from=unused-schedule",
589 "--stacktrace-event=1",
590 "--chaos-slowdown-max-factor=4.5",
591 "--chaos-epoch-length-ns=1000",
592 ]);
593 for (backend, capabilities) in TIMERLESS {
594 refuse_unsupported_preemption_with_capabilities(
595 &config,
596 backend,
597 capabilities,
598 true,
599 false,
600 )
601 .unwrap();
602 }
603 }
604 }
605}