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
5use anyhow::{Error, format_err};
6use extended_pstate::ExtendedPstatePointer;
7use starnix_core::arch::execution::new_syscall;
8use starnix_core::ptrace::{StopState, ptrace_syscall_enter, ptrace_syscall_exit};
9use starnix_core::signals::{
10    SignalInfo, deliver_signal, dequeue_signal, prepare_to_restart_syscall,
11};
12use starnix_core::task::{CurrentTask, ExceptionResult, ExitStatus, SeccompStateValue, TaskFlags};
13use starnix_logging::{
14    CATEGORY_STARNIX, NAME_HANDLE_EXCEPTION, NAME_RESTRICTED_KICK, NAME_RUN_TASK, log_error,
15    log_syscall, log_trace, log_warn, set_current_task_info,
16};
17use starnix_registers::RestrictedState;
18
19use starnix_syscalls::SyscallResult;
20use starnix_syscalls::decls::{Syscall, SyscallDecl};
21use starnix_uapi::errno;
22use starnix_uapi::errors::Errno;
23use starnix_uapi::signals::SIGKILL;
24use zerocopy::FromZeros;
25
26mod table;
27
28pub fn enter(current_task: &mut CurrentTask) -> ExitStatus {
29    // Zircon will populate this report on restricted exception exits. Initialize it to all zero
30    // since we're just reserving storage.
31    let mut exception_report = zx::sys::zx_exception_report_t::new_zeroed();
32    match RestrictedState::bind_and_map(
33        &mut current_task.thread_state.registers,
34        &mut exception_report,
35    ) {
36        Ok(restricted_state) => {
37            match run_task(current_task, restricted_state.bound_state.as_ptr(), &exception_report) {
38                Ok(ok) => ok,
39                Err(error) => {
40                    log_warn!("Died unexpectedly from {error:?}! treating as SIGKILL");
41                    ExitStatus::Kill(SignalInfo::kernel(SIGKILL))
42                }
43            }
44        }
45        Err(error) => {
46            log_error!("failed to map mode state vmo, {error:?}! treating as SIGKILL");
47            ExitStatus::Kill(SignalInfo::kernel(SIGKILL))
48        }
49    }
50}
51
52type RestrictedExitCallback = extern "C" fn(
53    *mut RestrictedEnterContext<'_>,
54    zx::sys::zx_restricted_reason_t,
55    *mut ExtendedPstatePointer,
56) -> bool;
57
58unsafe extern "C" {
59    // rustc doesn't like RestrictedEnterContext for FFI but we're just passing it back to
60    // ourselves with extra steps.
61    #[allow(improper_ctypes)]
62    fn restricted_enter_loop(
63        options: u32,
64        restricted_exit_callback: RestrictedExitCallback,
65        restricted_exit_callback_context: *mut RestrictedEnterContext<'_>,
66        restricted_state: *mut zx::sys::zx_restricted_state_t,
67        extended_pstate_ptr_ptr: *mut ExtendedPstatePointer,
68    ) -> zx::sys::zx_status_t;
69}
70
71const RESTRICTED_ENTER_OPTIONS: u32 = 0;
72
73struct RestrictedEnterContext<'a> {
74    current_task: &'a mut CurrentTask,
75    error_context: Option<ErrorContext>,
76    exit_status: Result<ExitStatus, Error>,
77    exception_report_raw: *const zx::sys::zx_exception_report_t,
78}
79
80/// Runs the `current_task` to completion.
81///
82/// The high-level flow of this function looks as follows:
83///
84///   1. Write the restricted state for the current thread to set it up to enter into the restricted
85///      (Linux) part of the address space.
86///   2. Enter restricted mode.
87///   3. Return from restricted mode, reading out the new state of the restricted mode execution.
88///      This state contains the thread's restricted register state, which is used to determine
89///      which system call to dispatch.
90///   4. Dispatch the system call.
91///   5. Handle pending signals.
92///   6. Goto 1.
93fn run_task(
94    current_task: &mut CurrentTask,
95    restricted_state_ptr: *mut zx::sys::zx_restricted_state_t,
96    exception_report_raw: *const zx::sys::zx_exception_report_t,
97) -> Result<ExitStatus, Error> {
98    set_current_task_info(
99        current_task.task.command(),
100        current_task.task.thread_group().read().leader_command(),
101        current_task.task.thread_group().leader,
102        current_task.tid,
103    );
104
105    fuchsia_trace::duration!(CATEGORY_STARNIX, NAME_RUN_TASK);
106
107    // This tracks the last failing system call for debugging purposes.
108    let error_context = None;
109
110    // We need to check for exit once, before the task starts executing, in case
111    // the task has already been sent a signal that will cause it to exit.
112    if let Some(exit_status) = process_completed_restricted_exit(current_task, &error_context)? {
113        return Ok(exit_status);
114    }
115
116    // This extended pstate pointer points to the storage for extended processor
117    // state (vector and FP registers).
118    let mut extended_pstate_ptr = current_task.thread_state.extended_pstate.as_ptr();
119
120    let mut restricted_enter_context = RestrictedEnterContext {
121        current_task,
122        error_context,
123        exit_status: Err(errno!(ENOEXEC).into()),
124        exception_report_raw,
125    };
126
127    #[allow(
128        clippy::undocumented_unsafe_blocks,
129        reason = "Force documented unsafe blocks in Starnix"
130    )]
131    let restricted_enter_status = zx::Status::from_raw(unsafe {
132        restricted_enter_loop(
133            RESTRICTED_ENTER_OPTIONS,
134            restricted_exit_callback_c,
135            &mut restricted_enter_context,
136            restricted_state_ptr,
137            &raw mut extended_pstate_ptr,
138        )
139    });
140    if restricted_enter_status != zx::Status::OK {
141        // If restricted_enter_loop failed, it means that we failed to satisfy
142        // a prerequisite of zx_restricted_enter which should never happen.
143        log_error!(
144            "restricted_enter_loop failed: {}, register state: {:?}",
145            restricted_enter_status,
146            restricted_enter_context.current_task.thread_state.registers
147        );
148    }
149    restricted_enter_context.exit_status
150}
151
152extern "C" fn restricted_exit_callback_c(
153    context: *mut RestrictedEnterContext<'_>,
154    reason_code: zx::sys::zx_restricted_reason_t,
155    extended_pstate_ptr_ptr: *mut ExtendedPstatePointer,
156) -> bool {
157    // SAFETY:
158    // `context` is a pointer to a `RestrictedEnterContext` that was passed to
159    // `restricted_enter_loop`.
160    //  `extended_pstate_ptr` is a pointer to the ExtendedPstatePointer instance
161    //  that was passed to `restricted_enter_loop.`
162    // Our restricted return assembly and Zircon together guarantee that this
163    // thread has exclusive access to these variables.
164    let (restricted_context, extended_pstate_ptr) =
165        unsafe { (&mut *context, extended_pstate_ptr_ptr.as_mut_unchecked()) };
166    restricted_exit_callback(
167        reason_code,
168        restricted_context.current_task,
169        &mut restricted_context.error_context,
170        &mut restricted_context.exit_status,
171        extended_pstate_ptr,
172        restricted_context.exception_report_raw,
173    )
174}
175
176fn restricted_exit_callback(
177    reason_code: zx::sys::zx_restricted_reason_t,
178    current_task: &mut CurrentTask,
179    error_context: &mut Option<ErrorContext>,
180    exit_status: &mut Result<ExitStatus, Error>,
181    extended_pstate_ptr: &mut ExtendedPstatePointer,
182    exception_report_raw: *const zx::sys::zx_exception_report_t,
183) -> bool {
184    debug_assert_eq!(
185        current_task.thread_state.restart_code, None,
186        "restart_code should only ever be Some() in normal mode",
187    );
188
189    let ret = match process_restricted_exit(
190        reason_code,
191        current_task,
192        error_context,
193        exception_report_raw,
194    ) {
195        Ok(None) => {
196            // Keep going!
197
198            *extended_pstate_ptr = current_task.thread_state.extended_pstate.as_ptr();
199
200            true
201        }
202        Ok(Some(completed_exit_status)) => {
203            *exit_status = Ok(completed_exit_status);
204            false
205        }
206        Err(error) => {
207            *exit_status = Err(error);
208            false
209        }
210    };
211
212    debug_assert_eq!(
213        current_task.thread_state.restart_code, None,
214        "restart_code should only ever be Some() in normal mode",
215    );
216
217    ret
218}
219
220fn process_restricted_exit(
221    reason_code: zx::sys::zx_restricted_reason_t,
222    current_task: &mut CurrentTask,
223    error_context: &mut Option<ErrorContext>,
224    exception_report_raw: *const zx::sys::zx_exception_report_t,
225) -> Result<Option<ExitStatus>, Error> {
226    current_task.thread_state.registers.sync_stack_ptr();
227
228    match reason_code {
229        zx::sys::ZX_RESTRICTED_REASON_SYSCALL => {
230            let syscall_decl = SyscallDecl::from_number(
231                current_task.thread_state.registers.syscall_register(),
232                current_task.thread_state.arch_width(),
233            );
234
235            if let Some(new_error_context) = execute_syscall(current_task, syscall_decl) {
236                *error_context = Some(new_error_context);
237            }
238        }
239        zx::sys::ZX_RESTRICTED_REASON_EXCEPTION => {
240            fuchsia_trace::duration!(CATEGORY_STARNIX, NAME_HANDLE_EXCEPTION);
241            // SAFETY: `exception_report_raw` was written by Zircon during this restricted exit.
242            let exception_report = unsafe { zx::ExceptionReport::from_raw(*exception_report_raw) };
243            let exception_result = current_task.process_exception(&exception_report);
244            process_completed_exception(current_task, exception_result, exception_report);
245        }
246        zx::sys::ZX_RESTRICTED_REASON_KICK => {
247            fuchsia_trace::instant!(
248                CATEGORY_STARNIX,
249                NAME_RESTRICTED_KICK,
250                fuchsia_trace::Scope::Thread
251            );
252            // Fall through to the post-syscall / post-exception handling logic. We were likely
253            // kicked because a signal is pending deliver or the task has exited. Spurious kicks are
254            // also possible.
255        }
256        _ => {
257            return Err(format_err!("Received unexpected restricted reason code: {}", reason_code));
258        }
259    }
260
261    if let Some(exit_status) = process_completed_restricted_exit(current_task, &error_context)? {
262        return Ok(Some(exit_status));
263    }
264
265    Ok(None)
266}
267
268fn process_completed_exception(
269    current_task: &mut CurrentTask,
270    exception_result: ExceptionResult,
271    restricted_exception: zx::ExceptionReport,
272) {
273    match exception_result {
274        ExceptionResult::Handled => {}
275        ExceptionResult::Signal(signal) => {
276            let mut task_state = current_task.task.write();
277            if task_state.ptrace_on_signal_consume() {
278                task_state.set_stopped(
279                    StopState::SignalDeliveryStopping,
280                    Some(signal),
281                    Some(&current_task),
282                    None,
283                );
284                return;
285            }
286
287            if let Some(status) = deliver_signal(
288                current_task.task.as_ref(),
289                current_task.thread_state.arch_width(),
290                task_state,
291                signal.into(),
292                &mut current_task.thread_state.registers,
293                &current_task.thread_state.extended_pstate,
294                Some(restricted_exception),
295            ) {
296                current_task.kill_thread_group(status);
297            }
298        }
299    }
300}
301
302/// Contains context to track the most recently failing system call.
303///
304/// When a task exits with a non-zero exit code, this context is logged to help debugging which
305/// system call may have triggered the failure.
306#[derive(Debug)]
307pub struct ErrorContext {
308    /// The system call that failed.
309    pub syscall: Syscall,
310
311    /// The error that was returned for the system call.
312    pub error: Errno,
313}
314
315/// Executes the provided `syscall` in `current_task`.
316///
317/// Returns an `ErrorContext` if the system call returned an error.
318#[inline(never)] // Inlining this function breaks the CFI directives used to unwind into user code.
319pub fn execute_syscall(
320    current_task: &mut CurrentTask,
321    syscall_decl: SyscallDecl,
322) -> Option<ErrorContext> {
323    fuchsia_trace::duration!(CATEGORY_STARNIX, syscall_decl.trace_name());
324    let syscall = new_syscall(syscall_decl, current_task);
325
326    current_task.thread_state.registers.save_registers_for_restart(syscall.decl.number);
327
328    if current_task.trace_syscalls.load(std::sync::atomic::Ordering::Relaxed) {
329        ptrace_syscall_enter(current_task);
330    }
331
332    log_syscall!(current_task, "{syscall:?}");
333
334    let _lockup_detector_guard = starnix_core::task::ThreadLockupDetector::track();
335    let result: Result<SyscallResult, Errno> =
336        if current_task.seccomp_filter_state.get() != SeccompStateValue::None {
337            // Inlined fast path for seccomp, so that we don't incur the cost
338            // of a method call when running the filters.
339            if let Some(res) = current_task.run_seccomp_filters(&syscall) {
340                res
341            } else {
342                table::dispatch_syscall(current_task, &syscall)
343            }
344        } else {
345            table::dispatch_syscall(current_task, &syscall)
346        };
347
348    current_task.trigger_delayed_releaser();
349
350    let return_value = match result {
351        Ok(return_value) => {
352            log_syscall!(current_task, "-> {:#x}", return_value.value());
353            current_task.thread_state.registers.set_return_register(return_value.value());
354            None
355        }
356        Err(errno) => {
357            log_syscall!(current_task, "!-> {errno}");
358            if errno.is_restartable() {
359                current_task.thread_state.restart_code = Some(errno.code);
360            }
361            current_task.thread_state.registers.set_return_register(errno.return_value());
362            Some(ErrorContext { error: errno, syscall })
363        }
364    };
365
366    if current_task.trace_syscalls.load(std::sync::atomic::Ordering::Relaxed) {
367        ptrace_syscall_exit(current_task, return_value.is_some());
368    }
369
370    return_value
371}
372
373/// Finishes `current_task` updates after a restricted mode exit such as a syscall, exception, or kick.
374///
375/// Returns an `ExitStatus` if the task is meant to exit.
376pub fn process_completed_restricted_exit(
377    current_task: &mut CurrentTask,
378    error_context: &Option<ErrorContext>,
379) -> Result<Option<ExitStatus>, Errno> {
380    let result;
381    loop {
382        // Checking for a signal might cause the task to exit, so check before processing exit
383        {
384            {
385                if !current_task.is_exitted() {
386                    dequeue_signal(current_task);
387                }
388                // The syscall may need to restart for a non-signal-related
389                // reason. This call does nothing if we aren't restarting.
390                prepare_to_restart_syscall(&mut current_task.thread_state, None);
391            }
392        }
393
394        let exit_status = current_task.exit_status();
395        if let Some(exit_status) = exit_status {
396            log_trace!("exiting with status {:?}", exit_status);
397            if let Some(error_context) = error_context {
398                match exit_status {
399                    ExitStatus::Exit(value) if value == 0 => {}
400                    _ => {
401                        log_trace!(
402                            "last failing syscall before exit: {:?}, failed with {:?}",
403                            error_context.syscall,
404                            error_context.error
405                        );
406                    }
407                };
408            }
409
410            result = Some(exit_status);
411            break;
412        } else {
413            // Block a stopped process after it's had a chance to handle signals, since a signal might
414            // cause it to stop.
415            if current_task.block_if_stopped() {
416                // If the task was stopped and has now woken up (e.g., via SIGCONT or PTRACE_CONT),
417                // loop back to process any pending signals before returning to userspace.
418                continue;
419            }
420            result = None;
421            // Always restore signal mask before returning to userspace.
422            current_task.write().restore_signal_mask();
423            break;
424        }
425    }
426
427    if let Some(ExitStatus::CoreDump(signal_info)) = &result {
428        if current_task.flags().contains(TaskFlags::DUMP_ON_EXIT) {
429            // Avoid taking a backtrace if the signal was sent by the same task.
430            if !signal_info.is_sent_by(&current_task.weak_task()) {
431                // Request a backtrace before reporting the crash to increase chance of a backtrace
432                // in logs. This call is kept as far up in the call stack as possible to avoid
433                // additional frames that are always the same and not relevant to users.
434                // TODO(https://fxbug.dev/356732164) collect a backtrace ourselves
435                debug::backtrace_request_current_thread();
436            }
437
438            if let Some(pending_report) =
439                current_task.kernel().crash_reporter.begin_crash_report(&current_task)
440            {
441                current_task.kernel().crash_reporter.handle_core_dump(
442                    &current_task,
443                    signal_info,
444                    pending_report,
445                );
446            }
447        }
448    }
449    return Ok(result);
450}