Skip to main content

starnix_syscall_loop/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![recursion_limit = "256"]
6
7use anyhow::{Error, format_err};
8use extended_pstate::ExtendedPstatePointer;
9use starnix_core::arch::execution::new_syscall;
10use starnix_core::signals::{SignalInfo, dequeue_signal, force_signal, prepare_to_restart_syscall};
11use starnix_core::task::{CurrentTask, ExceptionResult, ExitStatus, SeccompStateValue, TaskFlags};
12use starnix_logging::{
13    CATEGORY_STARNIX, NAME_HANDLE_EXCEPTION, NAME_RESTRICTED_KICK, NAME_RUN_TASK, log_error,
14    log_syscall, log_trace, log_warn, set_current_task_info,
15};
16use starnix_registers::RestrictedState;
17
18use starnix_syscalls::SyscallResult;
19use starnix_syscalls::decls::{Syscall, SyscallDecl};
20use starnix_uapi::errno;
21use starnix_uapi::errors::Errno;
22use starnix_uapi::signals::{SIGKILL, SIGSEGV};
23use zerocopy::FromZeros;
24
25mod table;
26
27pub fn enter(current_task: &mut CurrentTask) -> ExitStatus {
28    // Zircon will populate this report on restricted exception exits. Initialize it to all zero
29    // since we're just reserving storage.
30    let mut exception_report = zx::sys::zx_exception_report_t::new_zeroed();
31    match RestrictedState::bind_and_map(
32        &mut current_task.thread_state.registers,
33        &mut exception_report,
34    ) {
35        Ok(restricted_state) => {
36            match run_task(current_task, restricted_state.bound_state.as_ptr(), &exception_report) {
37                Ok(ok) => ok,
38                Err(error) => {
39                    log_warn!("Died unexpectedly from {error:?}! treating as SIGKILL");
40                    ExitStatus::Kill(SignalInfo::kernel(SIGKILL))
41                }
42            }
43        }
44        Err(error) => {
45            log_error!("failed to map mode state vmo, {error:?}! treating as SIGKILL");
46            ExitStatus::Kill(SignalInfo::kernel(SIGKILL))
47        }
48    }
49}
50
51unsafe extern "C" {
52    /// Enters restricted mode in a loop, invoking `restricted_exit_callback_c` on each restricted
53    /// exit until the callback returns `false` or `zx_restricted_enter` fails.
54    ///
55    /// # Safety
56    ///
57    /// * `options` must be a valid `zx_restricted_enter` options value.
58    /// * `restricted_exit_callback_context` must point to a valid [`RestrictedEnterContext`] that
59    ///   is exclusively accessed by the current thread and outlives this call.
60    /// * `restricted_state` must point to the current thread's bound and mapped
61    ///   `zx_restricted_state_t`, which must outlive this call.
62    /// * `extended_pstate_ptr_ptr` must point to a valid [`ExtendedPstatePointer`] whose backing
63    ///   storage matches the architectural mode selected by `restricted_state` and outlives this
64    ///   call.
65    // rustc doesn't like RestrictedEnterContext for FFI but we're just passing it back to
66    // ourselves with extra steps.
67    #[allow(improper_ctypes)]
68    fn restricted_enter_loop(
69        options: u32,
70        restricted_exit_callback_context: *mut RestrictedEnterContext<'_>,
71        restricted_state: *mut zx::sys::zx_restricted_state_t,
72        extended_pstate_ptr_ptr: *mut ExtendedPstatePointer,
73    ) -> zx::sys::zx_status_t;
74}
75
76const RESTRICTED_ENTER_OPTIONS: u32 = 0;
77
78struct RestrictedEnterContext<'a> {
79    current_task: &'a mut CurrentTask,
80    error_context: Option<ErrorContext>,
81    exit_status: Result<ExitStatus, Error>,
82    exception_report_raw: *const zx::sys::zx_exception_report_t,
83}
84
85/// Runs the `current_task` to completion.
86///
87/// The high-level flow of this function looks as follows:
88///
89///   1. Write the restricted state for the current thread to set it up to enter into the restricted
90///      (Linux) part of the address space.
91///   2. Enter restricted mode.
92///   3. Return from restricted mode, reading out the new state of the restricted mode execution.
93///      This state contains the thread's restricted register state, which is used to determine
94///      which system call to dispatch.
95///   4. Dispatch the system call.
96///   5. Handle pending signals.
97///   6. Goto 1.
98fn run_task(
99    current_task: &mut CurrentTask,
100    restricted_state_ptr: *mut zx::sys::zx_restricted_state_t,
101    exception_report_raw: *const zx::sys::zx_exception_report_t,
102) -> Result<ExitStatus, Error> {
103    set_current_task_info(
104        current_task.task.command(),
105        current_task.task.thread_group().read().leader_command(),
106        current_task.task.pid.id,
107        current_task.tid.id,
108    );
109
110    fuchsia_trace::duration!(CATEGORY_STARNIX, NAME_RUN_TASK);
111
112    // This tracks the last failing system call for debugging purposes.
113    let error_context = None;
114
115    // We need to check for exit once, before the task starts executing, in case
116    // the task has already been sent a signal that will cause it to exit.
117    if let Some(exit_status) = process_completed_restricted_exit(current_task, &error_context)? {
118        return Ok(exit_status);
119    }
120
121    let mut restricted_enter_context = RestrictedEnterContext {
122        current_task,
123        error_context,
124        exit_status: Err(errno!(ENOEXEC).into()),
125        exception_report_raw,
126    };
127
128    loop {
129        let mut extended_pstate_ptr =
130            restricted_enter_context.current_task.thread_state.extended_pstate.as_ptr();
131        // SAFETY:
132        // * `RESTRICTED_ENTER_OPTIONS` is a valid `zx_restricted_enter` options value.
133        // * `restricted_enter_context` is exclusively accessed by the current thread, outlives the
134        //   call, and is passed as a raw pointer to avoid asserting an intermediate `&mut` borrow.
135        // * `restricted_state_ptr` points to the current thread's bound and mapped
136        //   `zx_restricted_state_t`, which outlives this function.
137        // * `extended_pstate_ptr` is refreshed on each iteration from `current_task.thread_state`
138        //   so its backing storage matches the architectural mode in `restricted_state_ptr`.
139        let restricted_enter_status = zx::Status::ok(unsafe {
140            restricted_enter_loop(
141                RESTRICTED_ENTER_OPTIONS,
142                &raw mut restricted_enter_context,
143                restricted_state_ptr,
144                &raw mut extended_pstate_ptr,
145            )
146        });
147        match restricted_enter_status {
148            Ok(()) => break,
149            Err(zx::Status::BAD_STATE) => {
150                // `zx_restricted_enter` returns `ZX_ERR_BAD_STATE` if the restricted register
151                // state fails architectural pre-entry validation (e.g. an invalid user IP or flags).
152                // Deliver a forced `SIGSEGV` (`SI_KERNEL`) and process the completed exit to
153                // dispatch a signal handler or terminate the task.
154                force_signal(
155                    restricted_enter_context.current_task,
156                    SignalInfo::forced(SIGSEGV),
157                    /* restricted_exception = */ None,
158                );
159                if let Some(exit_status) = process_completed_restricted_exit(
160                    restricted_enter_context.current_task,
161                    &restricted_enter_context.error_context,
162                )? {
163                    return Ok(exit_status);
164                }
165            }
166            Err(status) => {
167                // If restricted_enter_loop failed, it means that we failed to satisfy
168                // a prerequisite of zx_restricted_enter which should never happen.
169                log_error!(
170                    "restricted_enter_loop failed: {}, register state: {:?}",
171                    status,
172                    restricted_enter_context.current_task.thread_state.registers
173                );
174                break;
175            }
176        }
177    }
178    restricted_enter_context.exit_status
179}
180
181/// Entry point invoked from the architecture-specific `restricted_return_loop` assembly
182/// trampoline upon each exit from restricted mode.
183///
184/// # Safety
185///
186/// * `context` must point to the valid [`RestrictedEnterContext`] passed to
187///   `restricted_enter_loop` on the current thread.
188/// * `extended_pstate_ptr_ptr` must point to the valid [`ExtendedPstatePointer`] passed to
189///   `restricted_enter_loop` on the current thread.
190#[allow(private_interfaces)]
191#[unsafe(no_mangle)]
192pub unsafe extern "C" fn restricted_exit_callback_c(
193    context: *mut RestrictedEnterContext<'_>,
194    reason_code: zx::sys::zx_restricted_reason_t,
195    extended_pstate_ptr_ptr: *mut ExtendedPstatePointer,
196) -> bool {
197    // SAFETY:
198    // `context` is a pointer to a `RestrictedEnterContext` that was passed to
199    // `restricted_enter_loop`.
200    //  `extended_pstate_ptr` is a pointer to the ExtendedPstatePointer instance
201    //  that was passed to `restricted_enter_loop.`
202    // Our restricted return assembly and Zircon together guarantee that this
203    // thread has exclusive access to these variables.
204    let (restricted_context, extended_pstate_ptr) =
205        unsafe { (&mut *context, extended_pstate_ptr_ptr.as_mut_unchecked()) };
206    restricted_exit_callback(
207        reason_code,
208        restricted_context.current_task,
209        &mut restricted_context.error_context,
210        &mut restricted_context.exit_status,
211        extended_pstate_ptr,
212        restricted_context.exception_report_raw,
213    )
214}
215
216fn restricted_exit_callback(
217    reason_code: zx::sys::zx_restricted_reason_t,
218    current_task: &mut CurrentTask,
219    error_context: &mut Option<ErrorContext>,
220    exit_status: &mut Result<ExitStatus, Error>,
221    extended_pstate_ptr: &mut ExtendedPstatePointer,
222    exception_report_raw: *const zx::sys::zx_exception_report_t,
223) -> bool {
224    debug_assert_eq!(
225        current_task.thread_state.restart_code, None,
226        "restart_code should only ever be Some() in normal mode",
227    );
228
229    let ret = match process_restricted_exit(
230        reason_code,
231        current_task,
232        error_context,
233        exception_report_raw,
234    ) {
235        Ok(None) => {
236            // Keep going!
237
238            *extended_pstate_ptr = current_task.thread_state.extended_pstate.as_ptr();
239
240            true
241        }
242        Ok(Some(completed_exit_status)) => {
243            *exit_status = Ok(completed_exit_status);
244            false
245        }
246        Err(error) => {
247            *exit_status = Err(error);
248            false
249        }
250    };
251
252    debug_assert_eq!(
253        current_task.thread_state.restart_code, None,
254        "restart_code should only ever be Some() in normal mode",
255    );
256
257    ret
258}
259
260fn process_restricted_exit(
261    reason_code: zx::sys::zx_restricted_reason_t,
262    current_task: &mut CurrentTask,
263    error_context: &mut Option<ErrorContext>,
264    exception_report_raw: *const zx::sys::zx_exception_report_t,
265) -> Result<Option<ExitStatus>, Error> {
266    #[cfg(target_arch = "aarch64")]
267    current_task.thread_state.registers.sync_stack_ptr();
268
269    match reason_code {
270        zx::sys::ZX_RESTRICTED_REASON_SYSCALL => {
271            execute_syscall(current_task, error_context);
272        }
273        zx::sys::ZX_RESTRICTED_REASON_EXCEPTION => {
274            if !current_task.is_exitted() {
275                fuchsia_trace::duration!(CATEGORY_STARNIX, NAME_HANDLE_EXCEPTION);
276                // SAFETY: `exception_report_raw` was written by Zircon during this restricted exit.
277                let exception_report =
278                    unsafe { zx::ExceptionReport::from_raw(*exception_report_raw) };
279                let exception_result = current_task.process_exception(&exception_report);
280                process_completed_exception(current_task, exception_result, exception_report);
281            }
282        }
283        zx::sys::ZX_RESTRICTED_REASON_KICK => {
284            fuchsia_trace::instant!(
285                CATEGORY_STARNIX,
286                NAME_RESTRICTED_KICK,
287                fuchsia_trace::Scope::Thread
288            );
289            // Fall through to the post-syscall / post-exception handling logic. We were likely
290            // kicked because a signal is pending deliver or the task has exited. Spurious kicks are
291            // also possible.
292        }
293        _ => {
294            return Err(format_err!("Received unexpected restricted reason code: {}", reason_code));
295        }
296    }
297
298    // Most restricted exits (e.g. syscalls that leave no signal, stop, exit or restart behind) give
299    // `process_completed_restricted_exit` nothing to do: only run it if work may be pending.
300    if !has_pending_exit_work(current_task) {
301        return Ok(None);
302    }
303
304    if let Some(exit_status) = process_completed_restricted_exit(current_task, &error_context)? {
305        return Ok(Some(exit_status));
306    }
307
308    Ok(None)
309}
310
311fn process_completed_exception(
312    current_task: &mut CurrentTask,
313    exception_result: ExceptionResult,
314    restricted_exception: zx::ExceptionReport,
315) {
316    match exception_result {
317        ExceptionResult::Handled => {}
318        ExceptionResult::Signal(signal) => {
319            force_signal(current_task, signal, Some(restricted_exception));
320        }
321    }
322}
323
324/// Contains context to track the most recently failing system call.
325///
326/// When a task exits with a non-zero exit code, this context is logged to help debugging which
327/// system call may have triggered the failure.
328#[derive(Debug)]
329pub struct ErrorContext {
330    /// The system call that failed.
331    pub syscall: Syscall,
332
333    /// The error that was returned for the system call.
334    pub error: Errno,
335}
336
337/// Executes the syscall for `current_task`.
338///
339/// Stores an `ErrorContext` in `error_context` if the system call returned an error, and leaves it
340/// unchanged otherwise, so that the most recent failure is preserved across successful syscalls.
341///
342/// `error_context` is an out-parameter rather than an `Option<ErrorContext>` return value because
343/// of that preservation requirement: a returned `None` must not overwrite the previous
344/// `ErrorContext`, so the return value cannot be constructed directly in `*error_context`. It would
345/// instead go through a separate hidden return slot that is written on every syscall and copied
346/// into `*error_context` on error. With the out-parameter, the common `Ok` path does not touch
347/// `error_context` at all.
348#[inline(never)] // Inlining this function breaks the CFI directives used to unwind into user code.
349pub fn execute_syscall(current_task: &mut CurrentTask, error_context: &mut Option<ErrorContext>) {
350    current_task.thread_state.registers.save_registers_for_restart();
351
352    current_task.ptrace().on_syscall_enter();
353    // A tracee killed (e.g., via `SIGKILL` or `PTRACE_KILL`) while stopped in `PTRACE_SYSCALL`
354    // entry is marked `EXITED` and woken from `on_syscall_enter` without executing the syscall.
355    if current_task.is_exitted() {
356        return;
357    }
358
359    let syscall_decl = SyscallDecl::from_number(
360        current_task.thread_state.registers.syscall_register(),
361        current_task.thread_state.arch_width(),
362    );
363
364    let _syscall_trace = starnix_core::task::begin_syscall_trace(|| syscall_decl.trace_name());
365    let syscall = new_syscall(syscall_decl, current_task);
366
367    log_syscall!(current_task, "{syscall:?}");
368
369    let _lockup_detector_guard = starnix_core::task::ThreadLockupDetector::track();
370    let result: Result<SyscallResult, Errno> =
371        if current_task.seccomp_filter_state.get() != SeccompStateValue::None {
372            // Inlined fast path for seccomp, so that we don't incur the cost
373            // of a method call when running the filters.
374            if let Some(res) = current_task.run_seccomp_filters(&syscall) {
375                res
376            } else {
377                table::dispatch_syscall(current_task, &syscall)
378            }
379        } else {
380            table::dispatch_syscall(current_task, &syscall)
381        };
382
383    current_task.trigger_delayed_releaser();
384
385    let is_error = match result {
386        Ok(return_value) => {
387            log_syscall!(current_task, "-> {:#x}", return_value.value());
388            current_task.thread_state.registers.set_return_register(return_value.value());
389            false
390        }
391        Err(errno) => {
392            log_syscall!(current_task, "!-> {errno}");
393            if errno.is_restartable() {
394                current_task.thread_state.restart_code = Some(errno.code);
395            }
396            current_task.thread_state.registers.set_return_register(errno.return_value());
397            *error_context = Some(ErrorContext { error: errno, syscall });
398            true
399        }
400    };
401
402    current_task.ptrace().on_syscall_exit(is_error);
403}
404
405/// Returns whether `process_completed_restricted_exit` may have work to do for `current_task`.
406///
407/// When this returns `false`, `process_completed_restricted_exit` would return `Ok(None)` without
408/// any side effect, because each of its steps is guarded by the state checked here:
409///
410/// * `dequeue_signal` is skipped for exited tasks and otherwise returns early unless
411///   `should_check_for_pending_signals()`, i.e. unless signals or kernel signals (e.g. freeze) are
412///   pending for the task or its thread group, or a temporary signal mask is set. The latter also
413///   guards restoring the temporary signal mask.
414/// * `prepare_to_restart_syscall` does nothing without a `restart_code`.
415/// * `exit_status()` is `None` unless the task is marked `EXITED`.
416/// * `block_if_stopped()` does nothing unless the task or its thread group is stopping or stopped
417///   (group stop or ptrace stop).
418///
419/// These are the same relaxed atomic loads that the full path performs, so this cannot observe a
420/// state that the full path could not have observed. A change made concurrently by another thread
421/// is handled at the next restricted exit, as when it lands just after the full path's checks.
422#[inline(always)]
423fn has_pending_exit_work(current_task: &CurrentTask) -> bool {
424    current_task.thread_state.restart_code.is_some()
425        || current_task.is_exitted()
426        || current_task.should_check_for_pending_signals()
427        || current_task.load_stopped().is_stopping_or_stopped()
428        || current_task.thread_group().load_stopped().is_stopping_or_stopped()
429}
430
431/// Finishes `current_task` updates after a restricted mode exit such as a syscall, exception, or kick.
432///
433/// Returns an `ExitStatus` if the task is meant to exit.
434///
435/// `process_restricted_exit` skips this function when `has_pending_exit_work` returns `false`, so
436/// any work added here must be guarded by state that `has_pending_exit_work` checks.
437pub fn process_completed_restricted_exit(
438    current_task: &mut CurrentTask,
439    error_context: &Option<ErrorContext>,
440) -> Result<Option<ExitStatus>, Errno> {
441    let result;
442    loop {
443        // Checking for a signal might cause the task to exit, so check before processing exit
444        {
445            {
446                if !current_task.is_exitted() {
447                    dequeue_signal(current_task);
448                }
449                // The syscall may need to restart for a non-signal-related
450                // reason. This call does nothing if we aren't restarting.
451                prepare_to_restart_syscall(&mut current_task.thread_state, None);
452            }
453        }
454
455        let exit_status = current_task.exit_status();
456        if let Some(exit_status) = exit_status {
457            log_trace!("exiting with status {:?}", exit_status);
458            if let Some(error_context) = error_context {
459                match exit_status {
460                    ExitStatus::Exit(value) if value == 0 => {}
461                    _ => {
462                        log_trace!(
463                            "last failing syscall before exit: {:?}, failed with {:?}",
464                            error_context.syscall,
465                            error_context.error
466                        );
467                    }
468                };
469            }
470
471            result = Some(exit_status);
472            break;
473        } else {
474            // Block a stopped process after it's had a chance to handle signals, since a signal might
475            // cause it to stop.
476            if current_task.block_if_stopped() {
477                // If the task was stopped and has now woken up (e.g., via SIGCONT or PTRACE_CONT),
478                // loop back to process any pending signals before returning to userspace.
479                continue;
480            }
481            // Restore signal mask if necessary, before returning to userspace.
482            if current_task.flags().contains(TaskFlags::TEMPORARY_SIGNAL_MASK) {
483                current_task.write().restore_signal_mask();
484                if current_task.should_check_for_pending_signals() {
485                    continue;
486                }
487            }
488
489            result = None;
490            break;
491        }
492    }
493
494    if let Some(ExitStatus::CoreDump(signal_info)) = &result {
495        if current_task.flags().contains(TaskFlags::DUMP_ON_EXIT) {
496            // Avoid taking a backtrace if the signal was sent by the same task.
497            if !signal_info.is_sent_by(&current_task.weak_task()) {
498                // Request a backtrace before reporting the crash to increase chance of a backtrace
499                // in logs. This call is kept as far up in the call stack as possible to avoid
500                // additional frames that are always the same and not relevant to users.
501                // TODO(https://fxbug.dev/356732164) collect a backtrace ourselves
502                debug::backtrace_request_current_thread();
503            }
504
505            if let Some(pending_report) =
506                current_task.kernel().crash_reporter.begin_crash_report(&current_task)
507            {
508                current_task.kernel().crash_reporter.handle_core_dump(
509                    &current_task,
510                    signal_info,
511                    pending_report,
512                );
513            }
514        }
515    }
516    return Ok(result);
517}
518
519#[cfg(all(test, target_arch = "aarch64"))]
520mod tests {
521    use super::*;
522    use core::arch::asm;
523    use extended_pstate::{
524        ExtendedAarch32PstateState, ExtendedPstatePointer, ExtendedPstateState,
525        restore_extended_pstate, save_extended_aarch32_pstate, save_extended_pstate,
526    };
527
528    unsafe extern "C" {
529        fn zx_vmar_root_self() -> zx::sys::zx_handle_t;
530    }
531
532    /// Seeds the hardware extended processor state from `seed_pstate_ptr`, invokes
533    /// `restricted_enter_loop` with `options = u32::MAX` (so `.restricted_enter_loop_top`
534    /// dispatches the CPSR-dependent extended processor state restore and `zx_restricted_enter`
535    /// immediately returns `ZX_ERR_INVALID_ARGS`), and saves the resulting hardware extended
536    /// processor state into `out_pstate_ptr`.
537    ///
538    /// # Safety
539    ///
540    /// * `seed_pstate_ptr` and `out_pstate_ptr` must point to valid `ExtendedPstatePointer`s
541    ///   backed by 64-bit `ExtendedPstateState` storage.
542    /// * `restricted_state` must point to a valid `zx_restricted_state_t`.
543    /// * `in_pstate_ptr` must point to a valid `ExtendedPstatePointer` whose active union field
544    ///   matches the architectural mode selected by `(*restricted_state).cpsr`.
545    unsafe fn call_restricted_enter_loop_and_save_pstate(
546        seed_pstate_ptr: *mut ExtendedPstatePointer,
547        restricted_state: *mut zx::sys::zx_restricted_state_t,
548        in_pstate_ptr: *mut ExtendedPstatePointer,
549        out_pstate_ptr: *mut ExtendedPstatePointer,
550    ) -> zx::sys::zx_status_t {
551        let status: zx::sys::zx_status_t;
552        // SAFETY:
553        // * `restore_extended_pstate`, `restricted_enter_loop`, and `save_extended_pstate` are
554        //   invoked back-to-back in a single assembly block so compiler-generated code cannot
555        //   clobber volatile SIMD/SVE registers between `restricted_enter_loop` and
556        //   `save_extended_pstate`.
557        // * `x20`..`x24` are callee-saved in AAPCS64 and preserved across all three calls.
558        // * All caller-saved registers (`clobber_abi("C")`) and callee-saved SIMD registers
559        //   (`v8`..`v15`) are declared clobbered, and `fpcr`/`fpsr` are reset to zero before
560        //   returning.
561        unsafe {
562            asm!(
563                "mov x0, x20",
564                "bl {restore_fn}",
565                "mov w0, #-1",
566                "mov x1, xzr",
567                "mov x2, x21",
568                "mov x3, x22",
569                "bl {enter_loop_fn}",
570                "mov w24, w0",
571                "mov x0, x23",
572                "bl {save_fn}",
573                "msr fpcr, xzr",
574                "msr fpsr, xzr",
575                restore_fn = sym restore_extended_pstate,
576                enter_loop_fn = sym restricted_enter_loop,
577                save_fn = sym save_extended_pstate,
578                in("x20") seed_pstate_ptr,
579                in("x21") restricted_state,
580                in("x22") in_pstate_ptr,
581                in("x23") out_pstate_ptr,
582                out("w24") status,
583                clobber_abi("C"),
584                out("v8") _, out("v9") _, out("v10") _, out("v11") _,
585                out("v12") _, out("v13") _, out("v14") _, out("v15") _,
586            );
587        }
588        status
589    }
590
591    #[test]
592    fn test_restricted_enter_loop_restores_aarch64_and_aarch32_pstate() {
593        assert_eq!(std::mem::offset_of!(zx::sys::zx_restricted_state_t, cpsr), 34 * 8);
594
595        let mut qregs64 = [0u128; 32];
596        for (i, q) in qregs64.iter_mut().enumerate() {
597            *q = ((i as u128 + 1) << 64) | (0x0102_0304_0506_0708_u128 + i as u128);
598        }
599        // Architecturally defined RW bits: FZ (bit 24) in FPCR; IOC | OFC | IXC (0x15) in FPSR.
600        let fpcr64: u32 = 1 << 24;
601        let fpsr64: u32 = 0x15;
602        let mut pregs64 = [0u16; 16];
603        for (i, p) in pregs64.iter_mut().enumerate() {
604            *p = 0x5a00 | (i as u16 + 1);
605        }
606        let ffr64: u16 = 0x00ff;
607
608        let mut state64 = ExtendedPstateState::default();
609        state64.set_arm64_state(&qregs64, fpsr64, fpcr64);
610        if state64.has_arm64_sve() {
611            state64.set_arm64_sve_state(&pregs64, ffr64);
612        }
613
614        let mut zero_seed = ExtendedPstateState::default();
615        let mut zero_seed_ptr = ExtendedPstatePointer { extended_pstate: &raw mut zero_seed };
616        let mut in_pstate64_ptr = ExtendedPstatePointer { extended_pstate: &raw mut state64 };
617        let mut saved_after_64 = ExtendedPstateState::default();
618        let mut saved_after_64_ptr =
619            ExtendedPstatePointer { extended_pstate: &raw mut saved_after_64 };
620
621        let mut restricted_state = zx::sys::zx_restricted_state_t::default();
622        restricted_state.cpsr = 0;
623
624        // SAFETY: All pointers reference valid stack-allocated state and `cpsr == 0` selects the
625        // 64-bit `ExtendedPstateState` union arm.
626        let status = unsafe {
627            call_restricted_enter_loop_and_save_pstate(
628                &raw mut zero_seed_ptr,
629                &raw mut restricted_state,
630                &raw mut in_pstate64_ptr,
631                &raw mut saved_after_64_ptr,
632            )
633        };
634        assert_eq!(status, zx::sys::ZX_ERR_INVALID_ARGS);
635        assert_eq!(saved_after_64.get_arm64_qregs(), &qregs64);
636        assert_eq!(saved_after_64.get_arm64_fpcr(), fpcr64);
637        assert_eq!(saved_after_64.get_arm64_fpsr(), fpsr64);
638        if state64.has_arm64_sve() {
639            assert_eq!(saved_after_64.get_arm64_p_regs(), &pregs64);
640            assert_eq!(saved_after_64.get_arm64_ffr(), ffr64);
641        }
642
643        // Allocate a 2-page VMAR where only the first page is mapped and the second page is an
644        // unmapped guard page, placing `ExtendedAarch32PstateState` at the very end of the first
645        // page (`page_end - size_of::<ExtendedAarch32PstateState>()`). If `.restricted_enter_loop_top`
646        // mistakenly called the 64-bit `restore_extended_pstate` when `CPSR.M[4]` (`0x10`) is set,
647        // it would read 283 bytes into the unmapped guard page and fault.
648        // SAFETY: `zx_vmar_root_self()` returns the process's valid root VMAR handle.
649        let root_vmar = unsafe { zx::Unowned::<zx::Vmar>::from_raw_handle(zx_vmar_root_self()) };
650        let page_size = zx::system_get_page_size() as usize;
651        let (sub_vmar, _sub_vmar_base) = root_vmar
652            .allocate(
653                0,
654                2 * page_size,
655                zx::VmarFlags::CAN_MAP_READ
656                    | zx::VmarFlags::CAN_MAP_WRITE
657                    | zx::VmarFlags::CAN_MAP_SPECIFIC,
658            )
659            .expect("allocate 2-page VMAR");
660        let vmo = zx::Vmo::create(page_size as u64).expect("create 1-page VMO");
661        let page_start = sub_vmar
662            .map(
663                0,
664                &vmo,
665                0,
666                page_size,
667                zx::VmarFlags::PERM_READ | zx::VmarFlags::PERM_WRITE | zx::VmarFlags::SPECIFIC,
668            )
669            .expect("map first page");
670        let page_end = page_start + page_size;
671        let state32_addr = page_end - std::mem::size_of::<ExtendedAarch32PstateState>();
672        let state32_ptr: *mut ExtendedAarch32PstateState =
673            std::ptr::without_provenance_mut(state32_addr);
674        assert!(state32_ptr.is_aligned());
675
676        let mut qregs32 = [0u128; 16];
677        for (i, q) in qregs32.iter_mut().enumerate() {
678            *q = 0xfedc_ba98_7654_3210_u128 ^ ((i as u128 + 1) * 0x1111_2222_3333_4444_u128);
679        }
680        // Architecturally defined RW bits: DN | RMode=RP in FPCR; DZC | UFC (0x0a) in FPSR.
681        let fpcr32: u32 = (1 << 25) | (1 << 22);
682        let fpsr32: u32 = 0x0a;
683
684        let mut init32_from_64 = ExtendedPstateState::with_sve(false);
685        let mut init32_qregs = [0u128; 32];
686        init32_qregs[..16].copy_from_slice(&qregs32);
687        init32_from_64.set_arm64_state(&init32_qregs, fpsr32, fpcr32);
688        let mut init32_from_64_ptr =
689            ExtendedPstatePointer { extended_pstate: &raw mut init32_from_64 };
690        let mut in_pstate32_ptr = ExtendedPstatePointer { extended_aarch32_pstate: state32_ptr };
691
692        // SAFETY: `state32_ptr` lies entirely within the mapped first page of `sub_vmar` and is
693        // 16-byte aligned.
694        unsafe {
695            state32_ptr.write(ExtendedAarch32PstateState::default());
696            asm!(
697                "mov x0, x20",
698                "bl {restore_fn}",
699                "mov x0, x21",
700                "bl {save_aarch32_fn}",
701                "msr fpcr, xzr",
702                "msr fpsr, xzr",
703                restore_fn = sym restore_extended_pstate,
704                save_aarch32_fn = sym save_extended_aarch32_pstate,
705                in("x20") &raw mut init32_from_64_ptr,
706                in("x21") &raw mut in_pstate32_ptr,
707                clobber_abi("C"),
708                out("v8") _, out("v9") _, out("v10") _, out("v11") _,
709                out("v12") _, out("v13") _, out("v14") _, out("v15") _,
710            );
711            let state32_ref = &*state32_ptr;
712            assert_eq!(state32_ref.get_arm32_qregs(), &qregs32);
713            assert_eq!(state32_ref.get_arm32_fpcr(), fpcr32);
714            assert_eq!(state32_ref.get_arm32_fpsr(), fpsr32);
715        }
716
717        restricted_state.cpsr = zx::sys::ZX_REG_CPSR_ARCH_32_MASK as u32;
718        let mut saved_after_32 = ExtendedPstateState::default();
719        let mut saved_after_32_ptr =
720            ExtendedPstatePointer { extended_pstate: &raw mut saved_after_32 };
721
722        // Seed `q0..q31` with `state64` immediately before invoking `restricted_enter_loop` in
723        // AArch32 mode so we can verify that `restore_extended_aarch32_pstate` updates `q0..q15`,
724        // `fpcr`, and `fpsr` while leaving `q16..q31` untouched and without faulting on the guard
725        // page immediately after `state32_ptr`.
726        // SAFETY: `in_pstate32_ptr` points to the valid `ExtendedAarch32PstateState` at the end of
727        // the mapped page, matching `restricted_state.cpsr`'s `ZX_REG_CPSR_ARCH_32_MASK` bit.
728        let status = unsafe {
729            call_restricted_enter_loop_and_save_pstate(
730                &raw mut in_pstate64_ptr,
731                &raw mut restricted_state,
732                &raw mut in_pstate32_ptr,
733                &raw mut saved_after_32_ptr,
734            )
735        };
736        assert_eq!(status, zx::sys::ZX_ERR_INVALID_ARGS);
737        assert_eq!(&saved_after_32.get_arm64_qregs()[..16], &qregs32);
738        assert_eq!(&saved_after_32.get_arm64_qregs()[16..], &qregs64[16..]);
739        assert_eq!(saved_after_32.get_arm64_fpcr(), fpcr32);
740        assert_eq!(saved_after_32.get_arm64_fpsr(), fpsr32);
741
742        // SAFETY: `state32_ptr` is not accessed after destroying `sub_vmar`.
743        unsafe {
744            sub_vmar.destroy().expect("destroy 2-page VMAR");
745        }
746    }
747}