Skip to main content

hermit/
preemption_admission.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//! Refuse timer requests that the selected runtime cannot implement.
10//!
11//! This is launch policy, not a timer implementation or a replacement for
12//! Detcore's scheduling policy. Cooperative scheduling at existing guest
13//! events remains available without a preemption timer. Each refusal is keyed
14//! on a capability the backend states, never on its name: a runtime that
15//! delivers no preemption timer events refuses every timer request, and one
16//! that delivers them but cannot unwind guest stacks refuses the requests to
17//! print a stack at each preemption.
18
19use detcore::Config;
20
21use crate::Backend;
22
23/// A named, pre-guest refusal of an unsupported preemption setting.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct UnsupportedPreemption {
26    backend: &'static str,
27    setting: &'static str,
28    missing: MissingCapability,
29}
30
31/// The backend capability whose absence refuses a setting.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum MissingCapability {
34    /// `delivers_preemption_timer_events`: the setting needs timer events.
35    TimerEvents,
36    /// `provides_guest_backtraces`: the setting prints a guest stack at each
37    /// preemption, which this runtime cannot unwind.
38    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
75/// Check command-line requests before launching a guest or consuming input.
76/// `maximum_requested` records parser provenance, including config-file and
77/// `--from` inputs. The inherited maximum is not a timer request: it also
78/// controls shared clock accounting and cooperative scheduling, and stays
79/// unchanged. `skid_margin_requested` represents the CLI-only PMU setting.
80///
81/// This check never rewrites the configuration. Library APIs without command-
82/// line provenance retain their existing admission behavior.
83/// A schedule-event recording or cooperative target alone does not request a
84/// timer. Preemption-point replay does, and is refused without inspecting or
85/// silently reducing the requested recording to a cooperative subset.
86///
87/// A runtime that delivers timer events but cannot unwind guest stacks
88/// (`provides_guest_backtraces` false, as on KVM) accepts every timer request
89/// except the two that print a stack at each preemption, which would print
90/// none there.
91pub 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
106/// The same admission for an executable's statically declared physical adapter.
107/// The name is diagnostic identity; the supplied capabilities determine policy.
108/// This never launches a runtime or changes the requested configuration.
109pub 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        // The range sets the maximum, a timer request in its own right.
135        "--chaos-timeslice-range"
136    } else if config.chaos_timeslice.is_some() {
137        // A replayed record's drawn maximum binds the replay as much.
138        "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/// A named, pre-guest refusal of a configuration that cannot replay a
166/// callback-annotated priority history (`Config::annotated_priority_replay`).
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct UnqualifiedAnnotatedReplay {
169    /// The selected backend for a command line; `None` for a library launch,
170    /// whose configuration is judged on the capabilities it carries.
171    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
193/// Refuse to replay a callback-annotated priority history where its row ends
194/// cannot be honored: they are actual callback endpoints at exact RCB counts,
195/// guarded one RCB past each. The requirements are
196/// `Config::annotated_priority_replay_unqualified`'s, judged here on the
197/// command line's configuration with the selected backend's capabilities, so
198/// the refusal comes before input is read and names the backend. Without
199/// them a replay would neither reach nor bound those endpoints, and silently
200/// falling back to the legacy expiry would replay the wrong schedule. A
201/// record without checkpoints keeps its existing admission; this check never
202/// rewrites the configuration.
203pub 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
214/// Preserve the annotated-replay checks for a statically selected adapter.
215pub 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
233/// The library launches' backstop for the same requirements, on the
234/// prepared configuration a backend is about to run: a library caller passes
235/// no command line, and its configuration reaches here unqualified. An
236/// annotated configuration must meet `annotated_priority_replay_unqualified`
237/// on the capabilities it carries. Its record is not reread here: Detcore
238/// refuses a record whose checkpoints disagree with the flag before any
239/// thread starts. Nothing is changed or forced.
240pub 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    // KVM left this set with Reverie 9443a6b2, which delivers preemption
262    // timer events at exact retired-branch counts.
263    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    /// A callback-annotated priority history replays only with precise PMU
375    /// preemption timer events and RCB time. Every other configuration is
376    /// refused by name, without rewriting it; the qualified one passes.
377    #[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        // KVM delivers precise timer events too.
388        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    /// A library launch is judged on the prepared configuration's own
440    /// capabilities: an annotated configuration nobody prepared (no timer
441    /// events), or one without sequentialized threads, is refused by name,
442    /// without being changed; the same configuration with ptrace's
443    /// capabilities and every requirement met passes, and an unannotated
444    /// configuration keeps its existing admission.
445    #[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    /// KVM delivers timer events but cannot unwind guest stacks. It refuses
492    /// the two requests that print a stack at each preemption by name, for
493    /// its missing backtraces rather than for timer events, without rewriting
494    /// the configuration; ptrace and e9patch preprocessing, which unwind,
495    /// accept them, and the timerless backends still refuse them for their
496    /// missing timer events.
497    #[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            // The inherited maximum is not a request (`maximum_requested`
535            // false), so the stack-trace flag is what a timerless runtime
536            // refuses, for its missing timer events.
537            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    /// Every other timer request runs on KVM, as on ptrace: an explicit or
553    /// replayed maximum, imprecise timers, interrupt points, an overshoot
554    /// panic, a skid margin, preemption-point replay and a chaos range.
555    #[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}