Skip to main content

detcore/slice_boundary/
grant.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//! Typed transport over an existing scheduler grant. Lookahead authenticates
10//! the live owner; the mailbox creates no new resource or turn.
11
12use 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/// An expectation from a validated stream, never an observed callback. An
30/// absolute committed guest-RCB target does not include scheduler turn time.
31#[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    /// Select a boundary in an already validated, single stored stream.
75    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    /// Retain the complete mandatory identity for matching; no field here may
86    /// be copied into the independently produced observation counters.
87    pub fn expected(self) -> Identity {
88        self.expected
89    }
90
91    /// Index in the complete stored stream, including boundary records.
92    pub fn stored_index(self) -> usize {
93        self.stored_index
94    }
95}
96
97/// A pending expectation authenticated to the actual address space. It is
98/// transported only when the existing RPC actually receives a grant. Control
99/// replies cannot steal it, and terminal replies discard it without delivery.
100#[derive(Default, Debug)]
101pub struct Mailbox {
102    pending: BTreeMap<DetTid, (MmId, Budget, bool)>,
103}
104
105impl Mailbox {
106    /// Discard only a retired image's advertisement. A delayed old RPC must
107    /// not consume a replacement image's pending budget on the same TID.
108    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    /// Stage a validated lookahead for an authenticated live owner. The
119    /// scheduler integration must supply its registered MmId, not an expected
120    /// trace's invented address space. Repeating the identical advertisement
121    /// keeps its commit state; a different outstanding hint is refused.
122    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    /// Called only at the existing committed `unblock_guest` point. The
140    /// broader RPC `Granted` marker also covers parked signal observation,
141    /// which alone must not consume a pending boundary budget.
142    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    /// Finish the actual RPC, without modifying its status, resources, clock
149    /// or run queue. Refusal retains the pending hint for diagnosis.
150    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
176/// Apply a returned expectation after the completed slice's reset. This is
177/// not timer/callback integration: software identities remain expectations,
178/// and only a recorded PMU cause installs a precise PMU branch target. The
179/// configured maximum and existing logical deadline are never changed here.
180pub 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
225/// A StartNewThread reply precedes its existing first reset. Keep the budget
226/// pending, without arming a timer which that reset would immediately erase.
227pub 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/// A one-use receipt for the original startup `next_timeslice` call. The
251/// timer-disabled path intentionally does not increment timeslice statistics;
252/// possession of this private receipt, not an inferred counter change, proves
253/// that the call happened. It cannot be cloned or constructed by callers.
254#[derive(Debug)]
255pub(crate) struct StartReset {
256    tid: DetTid,
257    mm: MmId,
258    owner: Option<Owner>,
259    before: u64,
260    after: u64,
261}
262
263/// Perform exactly the startup reset already required by the caller. Preserve
264/// its timer-disabled behavior and return evidence for installation after the
265/// unchanged deadline-cap call. This does not add a scheduler request.
266pub(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
305/// Install the start grant after consuming proof of the actual startup reset,
306/// and after its unchanged deadline-cap call. This never resets a slice or
307/// advances clocks itself. Missing, stale and repeated resets are refused.
308pub(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        // A parked signal observation is marked Granted but has not committed
400        // the next resource turn. It cannot consume this expectation.
401        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                    // Only this child test module can construct an invalid
480                    // receipt; production callers cannot bypass reset_start.
481                    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}