Skip to main content

cpu_local/
preempt.rs

1use core::{
2    marker::PhantomData,
3    mem::ManuallyDrop,
4    ptr::NonNull,
5    sync::atomic::{AtomicU32, Ordering},
6};
7
8use crate::{CpuLocalError, CpuPin};
9
10const PREEMPT_NO_PENDING: u32 = 1 << 31;
11const PREEMPT_DEPTH_MASK: u32 = !PREEMPT_NO_PENDING;
12
13/// Snapshot of one architecture-selected preemption state.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct PreemptionSnapshot {
16    depth: u32,
17    pending: bool,
18}
19
20impl PreemptionSnapshot {
21    /// Returns the number of active preemption exclusions.
22    pub const fn depth(self) -> u32 {
23        self.depth
24    }
25
26    /// Returns whether work is pending at the next preemptible boundary.
27    pub const fn is_pending(self) -> bool {
28        self.pending
29    }
30}
31
32/// Linear proof of one entered preemption exclusion.
33#[must_use = "every entered preemption token must be finished exactly once"]
34pub struct PreemptionToken {
35    owner: NonNull<PreemptionState>,
36    _not_send_or_sync: PhantomData<*mut ()>,
37}
38
39/// Linear proof that the final preemption depth is reserved for a safe point.
40#[must_use = "pending preemption must be released by the external safe-point owner"]
41pub struct PendingPreemption {
42    owner: NonNull<PreemptionState>,
43    _not_send_or_sync: PhantomData<*mut ()>,
44}
45
46/// Result of finishing one preemption exclusion.
47#[must_use]
48pub enum PreemptionExit {
49    /// A nested depth was consumed; preemption remains excluded.
50    Nested,
51    /// The final depth was consumed and execution is preemptible.
52    Enabled,
53    /// The final depth remains reserved until an external safe point claims it.
54    Pending(PendingPreemption),
55}
56
57/// Architecture-neutral preemption word.
58///
59/// The high bit uses inverted pending polarity, allowing a newly initialized
60/// context to start enabled without a runtime initialization write. The low
61/// bits contain the exclusion depth. All updates are local to the selected CPU
62/// or pinned context; external safe-point serialization provides ordering for
63/// protected state, so this word does not publish cross-CPU data.
64#[repr(transparent)]
65pub(crate) struct PreemptionState(AtomicU32);
66
67impl PreemptionState {
68    pub(crate) const fn new() -> Self {
69        Self(AtomicU32::new(PREEMPT_NO_PENDING))
70    }
71
72    pub(crate) const fn bootstrap_disabled() -> Self {
73        Self(AtomicU32::new(PREEMPT_NO_PENDING | 1))
74    }
75
76    fn snapshot(&self) -> PreemptionSnapshot {
77        let state = self.0.load(Ordering::Relaxed);
78        PreemptionSnapshot {
79            depth: state & PREEMPT_DEPTH_MASK,
80            pending: state & PREEMPT_NO_PENDING == 0,
81        }
82    }
83
84    fn set_pending(&self) {
85        self.0.fetch_and(PREEMPT_DEPTH_MASK, Ordering::Relaxed);
86    }
87
88    fn clear_pending(&self) {
89        self.0.fetch_or(PREEMPT_NO_PENDING, Ordering::Relaxed);
90    }
91
92    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
93    fn enter(&self) {
94        let previous = self.0.fetch_add(1, Ordering::Relaxed);
95        assert_ne!(
96            previous & PREEMPT_DEPTH_MASK,
97            PREEMPT_DEPTH_MASK,
98            "preemption nesting overflow"
99        );
100    }
101
102    fn finish(&self) -> PreemptionExit {
103        loop {
104            let state = self.0.load(Ordering::Relaxed);
105            let depth = state & PREEMPT_DEPTH_MASK;
106            assert!(depth > 0, "unbalanced preemption exit");
107
108            if depth == 1 && state & PREEMPT_NO_PENDING == 0 {
109                return PreemptionExit::Pending(PendingPreemption::new(self));
110            }
111
112            let next = state - 1;
113            if self
114                .0
115                .compare_exchange_weak(state, next, Ordering::Relaxed, Ordering::Relaxed)
116                .is_ok()
117            {
118                return if depth == 1 {
119                    PreemptionExit::Enabled
120                } else {
121                    PreemptionExit::Nested
122                };
123            }
124        }
125    }
126
127    fn release_pending(&self) {
128        assert_eq!(
129            self.0
130                .compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed),
131            Ok(1),
132            "pending preemption no longer owns the final depth"
133        );
134    }
135
136    fn release_bootstrap(&self) {
137        assert_eq!(
138            self.0.compare_exchange(
139                PREEMPT_NO_PENDING | 1,
140                PREEMPT_NO_PENDING,
141                Ordering::Relaxed,
142                Ordering::Relaxed,
143            ),
144            Ok(PREEMPT_NO_PENDING | 1),
145            "bootstrap preemption depth must be released exactly once"
146        );
147    }
148
149    #[cfg(any(all(target_arch = "x86_64", not(feature = "host-test")), test))]
150    fn release_initial_switch(&self) -> bool {
151        loop {
152            let state = self.0.load(Ordering::Relaxed);
153            if state == PREEMPT_NO_PENDING {
154                return false;
155            }
156            if state != (PREEMPT_NO_PENDING | 1) && state != 1 {
157                panic!("initial context switch found invalid preemption state {state:#x}");
158            }
159            // A nested outgoing guard may mirror its owner's pending bit after
160            // the switch guard was entered. The incoming context has its own
161            // upper-layer publication, so consume the inherited depth and
162            // reset that stale mirror as one transition.
163            if self
164                .0
165                .compare_exchange_weak(
166                    state,
167                    PREEMPT_NO_PENDING,
168                    Ordering::Relaxed,
169                    Ordering::Relaxed,
170                )
171                .is_ok()
172            {
173                return true;
174            }
175        }
176    }
177}
178
179impl PreemptionToken {
180    fn new(owner: &PreemptionState) -> Self {
181        Self {
182            owner: NonNull::from(owner),
183            _not_send_or_sync: PhantomData,
184        }
185    }
186
187    /// Converts this token into an opaque value for an ABI that transports
188    /// guard state as one machine word.
189    #[doc(hidden)]
190    pub fn into_raw(self) -> usize {
191        let token = ManuallyDrop::new(self);
192        token.owner.as_ptr() as usize
193    }
194
195    /// Reconstructs a token produced by [`Self::into_raw`].
196    ///
197    /// # Safety
198    ///
199    /// `raw` must come from one still-live token, refer to a live CPU-local
200    /// preemption owner, and be reconstructed and finished exactly once.
201    #[doc(hidden)]
202    pub unsafe fn from_raw(raw: usize) -> Option<Self> {
203        if !raw.is_multiple_of(core::mem::align_of::<PreemptionState>()) {
204            return None;
205        }
206        NonNull::new(raw as *mut PreemptionState).map(|owner| Self {
207            owner,
208            _not_send_or_sync: PhantomData,
209        })
210    }
211
212    fn state(&self) -> &PreemptionState {
213        // SAFETY: construction and raw reconstruction require the selected
214        // owner to remain live through this token's single finish operation.
215        unsafe { self.owner.as_ref() }
216    }
217
218    #[cfg(any(all(target_arch = "x86_64", not(feature = "host-test")), test))]
219    fn handoff_after_context_switch(self, resumed_owner: &PreemptionState) -> Self {
220        if self.owner == NonNull::from(resumed_owner) {
221            self
222        } else {
223            // The old CPU's incoming context consumed the depth represented by
224            // this proof. Consume the proof itself and adopt the equivalent
225            // depth left by the outgoing context on the resumed CPU.
226            Self::new(resumed_owner)
227        }
228    }
229}
230
231impl PendingPreemption {
232    fn new(owner: &PreemptionState) -> Self {
233        Self {
234            owner: NonNull::from(owner),
235            _not_send_or_sync: PhantomData,
236        }
237    }
238
239    /// Atomically consumes the final reserved preemption depth.
240    pub fn release(self) {
241        // SAFETY: the linear token retains its exact owner until this consume.
242        unsafe { self.owner.as_ref() }.release_pending();
243    }
244}
245
246/// Enters preemption exclusion on the owner selected by this architecture.
247#[inline(always)]
248pub fn enter_preemption() -> PreemptionToken {
249    #[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
250    {
251        // Increment through GS before resolving the token owner. Once the
252        // increment is visible this execution cannot migrate away from it.
253        unsafe { crate::register::enter_x86_preemption() };
254        let owner = crate::register::current_area()
255            .unwrap_or_else(|_| crate::register::fatal_register_invariant())
256            .runtime_anchor()
257            .preemption_state();
258        PreemptionToken::new(owner)
259    }
260
261    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
262    {
263        let current = unsafe { crate::current_context_unpinned() }
264            .unwrap_or_else(|_| crate::register::fatal_register_invariant());
265        // SAFETY: the architecture current register identifies the executing
266        // context itself. An interrupt may suspend and later migrate that
267        // context between this read and the increment, but it cannot resume
268        // this instruction stream as a different context; the pinned header
269        // stays live and therefore remains the exact token owner.
270        let owner = unsafe { current.as_ref() }.preemption_state();
271        owner.enter();
272        PreemptionToken::new(owner)
273    }
274}
275
276/// Observes the current architecture-selected preemption state.
277pub fn preemption_snapshot(pin: &CpuPin<'_>) -> Result<PreemptionSnapshot, CpuLocalError> {
278    Ok(selected_state(pin)?.snapshot())
279}
280
281/// Marks work pending at the current preemptible boundary.
282pub fn set_preemption_pending(pin: &CpuPin<'_>) -> Result<(), CpuLocalError> {
283    selected_state(pin)?.set_pending();
284    Ok(())
285}
286
287/// Clears the pending mark after the external owner has drained its work.
288pub fn clear_preemption_pending(pin: &CpuPin<'_>) -> Result<(), CpuLocalError> {
289    selected_state(pin)?.clear_pending();
290    Ok(())
291}
292
293/// Finishes the exact owner captured by [`enter_preemption`].
294pub fn finish_preemption(token: PreemptionToken) -> PreemptionExit {
295    token.state().finish()
296}
297
298/// Transfers a CPU-owned switch exclusion to the CPU where its context resumed.
299///
300/// Context-owned tokens keep their original owner across migration. A CPU-owned
301/// token is replaced only when a context switch resumes it on another CPU: the
302/// old CPU's incoming context has already consumed the old switch depth, while
303/// this CPU's outgoing context left the matching depth for the resumed guard.
304///
305/// # Errors
306///
307/// Returns an error when the pinned CPU has no valid selected preemption owner.
308///
309/// # Panics
310///
311/// Panics when a context-owned architecture observes a different owner after
312/// the context switch. Such architectures migrate the owner with the context.
313#[doc(hidden)]
314pub fn handoff_preemption_after_context_switch(
315    pin: &CpuPin<'_>,
316    token: PreemptionToken,
317) -> Result<PreemptionToken, CpuLocalError> {
318    let owner = selected_state(pin)?;
319    #[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
320    {
321        Ok(token.handoff_after_context_switch(owner))
322    }
323
324    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
325    {
326        assert_eq!(
327            token.owner,
328            NonNull::from(owner),
329            "context-owned preemption token changed owner across a context switch"
330        );
331        Ok(token)
332    }
333}
334
335/// Releases the single preemption depth inherited by a bootstrap context.
336///
337/// The caller uses this only after the context and every local safe-point
338/// dependency have been published.
339#[doc(hidden)]
340pub fn release_bootstrap_preemption(pin: &CpuPin<'_>) -> Result<(), CpuLocalError> {
341    selected_state(pin)?.release_bootstrap();
342    Ok(())
343}
344
345/// Releases the preemption exclusion transferred to a context on its first
346/// architecture switch.
347///
348/// CPU-owned preemption architectures carry the outgoing switch exclusion
349/// across the raw transfer, but a new context has no suspended caller whose
350/// guard can finish it. Context-owned architectures need no action because the
351/// new header begins enabled.
352#[doc(hidden)]
353pub fn release_initial_context_preemption(pin: &CpuPin<'_>) -> Result<bool, CpuLocalError> {
354    #[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
355    {
356        Ok(selected_state(pin)?.release_initial_switch())
357    }
358
359    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
360    {
361        let snapshot = selected_state(pin)?.snapshot();
362        assert_eq!(
363            snapshot.depth(),
364            0,
365            "new context-owned preemption state must start enabled"
366        );
367        Ok(false)
368    }
369}
370
371fn selected_state(pin: &CpuPin<'_>) -> Result<&'static PreemptionState, CpuLocalError> {
372    #[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
373    {
374        Ok(pin.area().runtime_anchor().preemption_state())
375    }
376
377    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
378    {
379        let current = crate::current_context(pin)?;
380        // SAFETY: current publication keeps the pinned context live, and the
381        // caller's CpuPin prevents it from changing during this observation.
382        Ok(unsafe { current.as_ref() }.preemption_state())
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    fn enter_on(state: &PreemptionState) -> PreemptionToken {
391        state.enter();
392        PreemptionToken::new(state)
393    }
394
395    #[test]
396    fn nested_and_final_exits_are_linear() {
397        let state = PreemptionState::new();
398        let outer = enter_on(&state);
399        let inner = enter_on(&state);
400
401        assert!(matches!(finish_preemption(inner), PreemptionExit::Nested));
402        assert_eq!(state.snapshot().depth(), 1);
403        assert!(matches!(finish_preemption(outer), PreemptionExit::Enabled));
404        assert_eq!(state.snapshot().depth(), 0);
405    }
406
407    #[test]
408    fn pending_exit_reserves_depth_until_release() {
409        let state = PreemptionState::new();
410        let token = enter_on(&state);
411        state.set_pending();
412
413        let PreemptionExit::Pending(pending) = finish_preemption(token) else {
414            panic!("final pending exit must retain its depth");
415        };
416        assert_eq!(state.snapshot().depth(), 1);
417        pending.release();
418        assert_eq!(state.snapshot().depth(), 0);
419        assert!(state.snapshot().is_pending());
420    }
421
422    #[test]
423    fn bootstrap_starts_disabled() {
424        let state = PreemptionState::bootstrap_disabled();
425        assert_eq!(state.snapshot().depth(), 1);
426        assert!(!state.snapshot().is_pending());
427    }
428
429    #[test]
430    fn initial_switch_discards_outgoing_pending_mirror() {
431        let state = PreemptionState::bootstrap_disabled();
432        state.set_pending();
433
434        assert!(state.release_initial_switch());
435        assert_eq!(state.snapshot().depth(), 0);
436        assert!(!state.snapshot().is_pending());
437    }
438
439    #[test]
440    #[should_panic(expected = "pending preemption no longer owns the final depth")]
441    fn pending_depth_cannot_be_consumed_twice() {
442        let state = PreemptionState(AtomicU32::new(1));
443        let first = PendingPreemption::new(&state);
444        let duplicate = PendingPreemption::new(&state);
445
446        first.release();
447        duplicate.release();
448    }
449
450    #[test]
451    #[should_panic(expected = "bootstrap preemption depth must be released exactly once")]
452    fn bootstrap_depth_cannot_be_released_twice() {
453        let state = PreemptionState::bootstrap_disabled();
454
455        state.release_bootstrap();
456        state.release_bootstrap();
457    }
458
459    #[test]
460    fn token_stays_bound_to_the_entry_owner() {
461        let original_context = PreemptionState::new();
462        let migrated_context = PreemptionState::new();
463        let original_cpu = PreemptionState::new();
464        let migrated_cpu = PreemptionState::new();
465
466        let context_token = enter_on(&original_context);
467        let cpu_token = enter_on(&original_cpu);
468        migrated_context.enter();
469        migrated_cpu.enter();
470
471        assert!(matches!(
472            finish_preemption(context_token),
473            PreemptionExit::Enabled
474        ));
475        assert!(matches!(
476            finish_preemption(cpu_token),
477            PreemptionExit::Enabled
478        ));
479        assert_eq!(original_context.snapshot().depth(), 0);
480        assert_eq!(original_cpu.snapshot().depth(), 0);
481        assert_eq!(migrated_context.snapshot().depth(), 1);
482        assert_eq!(migrated_cpu.snapshot().depth(), 1);
483    }
484
485    #[test]
486    fn cpu_owned_switch_token_handoff_follows_the_resumed_cpu() {
487        let original_cpu = PreemptionState::new();
488        let resumed_cpu = PreemptionState::new();
489
490        let suspended_switch = enter_on(&original_cpu);
491        // Another incoming context consumes the switch depth left on the old
492        // CPU while this context is suspended.
493        assert!(original_cpu.release_initial_switch());
494        // The outgoing context on the destination CPU leaves the equivalent
495        // switch depth for the context that is about to resume there.
496        resumed_cpu.enter();
497
498        let resumed_switch = suspended_switch.handoff_after_context_switch(&resumed_cpu);
499        assert!(matches!(
500            finish_preemption(resumed_switch),
501            PreemptionExit::Enabled
502        ));
503        assert_eq!(original_cpu.snapshot().depth(), 0);
504        assert_eq!(resumed_cpu.snapshot().depth(), 0);
505    }
506
507    #[test]
508    fn malformed_raw_owner_is_rejected() {
509        // SAFETY: no token is reconstructed because the value is misaligned.
510        assert!(unsafe { PreemptionToken::from_raw(1) }.is_none());
511    }
512}