Skip to main content

ax_runtime/thread/
context.rs

1use alloc::boxed::Box;
2use core::{
3    cell::UnsafeCell,
4    mem::offset_of,
5    pin::Pin,
6    ptr::{self, NonNull},
7};
8
9use ax_hal::{
10    cpu::context::TaskAnchor,
11    percpu::{CpuPin, ExecutionContextHeader, PreparedContextSwitch, PreviousContextBinding},
12};
13use ax_task::{
14    runtime::{
15        RuntimeHandleResult, RuntimeStatus,
16        resource::{ExecutionContextHandle, KernelContextRequest, StackHandle, UserContextRequest},
17        switch::{
18            ContextThreadBinding, CurrentThreadPublication, RuntimeSwitchPlan, ThreadIdentityV1,
19        },
20    },
21    thread::TaskError,
22};
23
24use super::{
25    resources::{RuntimeStack, runtime_tls_pointer},
26    runtime_status_error, with_current_cpu_pin,
27};
28
29/// Reports whether a kernel page fault hit the current runtime stack guard.
30pub fn diagnose_current_stack_guard_page_fault(fault: ax_memory_addr::VirtAddr) -> bool {
31    #[cfg(feature = "stack-guard-page")]
32    {
33        // SAFETY: trap execution cannot migrate before returning through its
34        // architecture epilogue.
35        unsafe {
36            with_current_cpu_pin(|cpu_pin| {
37                let Ok(context) = current_runtime_context(cpu_pin) else {
38                    return false;
39                };
40                let stack = context.stack.into_raw();
41                if stack == 0 {
42                    return false;
43                }
44                // SAFETY: the scheduler owns the stack until this context can
45                // no longer run, and the current header keeps it on-CPU.
46                let stack = &*ptr::with_exposed_provenance::<RuntimeStack>(stack);
47                let super::resources::StackBacking::VirtualPages(allocation) = &stack.backing
48                else {
49                    return false;
50                };
51                let guard_start = allocation.reservation_range().start.as_usize();
52                let guard_end = allocation.usable_range().start.as_usize();
53                if !(guard_start..guard_end).contains(&fault.as_usize()) {
54                    return false;
55                }
56                error!(
57                    "task stack guard page hit: fault_addr={:#x}, stack=[{:#x}..{:#x}), \
58                     guard=[{:#x}..{:#x})",
59                    fault.as_usize(),
60                    guard_end,
61                    stack.usable_top,
62                    guard_start,
63                    guard_end,
64                );
65                true
66            })
67        }
68    }
69    #[cfg(not(feature = "stack-guard-page"))]
70    {
71        let _ = fault;
72        false
73    }
74}
75
76struct RuntimeSwitchTail {
77    previous: NonNull<ExecutionContextHeader>,
78    binding: PreviousContextBinding,
79    #[cfg(feature = "qperf-metrics")]
80    qperf_runtime_tail_started_ns: u64,
81    #[cfg(feature = "qperf-metrics")]
82    qperf_switch_started_ns: u64,
83}
84
85/// Runtime-owned architecture context and its pinned scheduler identity.
86///
87/// The header stays at offset zero so a current-thread publication can be
88/// checked against its context handle without a second registry or per-CPU
89/// pointer. `switch_tail` is written only while this context is off-CPU and is
90/// consumed exactly once after it becomes current with local IRQs disabled.
91#[repr(C)]
92struct RuntimeContext {
93    header: ExecutionContextHeader,
94    publication: UnsafeCell<CurrentThreadPublication>,
95    inner: Box<UnsafeCell<ax_hal::context::TaskContext>>,
96    stack: StackHandle,
97    switch_tail: UnsafeCell<Option<RuntimeSwitchTail>>,
98}
99
100#[derive(Clone, Copy)]
101enum InitialPreemptionState {
102    Enabled,
103    BootstrapDisabled,
104}
105
106const _: () = assert!(offset_of!(RuntimeContext, header) == 0);
107
108impl RuntimeContext {
109    fn allocate(
110        inner: ax_hal::context::TaskContext,
111        stack: StackHandle,
112        preemption: InitialPreemptionState,
113    ) -> Result<*mut RuntimeContext, RuntimeStatus> {
114        let inner = super::allocation::try_box(UnsafeCell::new(inner))?;
115        let header = match preemption {
116            InitialPreemptionState::Enabled => ExecutionContextHeader::new(),
117            InitialPreemptionState::BootstrapDisabled => ExecutionContextHeader::new_bootstrap(),
118        };
119        Ok(Box::into_raw(super::allocation::try_box(Self {
120            header,
121            publication: UnsafeCell::new(CurrentThreadPublication::NONE),
122            inner,
123            stack,
124            switch_tail: UnsafeCell::new(None),
125        })?))
126    }
127
128    fn header(&self) -> Pin<&ExecutionContextHeader> {
129        // SAFETY: every RuntimeContext is constructed in a Box and is never
130        // moved before destruction after its header is no longer published.
131        unsafe { Pin::new_unchecked(&self.header) }
132    }
133
134    fn has_switch_tail(&self) -> bool {
135        // SAFETY: only the incoming scheduler continuation reads this slot;
136        // the context is current and local IRQs serialize scheduler entry.
137        unsafe { (*self.switch_tail.get()).is_some() }
138    }
139
140    unsafe fn stage_switch_tail(&self, tail: RuntimeSwitchTail) -> Result<(), RuntimeStatus> {
141        // SAFETY: the scheduler selected this context while it is off-CPU and
142        // holds the only right to prepare its next incoming continuation.
143        let slot = unsafe { &mut *self.switch_tail.get() };
144        if slot.is_some() {
145            return Err(RuntimeStatus::Busy);
146        }
147        *slot = Some(tail);
148        Ok(())
149    }
150
151    unsafe fn finish_switch_tail(&self) -> (u64, u64) {
152        // SAFETY: the current incoming continuation owns this slot with local
153        // IRQs disabled and completes the one-shot previous-binding token.
154        let slot = unsafe { &mut *self.switch_tail.get() };
155        let tail = slot
156            .take()
157            .expect("incoming runtime context is missing its switch tail");
158        // SAFETY: the outgoing header stays pinned and unreclaimable through
159        // the scheduler `on_cpu` handoff; this tail owns its exact epoch.
160        let previous = unsafe { Pin::new_unchecked(tail.previous.as_ref()) };
161        unsafe { tail.binding.finish(previous) }
162            .expect("runtime switch tail did not own the exact previous CPU binding");
163        #[cfg(feature = "qperf-metrics")]
164        return (
165            tail.qperf_runtime_tail_started_ns,
166            tail.qperf_switch_started_ns,
167        );
168        #[cfg(not(feature = "qperf-metrics"))]
169        (0, 0)
170    }
171}
172
173fn runtime_context(
174    handle: ExecutionContextHandle,
175) -> Result<&'static RuntimeContext, RuntimeStatus> {
176    if handle.is_none() {
177        return Err(RuntimeStatus::InvalidHandle);
178    }
179    let context = ptr::with_exposed_provenance::<RuntimeContext>(handle.into_raw());
180    // SAFETY: TaskRuntime receives only live handles created by this provider;
181    // the scheduler retains context ownership through every runtime call.
182    let context = unsafe { &*context };
183    if !ptr::eq(
184        ptr::addr_of!(context.header),
185        context as *const RuntimeContext as *const ExecutionContextHeader,
186    ) {
187        return Err(RuntimeStatus::InvalidHandle);
188    }
189    Ok(context)
190}
191
192fn current_runtime_context(cpu_pin: &CpuPin) -> Result<&'static RuntimeContext, RuntimeStatus> {
193    let current = ax_hal::percpu::current_context(cpu_pin)
194        .map_err(|_| RuntimeStatus::InvalidHandle)?
195        .as_ptr()
196        .expose_provenance();
197    let header = ptr::with_exposed_provenance::<ExecutionContextHeader>(current);
198    // SAFETY: the switch boundary validated the binding before publishing
199    // this live pinned header, and the supplied CPU pin prevents migration.
200    let header = unsafe { &*header };
201    // RuntimeContext is `repr(C)` and the pinned header is its offset-zero
202    // owner identity. The independently allocated architecture context keeps
203    // ContextIdentity free of self-referential outer pointers.
204    let context = unsafe { &*ptr::from_ref(header).cast::<RuntimeContext>() };
205    if !ptr::eq(context.header().get_ref(), header) {
206        return Err(RuntimeStatus::InvalidHandle);
207    }
208    Ok(context)
209}
210
211/// Immutable runtime identity captured by one safe user-execution object.
212#[cfg(feature = "uspace")]
213pub(super) struct RuntimeUserBinding {
214    #[cfg(all(target_arch = "x86_64", feature = "fp-simd"))]
215    context: NonNull<RuntimeContext>,
216}
217
218#[cfg(feature = "uspace")]
219impl RuntimeUserBinding {
220    pub(super) fn prepare_user_fp_return(&self) {
221        #[cfg(all(target_arch = "x86_64", feature = "fp-simd"))]
222        {
223            // SAFETY: this binding is owned by the current task's kernel stack.
224            // The final user-return boundary has local IRQs disabled, so its
225            // runtime context and CPU-local FPU owner cannot change here.
226            let context = unsafe { self.context.as_ref() };
227            let architecture_context = unsafe { &*context.inner.get() };
228            architecture_context.prepare_user_return_fp();
229        }
230    }
231}
232
233#[cfg(feature = "uspace")]
234pub(super) fn bind_current_user_context(
235    cpu_pin: &CpuPin<'_>,
236) -> Result<RuntimeUserBinding, RuntimeStatus> {
237    let context = current_runtime_context(cpu_pin)?;
238    if context.has_switch_tail() {
239        return Err(RuntimeStatus::UnsafeContext);
240    }
241    // SAFETY: the publication is immutable after context binding and the
242    // current header keeps this runtime context alive.
243    let publication = unsafe { *context.publication.get() };
244    if !publication.identity().is_bound() || publication.owner().is_none() {
245        return Err(RuntimeStatus::InvalidHandle);
246    }
247    Ok(RuntimeUserBinding {
248        #[cfg(all(target_arch = "x86_64", feature = "fp-simd"))]
249        context: NonNull::from(context),
250    })
251}
252
253pub(super) fn bind_bootstrap_runtime_context(
254    cpu_pin: &CpuPin,
255    handle: ExecutionContextHandle,
256    kernel_tls: usize,
257) -> Result<(), TaskError> {
258    let boot_context =
259        ax_hal::percpu::current_context(cpu_pin).map_err(|_| TaskError::InvalidConfiguration)?;
260    if !ax_hal::percpu::is_permanent_boot_context(boot_context)
261        .map_err(|_| TaskError::InvalidConfiguration)?
262    {
263        return Err(TaskError::InvalidConfiguration);
264    }
265    let context = runtime_context(handle).map_err(runtime_status_error)?;
266    // SAFETY: the CPU is still offline and trap-free, while the scheduler
267    // record keeps this pinned header alive until its switch tail withdraws it.
268    unsafe { ax_hal::percpu::install_bootstrap_context(cpu_pin, context.header()) }
269        .map_err(|_| TaskError::InvalidConfiguration)?;
270    #[cfg(kernel_tls)]
271    // SAFETY: the same offline bootstrap boundary owns the task TLS register.
272    unsafe {
273        ax_hal::percpu::install_bootstrap_kernel_tls(
274            cpu_pin,
275            ax_hal::context::KernelTlsBase::new(kernel_tls),
276        );
277    }
278    #[cfg(not(kernel_tls))]
279    assert_eq!(
280        kernel_tls, 0,
281        "TLS-disabled bootstrap must retain a zero TLS identity"
282    );
283    Ok(())
284}
285
286pub(super) fn finish_runtime_context_switch_tail() -> bool {
287    #[cfg(feature = "qperf-metrics")]
288    let qperf_incoming_tail_started_ns = crate::clock_event_runtime::monotonic_now().as_nanos();
289    // SAFETY: TaskSystem invokes this with the scheduler baton and local IRQs
290    // disabled immediately after entering the incoming context.
291    let (_qperf_runtime_tail_started_ns, _qperf_switch_started_ns) = unsafe {
292        with_current_cpu_pin(|cpu_pin| {
293            let current = current_runtime_context(cpu_pin)
294                .expect("incoming scheduler context is not runtime-owned");
295            // SAFETY: the incoming context exclusively owns its staged one-shot tail.
296            current.finish_switch_tail()
297        })
298    };
299    #[cfg(feature = "qperf-metrics")]
300    let qperf_incoming_tail_finished_ns = crate::clock_event_runtime::monotonic_now().as_nanos();
301    #[cfg(feature = "qperf-metrics")]
302    {
303        ax_task::diagnostics::qperf_record_switch_scheduler_detail(
304            21,
305            _qperf_switch_started_ns,
306            qperf_incoming_tail_started_ns,
307        );
308        ax_task::diagnostics::qperf_record_switch_scheduler_detail(
309            22,
310            qperf_incoming_tail_started_ns,
311            qperf_incoming_tail_finished_ns,
312        );
313        ax_task::diagnostics::qperf_record_switch_phase_runtime_tail(
314            _qperf_runtime_tail_started_ns,
315            qperf_incoming_tail_finished_ns,
316        );
317    }
318    super::address_space::take_context_switch_reclaim_ready()
319}
320
321pub(super) fn create_runtime_context(request: KernelContextRequest) -> RuntimeHandleResult {
322    create_runtime_context_parts(request.stack, request.entry, request.tls)
323}
324
325pub(super) fn create_user_runtime_context(request: UserContextRequest) -> RuntimeHandleResult {
326    #[cfg(not(feature = "uspace"))]
327    {
328        let _ = request;
329        RuntimeHandleResult::failure(RuntimeStatus::Unsupported)
330    }
331    #[cfg(feature = "uspace")]
332    {
333        create_runtime_context_parts(request.stack, request.entry, request.tls)
334    }
335}
336
337fn create_runtime_context_parts(
338    stack_handle: StackHandle,
339    entry: ax_task::runtime::resource::KernelEntry,
340    tls_handle: ax_task::runtime::resource::TlsHandle,
341) -> RuntimeHandleResult {
342    #[cfg(feature = "fault-injection")]
343    if super::creation_probe::record(super::creation_probe::CreationEvent::Context) {
344        return RuntimeHandleResult::failure(RuntimeStatus::NoMemory);
345    }
346    if stack_handle.is_none() {
347        return RuntimeHandleResult::failure(RuntimeStatus::InvalidHandle);
348    }
349    // SAFETY: the scheduler keeps the stack handle live until context destroy.
350    let stack = unsafe { &*ptr::with_exposed_provenance::<RuntimeStack>(stack_handle.into_raw()) };
351    let tls_pointer = runtime_tls_pointer(tls_handle);
352    let mut context = ax_hal::context::TaskContext::new();
353    context.init(
354        entry as usize,
355        ax_memory_addr::VirtAddr::from(stack.usable_top),
356        ax_hal::context::KernelTlsBase::new(tls_pointer),
357    );
358    match RuntimeContext::allocate(context, stack_handle, InitialPreemptionState::Enabled) {
359        Ok(context) => RuntimeHandleResult::success(context.expose_provenance()),
360        Err(status) => RuntimeHandleResult::failure(status),
361    }
362}
363
364pub(super) fn create_bootstrap_context() -> ExecutionContextHandle {
365    let context = ax_hal::context::TaskContext::new();
366    let context = RuntimeContext::allocate(
367        context,
368        StackHandle::NONE,
369        InitialPreemptionState::BootstrapDisabled,
370    )
371    .expect("bootstrap context allocation failed");
372    // SAFETY: Box::into_raw yields a non-null uniquely owned RuntimeContext
373    // that stays live until destroy_runtime_context consumes the handle.
374    unsafe { ExecutionContextHandle::from_raw(context.expose_provenance()) }
375}
376
377pub(super) fn destroy_runtime_context(handle: ExecutionContextHandle) -> RuntimeStatus {
378    if handle.is_none() {
379        return RuntimeStatus::InvalidHandle;
380    }
381    let context = ptr::with_exposed_provenance_mut::<RuntimeContext>(handle.into_raw());
382    // SAFETY: the scheduler keeps the runtime handle live while asking whether
383    // its physical CPU handoff has completed.
384    let context_ref = unsafe { &*context };
385    if context_ref.header.cpu_area().is_some() || context_ref.has_switch_tail() {
386        return RuntimeStatus::Busy;
387    }
388    // SAFETY: the scheduler proves this context cannot run again and consumes
389    // its runtime handle exactly once.
390    drop(unsafe { Box::from_raw(context) });
391    #[cfg(feature = "fault-injection")]
392    super::creation_probe::record(super::creation_probe::CreationEvent::DropContext);
393    RuntimeStatus::Success
394}
395
396pub(super) fn bind_runtime_context_thread(binding: ContextThreadBinding) -> RuntimeStatus {
397    #[cfg(feature = "fault-injection")]
398    if super::creation_probe::record(super::creation_probe::CreationEvent::Bind) {
399        return RuntimeStatus::NoMemory;
400    }
401    if !binding.publication.identity().is_bound() || binding.publication.owner().is_none() {
402        return RuntimeStatus::InvalidArgument;
403    }
404    let Ok(context) = runtime_context(binding.context) else {
405        return RuntimeStatus::InvalidHandle;
406    };
407    // Binding is immutable and occurs exactly once before scheduler publication.
408    if unsafe { *context.publication.get() } != CurrentThreadPublication::NONE {
409        return RuntimeStatus::InvalidArgument;
410    }
411    // Context binding runs exactly once before scheduler publication, so this
412    // is the sole write to the pinned current-thread publication.
413    unsafe { *context.publication.get() = binding.publication };
414    // Scheduler construction invokes this exactly once before the context can
415    // enter a run queue. The bootstrap placeholder is likewise not consumed by
416    // assembly until its first switch-out.
417    unsafe { &mut *context.inner.get() }
418        .set_task_anchor(TaskAnchor::new(context.header().as_non_null()));
419    RuntimeStatus::Success
420}
421
422/// Reads the immutable scheduler publication owned by the current task context.
423pub(super) fn scheduler_current_thread_publication() -> CurrentThreadPublication {
424    // SAFETY: the architecture current source identifies this executing
425    // context. Preemption may suspend and migrate it during the read, but the
426    // same pinned context resumes and its publication is immutable.
427    let Ok(header) = (unsafe { ax_hal::percpu::current_context_unpinned() }) else {
428        return CurrentThreadPublication::NONE;
429    };
430    // SAFETY: current_context_unpinned returned the live pinned header owned by
431    // this executing context; its construction kind never changes.
432    if unsafe { header.as_ref() }.is_permanent_boot_context() {
433        return CurrentThreadPublication::NONE;
434    }
435    let context = header.as_ptr().cast::<RuntimeContext>();
436    // SAFETY: RuntimeContext embeds the published header at offset zero and
437    // remains alive while this execution context can run or resume.
438    unsafe { *(*context).publication.get() }
439}
440
441/// Reads only the immutable scheduler identity owned by the current task.
442pub(super) fn scheduler_current_thread_identity() -> ThreadIdentityV1 {
443    scheduler_current_thread_publication().identity()
444}
445
446#[cfg(all(not(target_arch = "riscv64"), feature = "fp-simd", feature = "uspace"))]
447pub(super) fn validate_current_user_fp_clone_context() -> Result<(), TaskError> {
448    if !ax_cpu::interrupt::irqs_enabled() || ax_hal::irq::in_irq_context() {
449        return Err(TaskError::UnsafeContext);
450    }
451    ax_cpu::interrupt::disable_irqs();
452    // SAFETY: local IRQ exclusion pins the current header while validating
453    // that this call originates from a runtime-owned user task context.
454    let result = unsafe {
455        with_current_cpu_pin(|cpu_pin| {
456            current_runtime_context(cpu_pin)
457                .map(|_| ())
458                .map_err(runtime_status_error)
459        })
460    };
461    ax_cpu::interrupt::enable_irqs();
462    result
463}
464
465#[cfg(all(not(target_arch = "riscv64"), feature = "fp-simd", feature = "uspace"))]
466pub(super) fn inherit_current_user_fp_state(child_context: usize) {
467    assert!(
468        ax_cpu::interrupt::irqs_enabled() && !ax_hal::irq::in_irq_context(),
469        "FPU inheritance requires ordinary task context",
470    );
471    assert_ne!(child_context, 0, "FPU inheritance requires a child context");
472    let child = ptr::with_exposed_provenance_mut::<RuntimeContext>(child_context);
473    ax_cpu::interrupt::disable_irqs();
474    // SAFETY: the child allocation is exclusively owned by resource creation
475    // and remains unpublished. IRQ exclusion pins the current parent context
476    // and its CPU-local FPU owner through the architecture FP snapshot into the child.
477    unsafe {
478        with_current_cpu_pin(|cpu_pin| {
479            let parent = current_runtime_context(cpu_pin)
480                .unwrap_or_else(|status| panic!("invalid FPU clone parent context: {status:?}"));
481            assert!(!core::ptr::eq(parent, child));
482            let parent_architecture_context = &*parent.inner.get();
483            let child_architecture_context = &mut *(*child).inner.get();
484            parent_architecture_context.clone_user_fp_state_into(child_architecture_context);
485        })
486    };
487    ax_cpu::interrupt::enable_irqs();
488}
489
490#[cfg(all(target_arch = "x86_64", feature = "fp-simd", feature = "uspace"))]
491pub(super) fn capture_current_user_fp_state()
492-> Result<ax_hal::cpu::registers::UserXstate, TaskError> {
493    if !ax_cpu::interrupt::irqs_enabled() || ax_hal::irq::in_irq_context() {
494        return Err(TaskError::UnsafeContext);
495    }
496    ax_cpu::interrupt::disable_irqs();
497    // SAFETY: local IRQ exclusion pins the runtime context and CPU-local FPU
498    // owner while the current hardware image is copied into a task-owned value.
499    let result = unsafe {
500        with_current_cpu_pin(|cpu_pin| {
501            let context = current_runtime_context(cpu_pin).map_err(runtime_status_error)?;
502            // SAFETY: this is the current architecture context and the IRQ-off
503            // CPU pin excludes scheduler and remote context mutation.
504            let architecture_context = &*context.inner.get();
505            Ok(architecture_context.capture_user_fp_state())
506        })
507    };
508    ax_cpu::interrupt::enable_irqs();
509    result
510}
511
512#[cfg(all(target_arch = "x86_64", feature = "fp-simd", feature = "uspace"))]
513pub(super) fn replace_current_user_fp_state(
514    state: ax_hal::cpu::registers::UserXstate,
515) -> Result<(), TaskError> {
516    if !ax_cpu::interrupt::irqs_enabled() || ax_hal::irq::in_irq_context() {
517        return Err(TaskError::UnsafeContext);
518    }
519    ax_cpu::interrupt::disable_irqs();
520    // SAFETY: local IRQ exclusion pins the runtime context and CPU-local FPU
521    // owner through the task-memory replacement, hardware restore, and owner
522    // publication transaction.
523    let result = unsafe {
524        with_current_cpu_pin(|cpu_pin| {
525            let context = current_runtime_context(cpu_pin).map_err(runtime_status_error)?;
526            // SAFETY: this is the current architecture context and IRQ
527            // exclusion prevents concurrent scheduler mutation.
528            let architecture_context = &mut *context.inner.get();
529            architecture_context.replace_user_fp_state(state);
530            Ok(())
531        })
532    };
533    ax_cpu::interrupt::enable_irqs();
534    result
535}
536
537pub(super) fn reset_current_user_fp_state() -> Result<(), TaskError> {
538    #[cfg(all(target_arch = "x86_64", feature = "fp-simd", feature = "uspace"))]
539    {
540        if !ax_cpu::interrupt::irqs_enabled() || ax_hal::irq::in_irq_context() {
541            return Err(TaskError::UnsafeContext);
542        }
543        ax_cpu::interrupt::disable_irqs();
544        // SAFETY: local IRQ exclusion pins the current runtime context and its
545        // CPU-local FPU owner through the reset and owner publication.
546        let result = unsafe {
547            with_current_cpu_pin(|cpu_pin| {
548                let context = current_runtime_context(cpu_pin).map_err(runtime_status_error)?;
549                // SAFETY: this is the currently executing architecture context;
550                // IRQ exclusion prevents scheduler or interrupt re-entry.
551                let architecture_context = &mut *context.inner.get();
552                architecture_context.reset_user_fp_state();
553                Ok(())
554            })
555        };
556        ax_cpu::interrupt::enable_irqs();
557        result
558    }
559    #[cfg(not(all(target_arch = "x86_64", feature = "fp-simd", feature = "uspace")))]
560    {
561        Ok(())
562    }
563}
564
565fn prepare_runtime_thread_switch<'switch>(
566    pin: &'switch CpuPin<'_>,
567    previous: &'static RuntimeContext,
568    next: &'static RuntimeContext,
569) -> (PreparedContextSwitch<'switch>, PreviousContextBinding) {
570    // `prepare_context_switch` is the single production authority for current
571    // publication, previous binding and next-unbound validation. Repeating
572    // those checks here would reread the architecture current-thread register
573    // and split one switch transaction across two facts.
574    // SAFETY: the scheduler baton pins this CPU, and both runtime contexts stay
575    // live through the raw switch and incoming tail.
576    unsafe { ax_hal::percpu::prepare_context_switch(pin, previous.header(), next.header()) }
577        .unwrap_or_else(|error| panic!("failed to prepare runtime context switch: {error}"))
578}
579
580#[cfg(all(target_arch = "riscv64", feature = "fp-simd"))]
581pub(super) fn install_initial_fp_state(context: usize, fp_state: ax_hal::cpu::registers::FpState) {
582    let context = ptr::with_exposed_provenance_mut::<RuntimeContext>(context);
583    // SAFETY: the context allocation was just created and has not been
584    // published, so this construction path exclusively owns its FP snapshot.
585    unsafe { (*(*context).inner.get()).fp_state = fp_state };
586}
587
588pub(super) unsafe fn switch_runtime_context(plan: RuntimeSwitchPlan) {
589    let previous_address_space = plan.previous_address_space();
590    let next_address_space = plan.next_address_space();
591    let same_address_space = plan.same_address_space();
592    let previous_raw = plan.previous_context().into_raw();
593    let next_raw = plan.next_context().into_raw();
594    let previous = ptr::with_exposed_provenance_mut::<RuntimeContext>(previous_raw);
595    let next = ptr::with_exposed_provenance_mut::<RuntimeContext>(next_raw);
596    // SAFETY: the active scheduler baton keeps local IRQs disabled for
597    // preparation, publication, and the naked switch tail.
598    unsafe {
599        with_current_cpu_pin(|pin| {
600            #[cfg(feature = "qperf-metrics")]
601            let qperf_prepare_entry_finished_ns =
602                crate::clock_event_runtime::monotonic_now().as_nanos();
603            // SAFETY: both handles stay live and are uniquely owned by the
604            // committed scheduler switch plan.
605            let previous_context = &*previous;
606            let next_context = &*next;
607            let previous_arch_context = &mut *previous_context.inner.get();
608            let next_arch_context = &mut *next_context.inner.get();
609            debug_assert_eq!(
610                previous_arch_context.task_anchor(),
611                Some(TaskAnchor::new(previous_context.header().as_non_null())),
612                "outgoing architecture context retained a different current header"
613            );
614            debug_assert_eq!(
615                next_arch_context.task_anchor(),
616                Some(TaskAnchor::new(next_context.header().as_non_null())),
617                "incoming architecture context retained a different current header"
618            );
619            let prepared_address_space =
620                super::address_space::prepare_runtime_address_space_switch(
621                    pin,
622                    previous_address_space,
623                    next_address_space,
624                    same_address_space,
625                    super::address_space::AddressSpaceTransitionPhase::ContextSwitch,
626                )
627                .unwrap_or_else(|status| {
628                    panic!("failed to prepare runtime address-space switch: {status:?}")
629                });
630            #[cfg(feature = "qperf-metrics")]
631            let qperf_prepare_mm_finished_ns =
632                crate::clock_event_runtime::monotonic_now().as_nanos();
633            // All CPU binding, FP and active-mm validation precedes the
634            // irreversible baton transfer and both commits.
635            let (prepared, previous_binding) =
636                prepare_runtime_thread_switch(pin, previous_context, next_context);
637            #[cfg(feature = "qperf-metrics")]
638            let qperf_prepare_binding_finished_ns =
639                crate::clock_event_runtime::monotonic_now().as_nanos();
640            assert_eq!(
641                next_arch_context.task_anchor(),
642                Some(TaskAnchor::new(prepared.next_header())),
643                "prepared switch token must belong to the next task context",
644            );
645            previous_arch_context.prepare_switch_to(next_arch_context);
646            #[cfg(feature = "qperf-metrics")]
647            let qperf_runtime_tail_started_ns =
648                crate::clock_event_runtime::monotonic_now().as_nanos();
649            #[cfg(feature = "qperf-metrics")]
650            {
651                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
652                    17,
653                    plan.qperf_prepare_started_ns(),
654                    qperf_prepare_entry_finished_ns,
655                );
656                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
657                    18,
658                    qperf_prepare_entry_finished_ns,
659                    qperf_prepare_mm_finished_ns,
660                );
661                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
662                    19,
663                    qperf_prepare_mm_finished_ns,
664                    qperf_prepare_binding_finished_ns,
665                );
666                ax_task::diagnostics::qperf_record_switch_scheduler_detail(
667                    20,
668                    qperf_prepare_binding_finished_ns,
669                    qperf_runtime_tail_started_ns,
670                );
671                ax_task::diagnostics::qperf_record_switch_phase_prepare(
672                    plan.qperf_prepare_started_ns(),
673                    qperf_runtime_tail_started_ns,
674                );
675            }
676            #[cfg(feature = "qperf-metrics")]
677            let qperf_switch_started_ns = crate::clock_event_runtime::monotonic_now().as_nanos();
678            let tail = RuntimeSwitchTail {
679                previous: previous_context.header().as_non_null(),
680                binding: previous_binding,
681                #[cfg(feature = "qperf-metrics")]
682                qperf_runtime_tail_started_ns,
683                #[cfg(feature = "qperf-metrics")]
684                qperf_switch_started_ns,
685            };
686            next_context
687                .stage_switch_tail(tail)
688                .unwrap_or_else(|status| panic!("failed to stage runtime switch tail: {status:?}"));
689            // Validate the active scheduler baton once, before the first
690            // irreversible switch side effect. The resulting move-only token
691            // retains this exact CPU pin through the active-mm commit and is
692            // then consumed by the incoming continuation handoff.
693            let switch_baton = crate::guard::prepare_scheduler_switch_baton(pin);
694            prepared_address_space.commit();
695            switch_baton.transfer();
696            // SAFETY: scheduling and IRQ exclusion remain active. Commit consumes
697            // the sole publication token after the baton transfer. The next
698            // operation is the inlined machine transfer; no checks, callbacks
699            // or destructors run between publication and the naked switch.
700            prepared.commit();
701            previous_arch_context.switch_to(next_arch_context);
702        })
703    };
704}
705
706#[cfg(test)]
707mod tests {
708    use core::mem::MaybeUninit;
709
710    use cpu_local::{CpuAreaPrefix, CpuAreaRef, CpuIndex};
711
712    use super::*;
713
714    #[test]
715    fn switch_tail_consumes_the_exact_previous_binding_once() {
716        std::thread::spawn(|| {
717            let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
718            let base = storage.as_mut_ptr() as usize;
719            storage.write(CpuAreaPrefix::initialize(CpuIndex::try_from(0).unwrap(), base).unwrap());
720            // SAFETY: the leaked prefix is initialized and remains mapped for
721            // this modeled CPU's complete process lifetime.
722            let area = unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap();
723            // SAFETY: this fresh host thread owns its CPU-local register model.
724            unsafe { cpu_local::install_cpu_area(area) }.unwrap();
725
726            let previous = RuntimeContext::allocate(
727                ax_hal::context::TaskContext::new(),
728                StackHandle::NONE,
729                InitialPreemptionState::Enabled,
730            )
731            .unwrap();
732            let next = RuntimeContext::allocate(
733                ax_hal::context::TaskContext::new(),
734                StackHandle::NONE,
735                InitialPreemptionState::Enabled,
736            )
737            .unwrap();
738
739            // SAFETY: both leaked runtime contexts remain pinned for the
740            // modeled switch, and this host thread cannot migrate.
741            unsafe {
742                cpu_local::with_cpu_pin(|pin| {
743                    let previous = &*previous;
744                    let next = &*next;
745                    cpu_local::install_bootstrap_context(pin, previous.header()).unwrap();
746                    let (prepared, binding) =
747                        cpu_local::prepare_context_switch(pin, previous.header(), next.header())
748                            .unwrap();
749                    prepared.commit();
750
751                    next.stage_switch_tail(RuntimeSwitchTail {
752                        previous: previous.header().as_non_null(),
753                        binding,
754                        #[cfg(feature = "qperf-metrics")]
755                        qperf_runtime_tail_started_ns: 0,
756                        #[cfg(feature = "qperf-metrics")]
757                        qperf_switch_started_ns: 0,
758                    })
759                    .unwrap();
760                    next.finish_switch_tail();
761                    assert!(!next.has_switch_tail());
762                    assert_eq!(previous.header.cpu_area(), None);
763                })
764            }
765            .unwrap();
766        })
767        .join()
768        .expect("modeled CPU must complete the switch tail");
769    }
770
771    #[cfg(feature = "host-test")]
772    #[test]
773    fn switch_prepare_reuses_current_register_and_pinned_area_identity() {
774        std::thread::spawn(|| {
775            let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
776            let base = storage.as_mut_ptr() as usize;
777            storage.write(CpuAreaPrefix::initialize(CpuIndex::try_from(0).unwrap(), base).unwrap());
778            // SAFETY: the leaked prefix is initialized and remains mapped for
779            // this modeled CPU's complete process lifetime.
780            let area = unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap();
781            // SAFETY: this fresh host thread owns its CPU-local register model.
782            unsafe { cpu_local::install_cpu_area(area) }.unwrap();
783
784            let previous = RuntimeContext::allocate(
785                ax_hal::context::TaskContext::new(),
786                StackHandle::NONE,
787                InitialPreemptionState::Enabled,
788            )
789            .unwrap();
790            let next = RuntimeContext::allocate(
791                ax_hal::context::TaskContext::new(),
792                StackHandle::NONE,
793                InitialPreemptionState::Enabled,
794            )
795            .unwrap();
796
797            // SAFETY: both leaked contexts remain pinned while the modeled CPU
798            // validates and then rolls back this uncommitted switch.
799            unsafe {
800                cpu_local::with_cpu_pin(|pin| {
801                    let previous = &*previous;
802                    let next = &*next;
803                    cpu_local::install_bootstrap_context(pin, previous.header()).unwrap();
804                    cpu_local::host_test::reset_register_read_counts();
805
806                    let (prepared, _binding) = prepare_runtime_thread_switch(pin, previous, next);
807                    let reads = cpu_local::host_test::register_read_counts();
808                    assert_eq!(
809                        reads.current_context, 1,
810                        "switch preparation must validate current publication exactly once"
811                    );
812                    assert_eq!(
813                        reads.binding_observations, 1,
814                        "switch preparation must observe the outgoing binding exactly once"
815                    );
816                    assert_eq!(
817                        reads.initialized_area_validations, 0,
818                        "switch preparation must reuse the area identity carried by the CPU pin"
819                    );
820                    drop(prepared);
821                })
822            }
823            .unwrap();
824        })
825        .join()
826        .expect("modeled CPU must complete switch preparation");
827    }
828
829    #[test]
830    fn current_runtime_context_trusts_switch_binding_publication() {
831        std::thread::spawn(|| {
832            let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
833            let base = storage.as_mut_ptr() as usize;
834            storage.write(CpuAreaPrefix::initialize(CpuIndex::try_from(0).unwrap(), base).unwrap());
835            // SAFETY: the leaked prefix is initialized and remains mapped for
836            // this modeled CPU's complete process lifetime.
837            let area = unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap();
838            // SAFETY: this fresh host thread owns its CPU-local register model.
839            unsafe { cpu_local::install_cpu_area(area) }.unwrap();
840
841            let current = RuntimeContext::allocate(
842                ax_hal::context::TaskContext::new(),
843                StackHandle::NONE,
844                InitialPreemptionState::Enabled,
845            )
846            .unwrap();
847
848            // SAFETY: the leaked runtime context remains pinned while this
849            // host thread validates the modeled current publication.
850            unsafe {
851                cpu_local::with_cpu_pin(|pin| {
852                    let expected = &*current;
853                    cpu_local::install_bootstrap_context(pin, expected.header()).unwrap();
854                    cpu_local::host_test::reset_register_read_counts();
855
856                    let observed = current_runtime_context(pin).unwrap();
857                    assert!(ptr::eq(observed, expected));
858                    let reads = cpu_local::host_test::register_read_counts();
859                    assert_eq!(reads.current_context, 1);
860                    assert_eq!(
861                        reads.binding_observations, 0,
862                        "the pinned current lookup must trust switch-time binding validation"
863                    );
864                })
865            }
866            .unwrap();
867        })
868        .join()
869        .expect("modeled CPU must complete current lookup");
870    }
871
872    #[test]
873    fn current_publication_queries_do_not_resample_cpu_area() {
874        std::thread::spawn(|| {
875            let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
876            let base = storage.as_mut_ptr() as usize;
877            storage.write(CpuAreaPrefix::initialize(CpuIndex::try_from(0).unwrap(), base).unwrap());
878            // SAFETY: the leaked prefix is initialized and remains mapped for
879            // this modeled CPU's complete process lifetime.
880            let area = unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap();
881            // SAFETY: this fresh host thread owns its CPU-local register model.
882            unsafe { cpu_local::install_cpu_area(area) }.unwrap();
883
884            let current = RuntimeContext::allocate(
885                ax_hal::context::TaskContext::new(),
886                StackHandle::NONE,
887                InitialPreemptionState::Enabled,
888            )
889            .unwrap();
890
891            // SAFETY: the leaked runtime context remains pinned while this
892            // host thread reads its immutable scheduler publication.
893            unsafe {
894                cpu_local::with_cpu_pin(|pin| {
895                    let current = &*current;
896                    cpu_local::install_bootstrap_context(pin, current.header()).unwrap();
897                    cpu_local::host_test::reset_register_read_counts();
898
899                    assert_eq!(
900                        scheduler_current_thread_publication(),
901                        CurrentThreadPublication::NONE,
902                    );
903                    let reads = cpu_local::host_test::register_read_counts();
904                    assert_eq!(reads.current_context, 1);
905                    assert_eq!(
906                        reads.cpu_base, 0,
907                        "current publication lookup must not resample the CPU-area base",
908                    );
909
910                    cpu_local::host_test::reset_register_read_counts();
911                    assert_eq!(scheduler_current_thread_identity(), ThreadIdentityV1::NONE);
912                    let reads = cpu_local::host_test::register_read_counts();
913                    assert_eq!(reads.current_context, 1);
914                    assert_eq!(
915                        reads.cpu_base, 0,
916                        "current identity lookup must not resample the CPU-area base",
917                    );
918                })
919            }
920            .unwrap();
921        })
922        .join()
923        .expect("modeled CPU must classify its current runtime context");
924    }
925}