1use std::collections::BTreeMap;
13
14use anyhow::bail;
15use detcore_model::futex::MmId;
16use detcore_model::pid::DetTid;
17use detcore_model::schedule::boundary::Boundary;
18use detcore_model::schedule::boundary::Cause;
19use detcore_model::schedule::boundary::Identity;
20use detcore_model::schedule::boundary::Owner;
21use detcore_model::schedule::boundary::Record;
22use detcore_model::schedule::boundary::Schedule;
23use serde::Deserialize;
24use serde::Serialize;
25
26use crate::ThreadState;
27use crate::config::Config;
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
32#[serde(try_from = "Wire", into = "Wire")]
33pub struct Budget {
34 stored_index: usize,
35 expected: Identity,
36}
37
38#[derive(Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40struct Wire {
41 stored_index: usize,
42 expected: Identity,
43}
44
45impl TryFrom<Wire> for Budget {
46 type Error = detcore_model::schedule::boundary::Error;
47
48 fn try_from(wire: Wire) -> Result<Self, Self::Error> {
49 Boundary {
50 identity: wire.expected,
51 count: 1,
52 start_rip: None,
53 end_rip: None,
54 end_time: None,
55 }
56 .validate_shape(wire.stored_index)?;
57 Ok(Self {
58 stored_index: wire.stored_index,
59 expected: wire.expected,
60 })
61 }
62}
63
64impl From<Budget> for Wire {
65 fn from(budget: Budget) -> Self {
66 Self {
67 stored_index: budget.stored_index,
68 expected: budget.expected,
69 }
70 }
71}
72
73impl Budget {
74 pub fn at(schedule: &Schedule, index: usize) -> anyhow::Result<Self> {
76 let Some(Record::TimesliceEnd(boundary)) = schedule.records().get(index) else {
77 bail!("slice-boundary protocol refusal: budget index {index} is not a boundary");
78 };
79 Ok(Self {
80 stored_index: index,
81 expected: boundary.identity,
82 })
83 }
84
85 pub fn expected(self) -> Identity {
88 self.expected
89 }
90
91 pub fn stored_index(self) -> usize {
93 self.stored_index
94 }
95}
96
97#[derive(Default, Debug)]
101pub struct Mailbox {
102 pending: BTreeMap<DetTid, (MmId, Budget, bool)>,
103}
104
105impl Mailbox {
106 pub(crate) fn retire(&mut self, tid: DetTid, mm: MmId) {
109 if self
110 .pending
111 .get(&tid)
112 .is_some_and(|(owner_mm, _, _)| *owner_mm == mm)
113 {
114 self.pending.remove(&tid);
115 }
116 }
117
118 pub fn queue(&mut self, mm: MmId, budget: Budget) -> anyhow::Result<()> {
123 let owner = budget.expected.owner;
124 if owner.address_space_generation != mm.generation()
125 || owner.address_space_creator != mm.creator()
126 {
127 bail!("slice-boundary protocol refusal: budget address-space generation mismatch");
128 }
129 if let Some((prior_mm, prior, _)) = self.pending.get(&owner.tid) {
130 if *prior_mm == mm && *prior == budget {
131 return Ok(());
132 }
133 bail!("slice-boundary protocol refusal: duplicate outstanding grant budget");
134 }
135 self.pending.insert(owner.tid, (mm, budget, false));
136 Ok(())
137 }
138
139 pub fn committed_grant(&mut self, tid: DetTid) {
143 if let Some((_, _, committed)) = self.pending.get_mut(&tid) {
144 *committed = true;
145 }
146 }
147
148 pub fn finish(
151 &mut self,
152 tid: DetTid,
153 mm: MmId,
154 granted: bool,
155 terminal: bool,
156 ) -> anyhow::Result<Option<Budget>> {
157 if terminal {
158 self.retire(tid, mm);
159 return Ok(None);
160 }
161 let Some((owner_mm, budget, committed)) = self.pending.get(&tid) else {
162 return Ok(None);
163 };
164 if !granted || !committed {
165 return Ok(None);
166 }
167 if *owner_mm != mm {
168 bail!("slice-boundary protocol refusal: stale grant budget address space");
169 }
170 let budget = *budget;
171 self.pending.remove(&tid);
172 Ok(Some(budget))
173 }
174}
175
176pub fn install<T>(thread: &mut ThreadState<T>, budget: Budget) -> anyhow::Result<()> {
181 if thread
182 .slice_boundary
183 .as_ref()
184 .is_some_and(|state| state.pending_start_budget.is_some())
185 {
186 bail!(
187 "slice-boundary protocol refusal: ordinary grant cannot consume a staged start budget"
188 );
189 }
190 install_checked(thread, budget)
191}
192
193fn install_checked<T>(thread: &mut ThreadState<T>, budget: Budget) -> anyhow::Result<()> {
194 let budget = Budget::try_from(Wire {
195 stored_index: budget.stored_index,
196 expected: budget.expected,
197 })?;
198 let Some(observed) = &thread.slice_boundary else {
199 bail!("slice-boundary protocol refusal: grant to an uninitialized boundary owner");
200 };
201 observed.require_owner(thread)?;
202 let next_slice = observed.completed_slices.checked_add(1).ok_or_else(|| {
203 anyhow::anyhow!("slice-boundary protocol refusal: slice occurrence overflow")
204 })?;
205 if budget.expected.owner != observed.owner
206 || budget.expected.slice != next_slice
207 || budget.expected.checkin < observed.checkin
208 || budget.expected.committed_rcbs < thread.committed_clock_value
209 {
210 bail!("slice-boundary protocol refusal: stale or wrong-owner grant budget");
211 }
212 let precise =
213 (budget.expected.cause == Cause::PmuTimer).then_some(budget.expected.committed_rcbs);
214 if observed.expected_budget == Some(budget) && thread.replay_rcb_end == precise {
215 return Ok(());
216 }
217 if observed.expected_budget.is_some() || thread.replay_rcb_end.is_some() {
218 bail!("slice-boundary protocol refusal: grant would overwrite an active replay budget");
219 }
220 thread.slice_boundary.as_mut().unwrap().expected_budget = Some(budget);
221 thread.replay_rcb_end = precise;
222 Ok(())
223}
224
225pub fn stage_start<T>(thread: &mut ThreadState<T>, budget: Budget) -> anyhow::Result<()> {
228 let Some(observed) = &thread.slice_boundary else {
229 bail!("slice-boundary protocol refusal: start grant has no authenticated owner");
230 };
231 observed.require_owner(thread)?;
232 if observed.owner != budget.expected.owner
233 || observed.checkin != 0
234 || observed.completed_slices != 0
235 || observed.current.is_some()
236 || observed.expected_budget.is_some()
237 || observed.pending_start_budget.is_some()
238 || observed.start_reset_done
239 || thread.replay_rcb_end.is_some()
240 || budget.expected.slice != 1
241 || budget.expected.committed_rcbs < thread.committed_clock_value
242 {
243 bail!("slice-boundary protocol refusal: invalid or repeated start budget");
244 }
245 thread.slice_boundary.as_mut().unwrap().pending_start_budget =
246 Some((budget, thread.stats.timeslice_count));
247 Ok(())
248}
249
250#[derive(Debug)]
255pub(crate) struct StartReset {
256 tid: DetTid,
257 mm: MmId,
258 owner: Option<Owner>,
259 before: u64,
260 after: u64,
261}
262
263pub(crate) fn reset_start<T>(
267 thread: &mut ThreadState<T>,
268 cfg: &Config,
269) -> anyhow::Result<StartReset> {
270 let before = thread.stats.timeslice_count;
271 let owner = if let Some(observed) = &thread.slice_boundary {
272 observed.require_owner(thread)?;
273 if observed.start_reset_done
274 || observed.checkin != 0
275 || observed.completed_slices != 0
276 || observed.current.is_some()
277 || observed
278 .pending_start_budget
279 .is_some_and(|(_, staged_before)| staged_before != before)
280 {
281 bail!("slice-boundary protocol refusal: repeated or late startup reset");
282 }
283 Some(observed.owner)
284 } else {
285 None
286 };
287 thread.next_timeslice(cfg);
288 let after = thread.stats.timeslice_count;
289 let increment = u64::from(cfg.target_timeslice.or(cfg.max_timeslice).is_some());
290 if before.checked_add(increment) != Some(after) {
291 bail!("slice-boundary protocol refusal: startup reset changed unexpected statistics");
292 }
293 if let Some(observed) = &mut thread.slice_boundary {
294 observed.start_reset_done = true;
295 }
296 Ok(StartReset {
297 tid: thread.dettid,
298 mm: thread.mm_id,
299 owner,
300 before,
301 after,
302 })
303}
304
305pub(crate) fn finish_start<T>(
309 thread: &mut ThreadState<T>,
310 reset: StartReset,
311) -> anyhow::Result<()> {
312 if reset.tid != thread.dettid
313 || reset.mm != thread.mm_id
314 || reset.after != thread.stats.timeslice_count
315 || reset.owner != thread.slice_boundary.as_ref().map(|state| state.owner)
316 || thread
317 .slice_boundary
318 .as_ref()
319 .is_some_and(|state| !state.start_reset_done)
320 {
321 bail!("slice-boundary protocol refusal: missing or stale startup reset receipt");
322 }
323 let Some((budget, before)) = thread
324 .slice_boundary
325 .as_ref()
326 .and_then(|state| state.pending_start_budget)
327 else {
328 return Ok(());
329 };
330 if before != reset.before {
331 bail!("slice-boundary protocol refusal: start budget requires exactly one reset");
332 }
333 install_checked(thread, budget)?;
334 thread.slice_boundary.as_mut().unwrap().pending_start_budget = None;
335 Ok(())
336}
337
338#[cfg(test)]
339pub(crate) mod tests {
340 use detcore_model::schedule::boundary::Boundary;
341 use detcore_model::schedule::boundary::Entry;
342 use detcore_model::schedule::boundary::Owner;
343 use detcore_model::schedule::boundary::Phase;
344
345 use super::*;
346 use crate::config::Config;
347 use crate::slice_boundary::Observations;
348
349 #[test]
350 fn boundary_terminal_old_image_cannot_consume_replacement_budget() {
351 let tid = DetTid::from_raw(3);
352 let old = MmId::initial(tid);
353 let new = old.for_exec(tid);
354 let hint = budget(tid, new, 7);
355 let mut mailbox = Mailbox::default();
356 mailbox.queue(new, hint).unwrap();
357 mailbox.committed_grant(tid);
358 assert_eq!(mailbox.finish(tid, old, false, true).unwrap(), None);
359 assert_eq!(mailbox.finish(tid, new, true, false).unwrap(), Some(hint));
360 }
361
362 pub(crate) fn budget(tid: DetTid, mm: MmId, rcbs: u64) -> Budget {
363 let schedule = Schedule::new(vec![Record::TimesliceEnd(Boundary {
364 identity: Identity {
365 owner: Owner {
366 tid,
367 thread_generation: 0,
368 birth_tid: tid,
369 birth_generation: 0,
370 address_space_creator: mm.creator(),
371 address_space_generation: mm.generation(),
372 },
373 cause: Cause::PmuTimer,
374 phase: Phase::Prehook,
375 entry: Entry::PmuTimer,
376 committed_rcbs: rcbs,
377 checkin: 1,
378 slice: 1,
379 },
380 count: 1,
381 start_rip: None,
382 end_rip: None,
383 end_time: None,
384 })])
385 .unwrap();
386 Budget::at(&schedule, 0).unwrap()
387 }
388
389 #[test]
390 fn control_terminal_stale_exec_and_duplicate_grants_cannot_lose_or_reuse_a_budget() {
391 let tid = DetTid::from_raw(3);
392 let mm = MmId::initial(tid);
393 let expected = budget(tid, mm, 19);
394 let mut pending = Mailbox::default();
395 pending.queue(mm, expected).unwrap();
396 pending.queue(mm, expected).unwrap();
397 assert!(pending.queue(mm, budget(tid, mm, 20)).is_err());
398 assert_eq!(pending.finish(tid, mm, false, false).unwrap(), None);
399 assert_eq!(pending.finish(tid, mm, true, false).unwrap(), None);
402 pending.committed_grant(tid);
403 assert!(pending.finish(tid, mm.for_exec(tid), true, false).is_err());
404 assert_eq!(
405 pending.finish(tid, mm, true, false).unwrap(),
406 Some(expected)
407 );
408 assert_eq!(pending.finish(tid, mm, true, false).unwrap(), None);
409 pending.queue(mm, expected).unwrap();
410 assert_eq!(pending.finish(tid, mm, false, true).unwrap(), None);
411 assert_eq!(pending.finish(tid, mm, true, false).unwrap(), None);
412 }
413
414 #[test]
415 fn grant_is_guest_rcbs_not_turn_time_and_never_changes_deadline_or_maximum() {
416 let cfg = Config::default();
417 let tid = DetTid::from_raw(3);
418 let mut thread = ThreadState::new(tid, &cfg, ());
419 thread.next_timeslice(&cfg);
420 thread.slice_boundary = Some(Observations::new(&thread, 0));
421 thread.committed_clock_value = 7;
422 thread
423 .thread_logical_time
424 .advance_to(thread.thread_logical_time.as_nanos() + std::time::Duration::from_secs(5));
425 let before = (
426 thread.thread_logical_time.as_nanos(),
427 thread.end_of_timeslice,
428 thread.max_timeslice_end,
429 thread.stats.timeslice_count,
430 );
431 let hint = budget(tid, thread.mm_id, 19);
432 install(&mut thread, hint).unwrap();
433 assert_eq!(thread.replay_rcb_end, Some(19));
434 assert_eq!(thread.committed_clock_value, 7);
435 assert_eq!(
436 (
437 thread.thread_logical_time.as_nanos(),
438 thread.end_of_timeslice,
439 thread.max_timeslice_end,
440 thread.stats.timeslice_count
441 ),
442 before
443 );
444 install(&mut thread, hint).unwrap();
445 let replacement = budget(tid, thread.mm_id, 20);
446 assert!(install(&mut thread, replacement).is_err());
447 assert_eq!(thread.replay_rcb_end, Some(19));
448 }
449
450 #[test]
451 fn start_grant_survives_exactly_one_real_reset_and_refuses_other_lifetimes() {
452 let tid = DetTid::from_raw(3);
453 for timers in [true, false] {
454 let cfg = Config {
455 max_timeslice: if timers {
456 Config::default().max_timeslice
457 } else {
458 None
459 },
460 ..Config::default()
461 };
462 for resets in [0, 1, 2] {
463 let mut thread = ThreadState::new(tid, &cfg, ());
464 thread.slice_boundary = Some(Observations::new(&thread, 0));
465 let mut hint = budget(tid, thread.mm_id, 19);
466 if !timers {
467 hint.expected.entry = Entry::Syscall {
468 syscall: reverie::syscalls::Sysno::sched_yield,
469 occurrence: 1,
470 };
471 hint.expected.cause = Cause::ExplicitSchedYield;
472 }
473 let clock = thread.thread_logical_time.as_nanos();
474 stage_start(&mut thread, hint).unwrap();
475 assert_eq!(thread.replay_rcb_end, None);
476 assert!(stage_start(&mut thread, hint).is_err());
477 assert!(install(&mut thread, hint).is_err());
478 if resets == 0 {
479 let forged = StartReset {
482 tid,
483 mm: thread.mm_id,
484 owner: Some(hint.expected.owner),
485 before: 0,
486 after: 0,
487 };
488 let before =
489 serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap();
490 assert!(finish_start(&mut thread, forged).is_err());
491 assert_eq!(
492 serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap(),
493 before
494 );
495 } else {
496 let reset = reset_start(&mut thread, &cfg).unwrap();
497 if resets == 2 {
498 let before =
499 serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap();
500 assert!(reset_start(&mut thread, &cfg).is_err());
501 assert_eq!(
502 serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap(),
503 before
504 );
505 }
506 finish_start(&mut thread, reset).unwrap();
507 assert_eq!(thread.replay_rcb_end, timers.then_some(19));
508 assert_eq!(
509 thread.slice_boundary.as_ref().unwrap().expected_budget,
510 Some(hint)
511 );
512 assert!(
513 thread
514 .slice_boundary
515 .as_ref()
516 .unwrap()
517 .pending_start_budget
518 .is_none()
519 );
520 assert!(stage_start(&mut thread, hint).is_err());
521 }
522 assert_eq!(thread.thread_logical_time.as_nanos(), clock);
523 assert_eq!(
524 thread.stats.timeslice_count,
525 u64::from(timers && resets > 0)
526 );
527 }
528 for wrong_lifetime in 0..3 {
529 let mut thread = ThreadState::new(tid, &cfg, ());
530 thread.slice_boundary = Some(Observations::new(&thread, 0));
531 let hint = budget(tid, thread.mm_id, 19);
532 stage_start(&mut thread, hint).unwrap();
533 let reset = reset_start(&mut thread, &cfg).unwrap();
534 match wrong_lifetime {
535 0 => thread.mm_id = thread.mm_id.for_exec(tid),
536 1 => thread.dettid = DetTid::from_raw(17),
537 2 => thread.stats.timeslice_count += 1,
538 _ => unreachable!(),
539 }
540 assert!(finish_start(&mut thread, reset).is_err());
541 assert_eq!(thread.replay_rcb_end, None);
542 }
543 }
544 }
545
546 #[test]
547 fn malformed_wire_and_wrong_owner_budget_are_refused_before_state_changes() {
548 let cfg = Config::default();
549 let tid = DetTid::from_raw(3);
550 let mut thread = ThreadState::new(tid, &cfg, ());
551 thread.next_timeslice(&cfg);
552 thread.slice_boundary = Some(Observations::new(&thread, 0));
553 thread.committed_clock_value = 7;
554 let hint = budget(tid, thread.mm_id, 19);
555 let bytes = serde_json::to_vec(&hint).unwrap();
556 assert_eq!(serde_json::from_slice::<Budget>(&bytes).unwrap(), hint);
557 for bad in 0..7 {
558 let mut altered = hint;
559 match bad {
560 0 => altered.expected.owner.tid = DetTid::from_raw(4),
561 1 => altered.expected.owner.thread_generation += 1,
562 2 => altered.expected.owner.address_space_creator = DetTid::from_raw(4),
563 3 => altered.expected.owner.address_space_generation += 1,
564 4 => altered.expected.slice += 1,
565 5 => altered.expected.committed_rcbs = 6,
566 6 => altered.expected.checkin = 0,
567 _ => unreachable!(),
568 }
569 let before = serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap();
570 assert!(install(&mut thread, altered).is_err());
571 assert_eq!(
572 serde_json::to_vec(thread.slice_boundary.as_ref().unwrap()).unwrap(),
573 before
574 );
575 assert_eq!(thread.replay_rcb_end, None);
576 }
577 for bad in 0..4 {
578 let mut wire = serde_json::to_value(hint).unwrap();
579 match bad {
580 0 => wire["expected"]["slice"] = 0.into(),
581 1 => wire["expected"]["checkin"] = 0.into(),
582 2 => wire["expected"]["entry"] = "Cpuid".into(),
583 3 => wire["ignored"] = true.into(),
584 _ => unreachable!(),
585 }
586 assert!(serde_json::from_value::<Budget>(wire).is_err());
587 }
588 }
589}