1use anyhow::bail;
14use detcore_model::futex::MmId;
15use detcore_model::schedule::InstructionPointer;
16use detcore_model::schedule::boundary::Boundary;
17use detcore_model::schedule::boundary::Cause;
18use detcore_model::schedule::boundary::Entry;
19use detcore_model::schedule::boundary::Identity;
20use detcore_model::schedule::boundary::Owner;
21use detcore_model::schedule::boundary::Phase;
22use reverie::syscalls::Sysno;
23use serde::Deserialize;
24use serde::Serialize;
25
26use crate::ThreadState;
27use crate::config::Config;
28use crate::scheduler::Priority;
29
30pub mod grant;
33
34pub(crate) mod lifecycle;
36
37pub(crate) mod callbacks;
39
40#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
44pub struct Observations {
45 owner: Owner,
46 mm: MmId,
47 checkin: u64,
48 completed_slices: u64,
49 syscall_occurrence: u64,
50 signal_entry_occurrence: u64,
51 active_syscall: Option<(Sysno, u64)>,
52 current: Option<(Entry, Phase, u64)>,
53 expected_budget: Option<grant::Budget>,
54 pending_start_budget: Option<(grant::Budget, u64)>,
55 start_reset_done: bool,
56}
57
58#[derive(Debug)]
63pub struct ClosedSlice {
64 pub boundary: Boundary,
66 pub next_priority: Option<Priority>,
68}
69
70impl Observations {
71 fn from_owner<T>(thread: &ThreadState<T>, owner: Owner) -> anyhow::Result<Self> {
74 if owner.tid != thread.dettid
75 || owner.birth_tid.as_raw() <= 0
76 || owner.address_space_creator != thread.mm_id.creator()
77 || owner.address_space_generation != thread.mm_id.generation()
78 {
79 bail!("slice-boundary protocol refusal: lifecycle reply does not name this image");
80 }
81 let mut state = Self::new(thread, owner.thread_generation);
82 state.owner = owner;
83 Ok(state)
84 }
85
86 pub fn new<T>(thread: &ThreadState<T>, thread_generation: u64) -> Self {
90 Self {
91 owner: Owner {
92 tid: thread.dettid,
93 thread_generation,
94 birth_tid: thread.dettid,
95 birth_generation: thread_generation,
96 address_space_creator: thread.mm_id.creator(),
97 address_space_generation: thread.mm_id.generation(),
98 },
99 mm: thread.mm_id,
100 checkin: 0,
101 completed_slices: 0,
102 syscall_occurrence: 0,
103 signal_entry_occurrence: 0,
104 active_syscall: None,
105 current: None,
106 expected_budget: None,
107 pending_start_budget: None,
108 start_reset_done: false,
109 }
110 }
111
112 fn require_owner<T>(&self, thread: &ThreadState<T>) -> anyhow::Result<()> {
113 if self.owner.tid != thread.dettid || self.mm != thread.mm_id {
114 bail!(
115 "slice-boundary protocol refusal: stale observation owner {:?}",
116 self.owner
117 );
118 }
119 Ok(())
120 }
121
122 fn publish<T>(
123 &mut self,
124 thread: &ThreadState<T>,
125 entry: Entry,
126 phase: Phase,
127 ) -> anyhow::Result<()> {
128 self.require_owner(thread)?;
129 let checkin = self.checkin.checked_add(1).ok_or_else(|| {
130 anyhow::anyhow!("slice-boundary protocol refusal: semantic check-in overflow")
131 })?;
132 if self
133 .current
134 .is_some_and(|(_, _, rcbs)| thread.committed_clock_value < rcbs)
135 {
136 bail!("slice-boundary protocol refusal: committed guest RCB count moved backwards");
137 }
138 self.checkin = checkin;
139 self.current = Some((entry, phase, thread.committed_clock_value));
140 Ok(())
141 }
142
143 pub fn syscall<T>(
147 &mut self,
148 thread: &ThreadState<T>,
149 syscall: Sysno,
150 phase: Phase,
151 ) -> anyhow::Result<()> {
152 let occurrence = match phase {
153 Phase::Prehook => self.syscall_occurrence.checked_add(1).ok_or_else(|| {
154 anyhow::anyhow!("slice-boundary protocol refusal: syscall occurrence overflow")
155 })?,
156 Phase::Posthook => match self.active_syscall {
157 Some((active, occurrence)) if active == syscall => occurrence,
158 _ => {
159 bail!("slice-boundary protocol refusal: syscall posthook has no matching entry")
160 }
161 },
162 Phase::TimerMaximum => bail!("slice-boundary protocol refusal: syscall at timer body"),
163 };
164 self.publish(
165 thread,
166 Entry::Syscall {
167 syscall,
168 occurrence,
169 },
170 phase,
171 )?;
172 match phase {
173 Phase::Prehook => {
174 self.syscall_occurrence = occurrence;
175 self.active_syscall = Some((syscall, occurrence));
176 }
177 Phase::Posthook => self.active_syscall = None,
178 Phase::TimerMaximum => unreachable!(),
179 }
180 Ok(())
181 }
182
183 pub fn callback<T>(
187 &mut self,
188 thread: &ThreadState<T>,
189 entry: Entry,
190 phase: Phase,
191 ) -> anyhow::Result<()> {
192 match entry {
193 Entry::Syscall { .. } => {
194 bail!("slice-boundary protocol refusal: use syscall occurrence allocator")
195 }
196 Entry::Signal {
197 signal, delivery, ..
198 } if delivery == 0 || !(1..=64).contains(&signal.raw()) => {
199 bail!("slice-boundary protocol refusal: invalid logical signal delivery");
200 }
201 _ => self.publish(thread, entry, phase),
202 }
203 }
204
205 pub fn close_slice<T>(
209 &mut self,
210 thread: &mut ThreadState<T>,
211 config: &Config,
212 explicit_yield: bool,
213 rip: Option<InstructionPointer>,
214 ) -> anyhow::Result<ClosedSlice> {
215 let identity = self.current_identity(thread, explicit_yield)?;
216 let boundary = Boundary {
217 identity,
218 count: 1,
219 start_rip: None,
220 end_rip: rip,
221 end_time: Some(thread.thread_logical_time.as_nanos()),
222 };
223 if let Some(expected) = self.expected_budget
224 && expected.expected() != boundary.identity
225 {
226 bail!(
227 "slice-boundary protocol refusal at stored index {}: expected {:?}, observed {:?}",
228 expected.stored_index(),
229 expected.expected(),
230 boundary.identity
231 );
232 }
233 let next_priority = thread.next_timeslice(config);
234 self.completed_slices = identity.slice;
235 self.expected_budget = None;
236 Ok(ClosedSlice {
237 boundary,
238 next_priority,
239 })
240 }
241
242 fn current_identity<T>(
243 &self,
244 thread: &ThreadState<T>,
245 explicit_yield: bool,
246 ) -> anyhow::Result<Identity> {
247 self.require_owner(thread)?;
248 if thread.own_signal_turn_hold.is_some() {
249 bail!("slice-boundary protocol refusal: boundary conflicts with own-signal turn hold");
250 }
251 let Some((entry, phase, rcbs)) = self.current else {
252 bail!("slice-boundary protocol refusal: slice end has no semantic check-in");
253 };
254 if rcbs != thread.committed_clock_value {
255 bail!("slice-boundary protocol refusal: guest progressed beyond the observed check-in");
256 }
257 let cause = if explicit_yield {
258 if !matches!(
259 entry,
260 Entry::Syscall {
261 syscall: Sysno::sched_yield,
262 ..
263 }
264 ) {
265 bail!("slice-boundary protocol refusal: explicit yield at a different syscall");
266 }
267 Cause::ExplicitSchedYield
268 } else {
269 match entry {
270 Entry::PmuTimer => Cause::PmuTimer,
271 Entry::NoRcbTimer => Cause::NoRcbTimer,
272 _ => Cause::SoftwareCheckin,
273 }
274 };
275 let slice = self.completed_slices.checked_add(1).ok_or_else(|| {
276 anyhow::anyhow!("slice-boundary protocol refusal: slice occurrence overflow")
277 })?;
278 Ok(Identity {
279 owner: self.owner,
280 cause,
281 phase,
282 entry,
283 committed_rcbs: rcbs,
284 checkin: self.checkin,
285 slice,
286 })
287 }
288
289 fn replay_expiration<T>(&self, thread: &ThreadState<T>) -> anyhow::Result<Option<bool>> {
293 let Some(budget) = self.expected_budget else {
294 return Ok(None);
295 };
296 self.require_owner(thread)?;
297 let expected = budget.expected();
298 if self.checkin > expected.checkin || thread.committed_clock_value > expected.committed_rcbs
299 {
300 bail!(
301 "slice-boundary protocol refusal at stored index {}: passed expected {:?}; actual check-in {}, guest RCBs {}",
302 budget.stored_index(),
303 expected,
304 self.checkin,
305 thread.committed_clock_value
306 );
307 }
308 if self.checkin < expected.checkin {
309 return Ok(Some(false));
310 }
311 let observed = self.current_identity(thread, false)?;
312 let mut location = expected;
316 if expected.cause == Cause::ExplicitSchedYield {
317 location.cause = Cause::SoftwareCheckin;
318 }
319 if observed != location {
320 bail!(
321 "slice-boundary protocol refusal at stored index {}: expected {:?}, observed {:?}",
322 budget.stored_index(),
323 expected,
324 observed
325 );
326 }
327 Ok(Some(expected.cause != Cause::ExplicitSchedYield))
328 }
329}
330
331pub(crate) fn replay_expiration<T>(thread: &ThreadState<T>) -> anyhow::Result<Option<bool>> {
332 match &thread.slice_boundary {
333 Some(observed) => observed.replay_expiration(thread),
334 None => Ok(None),
335 }
336}
337
338pub(crate) fn no_rcb_maximum<T>(thread: &mut ThreadState<T>) -> anyhow::Result<()> {
339 if let Some(mut observed) = thread.slice_boundary.take() {
340 let result = observed.publish(thread, Entry::NoRcbTimer, Phase::TimerMaximum);
341 thread.slice_boundary = Some(observed);
342 result?;
343 }
344 Ok(())
345}
346
347pub(crate) fn adopt_owner<T>(
352 thread: &mut ThreadState<T>,
353 owner: Owner,
354 starts_thread: bool,
355) -> anyhow::Result<()> {
356 let candidate = Observations::from_owner(thread, owner)?;
357 if let Some(prior) = &thread.slice_boundary {
358 if prior.owner == owner {
359 return prior.require_owner(thread);
360 }
361 if (prior.owner.birth_tid, prior.owner.birth_generation)
362 != (owner.birth_tid, owner.birth_generation)
363 || prior.owner.address_space_generation.checked_add(1)
364 != Some(owner.address_space_generation)
365 || prior.mm == thread.mm_id
366 {
367 bail!("slice-boundary protocol refusal: lifecycle reply replaced a different thread");
368 }
369 } else if !starts_thread {
370 bail!("slice-boundary protocol refusal: first owner did not arrive through startup");
371 }
372 thread.slice_boundary = Some(candidate);
373 thread.replay_rcb_end = None;
375 Ok(())
376}
377
378#[cfg(test)]
379mod tests {
380 use std::num::NonZeroU64;
381 use std::time::Duration;
382
383 use detcore_model::schedule::boundary::Record;
384 use detcore_model::schedule::boundary::Schedule;
385
386 use super::*;
387 use crate::types::DetTid;
388
389 fn fixture() -> (Config, ThreadState<()>, Observations) {
390 let cfg = Config {
391 max_timeslice: NonZeroU64::new(200_000_000),
392 ..Default::default()
393 };
394 let mut thread = ThreadState::new(DetTid::from_raw(3), &cfg, ());
395 thread.next_timeslice(&cfg);
396 let observations = Observations::new(&thread, 0);
397 (cfg, thread, observations)
398 }
399
400 #[test]
401 fn shared_and_copied_clone_address_spaces_are_distinct_in_the_stored_identity() {
402 let cfg = Config::default();
403 let parent = DetTid::from_raw(3);
404 let child = DetTid::from_raw(4);
405 let mut shared = ThreadState::new(child, &cfg, ());
406 shared.mm_id = MmId::for_clone(MmId::initial(parent), child, true);
407 let mut copied = ThreadState::new(child, &cfg, ());
408 copied.mm_id = MmId::for_clone(MmId::initial(parent), child, false);
409 let mut a = Observations::new(&shared, 0);
410 let mut b = Observations::new(&copied, 0);
411 a.callback(&shared, Entry::Cpuid, Phase::Prehook).unwrap();
412 b.callback(&copied, Entry::Cpuid, Phase::Prehook).unwrap();
413 let a = a
414 .close_slice(&mut shared, &cfg, false, None)
415 .unwrap()
416 .boundary;
417 let b = b
418 .close_slice(&mut copied, &cfg, false, None)
419 .unwrap()
420 .boundary;
421 assert_ne!(a.identity.owner, b.identity.owner);
422 let schedule = Schedule::new(vec![Record::TimesliceEnd(a)]).unwrap();
423 let mut cursor = schedule.cursor();
424 assert!(cursor.observe(&Record::TimesliceEnd(b)).is_err());
425 assert_eq!(cursor.stored_index(), 0);
426 }
427
428 #[test]
429 fn actual_reset_snapshots_completed_slice_without_moving_guest_clock() {
430 let (cfg, mut thread, mut observed) = fixture();
431 thread.committed_clock_value = 71;
432 thread.thread_logical_time.add_rcbs(71);
433 thread.stats.count_syscall(Sysno::write);
434 observed
435 .syscall(&thread, Sysno::write, Phase::Prehook)
436 .unwrap();
437 let before_clock = thread.thread_logical_time.clone();
438 let before_slices = thread.stats.timeslice_count;
439 let closed = observed
440 .close_slice(&mut thread, &cfg, false, None)
441 .unwrap();
442 assert_eq!(closed.boundary.identity.committed_rcbs, 71);
443 assert_eq!(closed.boundary.identity.slice, 1);
444 assert_eq!(closed.boundary.end_time, Some(before_clock.as_nanos()));
445 assert_eq!(
446 thread.thread_logical_time.as_nanos(),
447 before_clock.as_nanos()
448 );
449 assert_eq!(thread.committed_clock_value, 71);
450 assert_eq!(thread.stats.timeslice_count, before_slices + 1);
451 assert_eq!(thread.stats.timeslice_syscall_count, 0);
452 assert_eq!(
453 thread.max_timeslice_end,
454 Some(before_clock.as_nanos() + Duration::from_nanos(200_000_000))
455 );
456 }
457
458 #[test]
459 fn adjacent_zero_rcb_resets_and_repeated_syscalls_have_distinct_identities() {
460 let (cfg, mut thread, mut observed) = fixture();
461 observed
462 .syscall(&thread, Sysno::write, Phase::Prehook)
463 .unwrap();
464 let first = observed
465 .close_slice(&mut thread, &cfg, false, None)
466 .unwrap()
467 .boundary;
468 let second = observed
469 .close_slice(&mut thread, &cfg, false, None)
470 .unwrap()
471 .boundary;
472 assert_eq!(first.identity.checkin, second.identity.checkin);
473 assert_eq!(
474 first.identity.committed_rcbs,
475 second.identity.committed_rcbs
476 );
477 assert_ne!(first.identity.slice, second.identity.slice);
478 observed
479 .syscall(&thread, Sysno::write, Phase::Posthook)
480 .unwrap();
481 observed
482 .syscall(&thread, Sysno::write, Phase::Prehook)
483 .unwrap();
484 let third = observed
485 .close_slice(&mut thread, &cfg, false, None)
486 .unwrap()
487 .boundary;
488 assert_ne!(first.identity.entry, third.identity.entry);
489 Schedule::new(vec![
490 Record::TimesliceEnd(first),
491 Record::TimesliceEnd(second),
492 Record::TimesliceEnd(third),
493 ])
494 .unwrap();
495 }
496
497 #[test]
498 fn signal_hold_stale_exec_and_wrong_callback_refuse_before_reset() {
499 for bad in 0..5 {
500 let (cfg, mut thread, mut observed) = fixture();
501 observed
502 .syscall(&thread, Sysno::write, Phase::Prehook)
503 .unwrap();
504 match bad {
505 0 => thread.own_signal_turn_hold = Some(libc::SIGALRM),
506 1 => thread.mm_id = thread.mm_id.for_exec(thread.dettid),
507 2 => thread.committed_clock_value += 1,
508 3 => observed.completed_slices = u64::MAX,
509 4 => {}
510 _ => unreachable!(),
511 }
512 let before = observed.clone();
513 let slices = thread.stats.timeslice_count;
514 let end = thread.end_of_timeslice;
515 let max = thread.max_timeslice_end;
516 let clock = thread.thread_logical_time.as_nanos();
517 assert!(
518 observed
519 .close_slice(&mut thread, &cfg, bad == 4, None)
520 .is_err()
521 );
522 assert_eq!(observed, before);
523 assert_eq!(thread.stats.timeslice_count, slices);
524 assert_eq!(thread.end_of_timeslice, end);
525 assert_eq!(thread.max_timeslice_end, max);
526 assert_eq!(thread.thread_logical_time.as_nanos(), clock);
527 }
528 }
529
530 #[test]
531 fn pmu_no_rcb_and_software_callbacks_are_not_inferred_from_rip() {
532 for (entry, cause) in [
533 (Entry::Cpuid, Cause::SoftwareCheckin),
534 (Entry::PmuTimer, Cause::PmuTimer),
535 (Entry::NoRcbTimer, Cause::NoRcbTimer),
536 ] {
537 let (cfg, mut thread, mut observed) = fixture();
538 observed.callback(&thread, entry, Phase::Prehook).unwrap();
539 let closed = observed
540 .close_slice(&mut thread, &cfg, false, None)
541 .unwrap();
542 assert_eq!(closed.boundary.identity.cause, cause);
543 assert_eq!(closed.boundary.end_rip, None);
544 }
545 }
546
547 #[test]
548 fn posthook_and_counter_overflow_fail_without_advancing_observation() {
549 let (_, thread, mut observed) = fixture();
550 let before = observed.clone();
551 assert!(
552 observed
553 .syscall(&thread, Sysno::read, Phase::Posthook)
554 .is_err()
555 );
556 assert_eq!(observed, before);
557 observed.checkin = u64::MAX;
558 let before = observed.clone();
559 assert!(
560 observed
561 .syscall(&thread, Sysno::read, Phase::Prehook)
562 .is_err()
563 );
564 assert_eq!(observed, before);
565 }
566
567 #[test]
568 fn boundary_software_expiry_requires_actual_entry_phase_rcbs_and_occurrence() {
569 let (config, mut thread, mut observed) = fixture();
570 observed
571 .syscall(&thread, Sysno::poll, Phase::Prehook)
572 .unwrap();
573 let mut recorded_thread = ThreadState::new(thread.dettid, &config, ());
574 let mut recorded = observed.clone();
575 recorded
576 .syscall(&recorded_thread, Sysno::poll, Phase::Posthook)
577 .unwrap();
578 let marker = recorded
579 .close_slice(&mut recorded_thread, &config, false, None)
580 .unwrap()
581 .boundary;
582 let schedule = Schedule::new(vec![Record::TimesliceEnd(marker)]).unwrap();
583 let hint = grant::Budget::at(&schedule, 0).unwrap();
584 thread.slice_boundary = Some(observed);
585 grant::install(&mut thread, hint).unwrap();
586 let clocks = (
587 thread.committed_clock_value,
588 thread.thread_logical_time.as_nanos(),
589 thread.end_of_timeslice,
590 thread.max_timeslice_end,
591 );
592 assert_eq!(replay_expiration(&thread).unwrap(), Some(false));
593 let mut observed = thread.slice_boundary.take().unwrap();
594 observed
595 .syscall(&thread, Sysno::poll, Phase::Posthook)
596 .unwrap();
597 thread.slice_boundary = Some(observed);
598 assert_eq!(replay_expiration(&thread).unwrap(), Some(true));
599 assert_eq!(
600 clocks,
601 (
602 thread.committed_clock_value,
603 thread.thread_logical_time.as_nanos(),
604 thread.end_of_timeslice,
605 thread.max_timeslice_end
606 )
607 );
608 for field in 0..5 {
609 let mut bad = thread.slice_boundary.clone().unwrap();
610 match field {
611 0 => bad.checkin += 1,
612 1 => bad.current.as_mut().unwrap().0 = Entry::Cpuid,
613 2 => bad.current.as_mut().unwrap().1 = Phase::Prehook,
614 3 => bad.owner.birth_generation += 1,
615 4 => bad.current.as_mut().unwrap().2 += 1,
616 _ => unreachable!(),
617 }
618 assert!(bad.replay_expiration(&thread).is_err(), "field {field}");
619 }
620 thread.own_signal_turn_hold = Some(libc::SIGUSR1);
621 assert!(replay_expiration(&thread).is_err());
622 }
623
624 #[test]
625 fn boundary_explicit_yield_is_consumed_only_by_its_original_handler() {
626 let (config, mut thread, mut observed) = fixture();
627 observed
628 .syscall(&thread, Sysno::sched_yield, Phase::Prehook)
629 .unwrap();
630 let mut copy = observed.clone();
631 let mut record_thread = ThreadState::new(thread.dettid, &config, ());
632 let marker = copy
633 .close_slice(&mut record_thread, &config, true, None)
634 .unwrap()
635 .boundary;
636 let schedule = Schedule::new(vec![Record::TimesliceEnd(marker)]).unwrap();
637 thread.slice_boundary = Some(observed);
638 grant::install(&mut thread, grant::Budget::at(&schedule, 0).unwrap()).unwrap();
639 assert_eq!(replay_expiration(&thread).unwrap(), Some(false));
640 let mut observed = thread.slice_boundary.take().unwrap();
641 let before = thread.stats.timeslice_count;
642 assert!(
643 observed
644 .close_slice(&mut thread, &config, false, None)
645 .is_err()
646 );
647 assert_eq!(thread.stats.timeslice_count, before);
648 observed
649 .close_slice(&mut thread, &config, true, None)
650 .unwrap();
651 assert_eq!(thread.stats.timeslice_count, before + 1);
652 }
653
654 #[test]
655 fn boundary_no_rcb_maximum_is_distinct_from_its_earlier_prehook() {
656 let (config, mut thread, mut observed) = fixture();
657 observed
658 .callback(&thread, Entry::NoRcbTimer, Phase::Prehook)
659 .unwrap();
660 let mut copy = observed.clone();
661 let mut record_thread = ThreadState::new(thread.dettid, &config, ());
662 copy.publish(&record_thread, Entry::NoRcbTimer, Phase::TimerMaximum)
663 .unwrap();
664 let marker = copy
665 .close_slice(&mut record_thread, &config, false, None)
666 .unwrap()
667 .boundary;
668 let schedule = Schedule::new(vec![Record::TimesliceEnd(marker)]).unwrap();
669 thread.slice_boundary = Some(observed);
670 grant::install(&mut thread, grant::Budget::at(&schedule, 0).unwrap()).unwrap();
671 assert_eq!(replay_expiration(&thread).unwrap(), Some(false));
672 no_rcb_maximum(&mut thread).unwrap();
673 assert_eq!(replay_expiration(&thread).unwrap(), Some(true));
674 let mut observed = thread.slice_boundary.take().unwrap();
675 assert_eq!(
676 observed
677 .close_slice(&mut thread, &config, false, None)
678 .unwrap()
679 .boundary
680 .identity
681 .phase,
682 Phase::TimerMaximum
683 );
684 }
685}