Skip to main content

starnix_modules_syscalls/
reboot.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 std::sync::Arc;
6
7use bstr::ByteSlice;
8use fidl_fuchsia_hardware_power_statecontrol as fpower;
9use fuchsia_component::client::connect_to_protocol;
10use linux_uapi::{
11    LINUX_REBOOT_CMD_CAD_OFF, LINUX_REBOOT_CMD_CAD_ON, LINUX_REBOOT_CMD_HALT,
12    LINUX_REBOOT_CMD_KEXEC, LINUX_REBOOT_CMD_POWER_OFF, LINUX_REBOOT_CMD_RESTART,
13    LINUX_REBOOT_CMD_RESTART2, LINUX_REBOOT_CMD_SW_SUSPEND,
14};
15use starnix_core::mm::MemoryAccessorExt;
16use starnix_core::security;
17use starnix_core::task::{CurrentTask, ExitStatus};
18use starnix_core::vfs::FsString;
19use starnix_logging::{log_debug, log_error, log_info, log_warn, track_stub};
20use starnix_sync::{InterruptibleEvent, Mutex};
21use starnix_uapi::auth::CAP_SYS_BOOT;
22use starnix_uapi::errors::{EINTR, Errno};
23use starnix_uapi::signals::SigSet;
24use starnix_uapi::user_address::{UserAddress, UserCString};
25use starnix_uapi::{
26    LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_MAGIC2A, LINUX_REBOOT_MAGIC2B,
27    LINUX_REBOOT_MAGIC2C, errno, error,
28};
29
30pub fn sys_reboot(
31    current_task: &mut CurrentTask,
32    magic: u32,
33    magic2: u32,
34    cmd: u32,
35    arg: UserAddress,
36) -> Result<(), Errno> {
37    if magic != LINUX_REBOOT_MAGIC1
38        || (magic2 != LINUX_REBOOT_MAGIC2
39            && magic2 != LINUX_REBOOT_MAGIC2A
40            && magic2 != LINUX_REBOOT_MAGIC2B
41            && magic2 != LINUX_REBOOT_MAGIC2C)
42    {
43        return error!(EINVAL);
44    }
45    security::check_task_capable(current_task, CAP_SYS_BOOT)?;
46
47    let arg_bytes = if matches!(cmd, LINUX_REBOOT_CMD_RESTART2)
48        || (matches!(cmd, LINUX_REBOOT_CMD_POWER_OFF) && !arg.is_null())
49    {
50        // This is an arbitrary limit that should be large enough.
51        const MAX_REBOOT_ARG_LEN: usize = 256;
52        current_task
53            .read_c_string_to_vec(UserCString::new(current_task, arg), MAX_REBOOT_ARG_LEN)?
54    } else {
55        FsString::default()
56    };
57
58    if current_task.kernel().is_shutting_down() {
59        log_debug!("Ignoring reboot() and parking caller, already shutting down.");
60        let event = InterruptibleEvent::new();
61        return current_task.block_until(event.begin_wait(), zx::MonotonicInstant::INFINITE);
62    }
63
64    match cmd {
65        // CAD on/off commands turn Ctrl-Alt-Del keystroke on or off without halting the system.
66        LINUX_REBOOT_CMD_CAD_ON | LINUX_REBOOT_CMD_CAD_OFF => Ok(()),
67
68        // `kexec_load()` is not supported.
69        LINUX_REBOOT_CMD_KEXEC => error!(EINVAL),
70
71        // Suspend is not implemented.
72        LINUX_REBOOT_CMD_SW_SUSPEND => error!(EINVAL),
73
74        LINUX_REBOOT_CMD_HALT | LINUX_REBOOT_CMD_POWER_OFF => {
75            log_info!("Powering off");
76            let reboot_args: Vec<_> = arg_bytes.split_str(b",").collect();
77            let shutdown_reason = parse_shutdown_reason(&reboot_args, &arg_bytes);
78            let options = fpower::ShutdownOptions {
79                action: Some(fpower::ShutdownAction::Poweroff),
80                reasons: Some(vec![shutdown_reason]),
81                ..Default::default()
82            };
83            shutdown_and_block(current_task, options, "sys_reboot_poweroff")
84        }
85
86        LINUX_REBOOT_CMD_RESTART | LINUX_REBOOT_CMD_RESTART2 => {
87            let reboot_args: Vec<_> = arg_bytes.split_str(b",").collect();
88
89            let options = if reboot_args.contains(&&b"bootloader"[..]) {
90                log_info!("Rebooting to bootloader");
91                fpower::ShutdownOptions {
92                    action: Some(fpower::ShutdownAction::RebootToBootloader),
93                    reasons: Some(vec![fpower::ShutdownReason::StarnixContainerNoReason]),
94                    ..Default::default()
95                }
96            } else if reboot_args.contains(&&b"recovery"[..]) {
97                log_info!("Rebooting to recovery...");
98                fpower::ShutdownOptions {
99                    action: Some(fpower::ShutdownAction::RebootToRecovery),
100                    reasons: Some(vec![fpower::ShutdownReason::StarnixContainerNoReason]),
101                    ..Default::default()
102                }
103            } else {
104                let shutdown_reason = parse_shutdown_reason(&reboot_args, &arg_bytes);
105                log_info!("Rebooting... reason: {:?}", shutdown_reason);
106                fpower::ShutdownOptions {
107                    action: Some(fpower::ShutdownAction::Reboot),
108                    reasons: Some(vec![shutdown_reason]),
109                    ..Default::default()
110                }
111            };
112            shutdown_and_block(current_task, options, "sys_reboot_reboot")
113        }
114
115        _ => error!(EINVAL),
116    }
117}
118
119fn shutdown_and_block(
120    current_task: &mut CurrentTask,
121    options: fpower::ShutdownOptions,
122    debug_name: &'static str,
123) -> Result<(), Errno> {
124    let event = InterruptibleEvent::new();
125    let event_clone = event.clone();
126    let error_state = Arc::new(Mutex::new(None));
127    let error_state_clone = error_state.clone();
128
129    let kernel = current_task.kernel().clone();
130    kernel.kthreads.spawn_future(
131        move || async move {
132            match connect_to_protocol::<fpower::AdminMarker>() {
133                Ok(proxy) => match proxy.shutdown(&options).await {
134                    Ok(Ok(())) => {}
135                    Ok(Err(status)) => {
136                        log_error!(
137                            "sys_reboot: proxy.shutdown async ({debug_name}) returned error: {}",
138                            zx::Status::err_from_raw(status)
139                        );
140                        error_state_clone.lock().replace(errno!(EIO));
141                        event_clone.notify();
142                    }
143                    Err(e) => {
144                        log_error!("sys_reboot: proxy.shutdown async ({debug_name}) failed: {e:?}");
145                        error_state_clone.lock().replace(errno!(EIO));
146                        event_clone.notify();
147                    }
148                },
149                Err(e) => {
150                    log_error!(
151                        "sys_reboot: failed to connect to Admin async ({debug_name}): {e:?}"
152                    );
153                    error_state_clone.lock().replace(errno!(ENOTSUP));
154                    event_clone.notify();
155                }
156            }
157        },
158        debug_name,
159    );
160
161    // Mask all maskable signals for the current task before blocking so that
162    // normal signals do not wake up the task and return EINTR to userspace.
163    // Unmaskable signals like SIGKILL (sent during container teardown) will
164    // still interrupt the block and cause the task to exit without returning to userspace.
165    let result = current_task.wait_with_temporary_mask(!SigSet::default(), |current_task| {
166        current_task.block_until(event.begin_wait(), zx::MonotonicInstant::INFINITE)
167    });
168
169    if let Some(err) = error_state.lock().take() {
170        return Err(err);
171    }
172
173    if let Err(err) = result {
174        if err.code == EINTR {
175            // Linux expects `sys_reboot` to never return. While unmaskable
176            // signals like SIGKILL typically terminate the task before returning
177            // from this method, we keep this as a safeguard to ensure a clean
178            // `Exit(0)` on teardown instead of `SIGKILL` and to prevent Android
179            // `init` from ever observing `EINTR` in edge cases (e.g. cgroup
180            // freeze or ptrace), which would cause `init` to abort and re-enter
181            // `reboot()`, deadlocking container teardown.
182            // Thus, we manually terminate the thread group.
183            current_task.thread_group().kill(ExitStatus::Exit(0), None);
184            return Ok(());
185        }
186
187        return Err(err);
188    }
189
190    Ok(())
191}
192
193fn parse_shutdown_reason(reboot_args: &[&[u8]], arg_bytes: &FsString) -> fpower::ShutdownReason {
194    // TODO(https://fxbug.dev/391585107): Loop through all the arguments and
195    // generate a list of shutdown reasons.
196    if let Some(arg) = reboot_args.iter().find(|arg| arg.ends_with(b"-failed")) {
197        let process_name = String::from_utf8_lossy(arg.strip_suffix(b"-failed").unwrap());
198        // This log message is load-bearing server-side as it's used to
199        // extract the critical process responsible for the reboot.
200        // Please notify //src/developer/forensics/OWNERS upon changing.
201        log_info!("Android critical process '{}' failed, rebooting", process_name);
202        fpower::ShutdownReason::AndroidCriticalProcessFailure
203    } else if reboot_args.contains(&&b"ota_update"[..])
204        || reboot_args.contains(&&b"System update during setup"[..])
205    {
206        fpower::ShutdownReason::SystemUpdate
207    } else if reboot_args.contains(&&b"shell"[..]) {
208        fpower::ShutdownReason::DeveloperRequest
209    } else if reboot_args.contains(&&b"RescueParty"[..])
210        || reboot_args.contains(&&b"rescueparty"[..])
211    {
212        fpower::ShutdownReason::AndroidRescueParty
213    } else if reboot_args.contains(&&b"userrequested"[..]) {
214        fpower::ShutdownReason::UserRequest
215    } else if reboot_args.contains(&&b"thermal"[..]) {
216        fpower::ShutdownReason::HighTemperature
217    } else if reboot_args.contains(&&b"battery"[..]) {
218        fpower::ShutdownReason::BatteryDrained
219    } else if reboot_args == [b""]
220    // args empty? splitting "" returns [""], not []
221    {
222        fpower::ShutdownReason::StarnixContainerNoReason
223    } else {
224        log_warn!("Unknown reboot args: {arg_bytes:?}");
225        track_stub!(
226            TODO("https://fxbug.dev/322874610"),
227            "unknown reboot args, see logs for strings"
228        );
229        fpower::ShutdownReason::AndroidUnexpectedReason
230    }
231}
232
233#[cfg(target_arch = "aarch64")]
234mod arch32 {
235    pub use super::sys_reboot as sys_arch32_reboot;
236}
237
238#[cfg(target_arch = "aarch64")]
239pub use arch32::*;
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    fn check_parse_shutdown_reason(args: &str, expected: fpower::ShutdownReason) {
246        let bytes = FsString::from(args.as_bytes().to_vec());
247        let split_args: Vec<_> = bytes.split_str(b",").collect();
248        let reason = super::parse_shutdown_reason(&split_args, &bytes);
249        assert_eq!(reason, expected, "Failed for args: {}", args);
250    }
251
252    #[test]
253    fn parse_shutdown_reason_thermal() {
254        check_parse_shutdown_reason("shutdown,thermal", fpower::ShutdownReason::HighTemperature);
255    }
256
257    #[test]
258    fn parse_shutdown_reason_battery() {
259        check_parse_shutdown_reason("shutdown,battery", fpower::ShutdownReason::BatteryDrained);
260    }
261
262    #[test]
263    fn parse_shutdown_reason_thermal_and_battery() {
264        check_parse_shutdown_reason(
265            "shutdown,thermal,battery",
266            fpower::ShutdownReason::HighTemperature,
267        );
268    }
269}