mouse/cheesed

Blob: src/linux.rs

Raw · Blame

use crate::policy::{ServiceCatalog, generate_runlevels, parse_rc_conf};
use crate::{STARTUP_BANNER, ShutdownAction, shutdown_action};
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::os::unix::process::CommandExt;
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::thread;
use std::time::{Duration, Instant};

const CONSOLE_PATH: &str = "/dev/console";
const EMERGENCY_SHELL: &str = "/bin/tcsh";
const HOSTNAME_PATH: &str = "/etc/hostname";
const OPENRC_PATH: &str = "/sbin/openrc";
const RC_CONF_PATH: &str = "/etc/rc.conf";
const SERVICE_DEFINITIONS: &str = "/etc/init.d";
const RUNLEVEL_TEMPLATES: &str = "/usr/lib/mouse/runlevels";
const RUNTIME_RUNLEVELS: &str = "/run/openrc/runlevels";
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const SHELL_RESTART_DELAY: Duration = Duration::from_secs(1);
const BOOT_TRANSITION_TIMEOUT: Duration = Duration::from_secs(120);
const SHUTDOWN_TRANSITION_TIMEOUT: Duration = Duration::from_secs(30);
const SERVICE_STATUS_TIMEOUT: Duration = Duration::from_secs(10);
const TERMINATION_GRACE: Duration = Duration::from_secs(5);

static CHILD_EXITED: AtomicBool = AtomicBool::new(false);
static SHUTDOWN_SIGNAL: AtomicI32 = AtomicI32::new(0);

extern "C" fn handle_signal(signal: libc::c_int) {
    if signal == libc::SIGCHLD {
        CHILD_EXITED.store(true, Ordering::Relaxed);
    } else {
        let _ = SHUTDOWN_SIGNAL.compare_exchange(0, signal, Ordering::Relaxed, Ordering::Relaxed);
    }
}

pub(crate) fn run() -> Result<(), String> {
    // SAFETY: getpid has no preconditions and does not mutate memory.
    if unsafe { libc::getpid() } != 1 {
        return Err("normal mode must run as PID 1".to_owned());
    }

    run_pid1()
}

fn run_pid1() -> ! {
    let mut console = loop {
        match Console::open() {
            Ok(console) => break console,
            Err(error) => {
                eprintln!("cheesed: cannot open {CONSOLE_PATH} for early boot logging: {error}");
                thread::sleep(SHELL_RESTART_DELAY);
            }
        }
    };

    // SAFETY: cheesed is single-threaded here and passes a conventional process umask.
    unsafe {
        libc::umask(0o022);
    }
    if let Err(error) = std::env::set_current_dir("/") {
        remain_alive_after_fatal_error(&mut console, &format!("cannot chdir to /: {error}"));
    }
    if let Err(error) = install_signal_handlers() {
        remain_alive_after_fatal_error(
            &mut console,
            &format!("cannot install signal handlers: {error}"),
        );
    }
    if let Err(error) = mount_early_filesystems() {
        remain_alive_after_fatal_error(
            &mut console,
            &format!("cannot mount early filesystems: {error}"),
        );
    }
    if let Err(error) = configure_hostname() {
        remain_alive_after_fatal_error(
            &mut console,
            &format!("cannot configure hostname: {error}"),
        );
    }

    console.write_line(STARTUP_BANNER);
    console.log("C3 bootstrap complete; cheesed is PID 1");
    let recovery = match boot_services(&mut console) {
        Ok(Some(action)) => perform_shutdown(&mut console, None, action),
        Ok(None) => false,
        Err(error) => {
            console.log(&format!("{error}; entering recovery console"));
            true
        }
    };
    let mut shell_pid = if recovery {
        match spawn_emergency_shell(&mut console) {
            Ok(pid) => Some(pid),
            Err(error) => {
                console.log(&error);
                None
            }
        }
    } else {
        None
    };

    loop {
        if CHILD_EXITED.swap(false, Ordering::Relaxed) {
            let shell_exited = reap_children(&mut console, shell_pid);
            if shell_exited {
                shell_pid = None;
            }
        }

        let signal = SHUTDOWN_SIGNAL.swap(0, Ordering::Relaxed);
        if let Some(action) = shutdown_action(signal) {
            perform_shutdown(&mut console, shell_pid, action);
        }

        if recovery && shell_pid.is_none() {
            console.log("emergency shell exited; restarting it in one second");
            thread::sleep(SHELL_RESTART_DELAY);
            shell_pid = Some(match spawn_emergency_shell(&mut console) {
                Ok(pid) => pid,
                Err(error) => {
                    console.log(&format!("cannot restart emergency shell: {error}"));
                    continue;
                }
            });
        }

        thread::sleep(POLL_INTERVAL);
    }
}

fn remain_alive_after_fatal_error(console: &mut Console, error: &str) -> ! {
    console.log(error);
    console.log("unrecoverable bootstrap failure; PID 1 will remain alive");
    loop {
        thread::sleep(Duration::from_secs(60));
    }
}

struct Console {
    file: File,
}

impl Console {
    fn open() -> io::Result<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(CONSOLE_PATH)?;
        Ok(Self { file })
    }

    fn log(&mut self, message: &str) {
        self.write_line(&format!("cheesed: {message}"));
    }

    fn write_line(&mut self, message: &str) {
        let _ = writeln!(self.file, "{message}");
        let _ = self.file.flush();
    }

    fn clone_file(&self) -> io::Result<File> {
        self.file.try_clone()
    }
}

fn install_signal_handlers() -> io::Result<()> {
    for signal in [libc::SIGCHLD, libc::SIGINT, libc::SIGTERM] {
        // SAFETY: zero is a valid initial representation for sigaction before its
        // fields and mask are initialized below.
        let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
        action.sa_sigaction = handle_signal as *const () as usize;
        action.sa_flags = libc::SA_RESTART;
        if signal == libc::SIGCHLD {
            action.sa_flags |= libc::SA_NOCLDSTOP;
        }

        // SAFETY: action owns a valid sigset_t and the signal number is known.
        if unsafe { libc::sigemptyset(&raw mut action.sa_mask) } == -1 {
            return Err(io::Error::last_os_error());
        }
        // SAFETY: action remains alive for the call and the old action is not needed.
        if unsafe { libc::sigaction(signal, &raw const action, std::ptr::null_mut()) } == -1 {
            return Err(io::Error::last_os_error());
        }
    }
    Ok(())
}

fn mount_early_filesystems() -> io::Result<()> {
    mount_filesystem(c"devtmpfs".as_ptr(), c"/dev".as_ptr(), c"devtmpfs".as_ptr())?;
    mount_filesystem(c"proc".as_ptr(), c"/proc".as_ptr(), c"proc".as_ptr())?;
    mount_filesystem(c"sysfs".as_ptr(), c"/sys".as_ptr(), c"sysfs".as_ptr())?;
    mount_filesystem(c"tmpfs".as_ptr(), c"/run".as_ptr(), c"tmpfs".as_ptr())
}

fn mount_filesystem(
    source: *const libc::c_char,
    target: *const libc::c_char,
    filesystem_type: *const libc::c_char,
) -> io::Result<()> {
    // SAFETY: all pointers are valid, static, NUL-terminated C strings; no data
    // argument is required for these pseudo-filesystems.
    if unsafe {
        libc::mount(
            source,
            target,
            filesystem_type,
            0,
            std::ptr::null::<libc::c_void>(),
        )
    } == -1
    {
        let error = io::Error::last_os_error();
        if error.raw_os_error() != Some(libc::EBUSY) {
            return Err(error);
        }
    }
    Ok(())
}

fn configure_hostname() -> io::Result<()> {
    let hostname = std::fs::read_to_string(HOSTNAME_PATH)?;
    let hostname = hostname.trim();
    if hostname.is_empty() || hostname.as_bytes().contains(&0) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("{HOSTNAME_PATH} must contain a non-empty hostname"),
        ));
    }

    // SAFETY: hostname points to readable bytes for the supplied length, and
    // sethostname does not require a terminating NUL.
    if unsafe { libc::sethostname(hostname.as_ptr().cast::<libc::c_char>(), hostname.len()) } == -1
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

fn boot_services(console: &mut Console) -> Result<Option<ShutdownAction>, String> {
    let catalog = ServiceCatalog::discover(
        std::path::Path::new(SERVICE_DEFINITIONS),
        std::path::Path::new(RUNLEVEL_TEMPLATES),
    )
    .map_err(|error| format!("cannot discover services: {error}"))?;
    let contents = std::fs::read_to_string(RC_CONF_PATH)
        .map_err(|error| format!("cannot read {RC_CONF_PATH}: {error}"))?;
    let policy = parse_rc_conf(&contents, &catalog)
        .map_err(|error| format!("invalid service policy: {error}"))?;
    let enabled = policy.enabled().collect::<Vec<_>>();
    if enabled.is_empty() {
        console.log("enabled optional services: (none)");
    } else {
        console.log(&format!(
            "enabled optional services: {}",
            enabled.join(", ")
        ));
    }

    let generation = generate_runlevels(&policy, &catalog, std::path::Path::new(RUNTIME_RUNLEVELS))
        .map_err(|error| format!("cannot generate OpenRC runlevels: {error}"))?;
    console.log(&format!(
        "generated OpenRC runlevels at {}",
        generation.display()
    ));

    for runlevel in ["sysinit", "boot"] {
        if let Some(action) = run_openrc(console, runlevel, BOOT_TRANSITION_TIMEOUT, true)? {
            return Ok(Some(action));
        }
    }
    if let Some(action) = run_openrc(console, "default", BOOT_TRANSITION_TIMEOUT, false)? {
        return Ok(Some(action));
    }
    for service in catalog.required() {
        if let Some(action) = run_service_status(console, service)
            .map_err(|error| format!("required service {service} is not started: {error}"))?
        {
            return Ok(Some(action));
        }
    }
    for service in policy.enabled() {
        match run_service_status(console, service) {
            Ok(Some(action)) => return Ok(Some(action)),
            Ok(None) => {}
            Err(error) => console.log(&format!("optional service {service} failed: {error}")),
        }
    }
    Ok(None)
}

fn run_openrc(
    console: &mut Console,
    runlevel: &str,
    timeout: Duration,
    require_success: bool,
) -> Result<Option<ShutdownAction>, String> {
    console.log(&format!("starting OpenRC {runlevel} transition"));
    let status = match command_with_console(OPENRC_PATH, &[runlevel], console, timeout)? {
        CommandResult::Exited(status) => status,
        CommandResult::Shutdown(action) => return Ok(Some(action)),
    };
    if require_success && !status.success() {
        return Err(format!(
            "OpenRC {runlevel} transition exited with {}",
            describe_exit_status(status)
        ));
    }
    if status.success() {
        console.log(&format!("OpenRC {runlevel} transition complete"));
    } else {
        console.log(&format!(
            "OpenRC {runlevel} transition exited with {}; checking required services",
            describe_exit_status(status)
        ));
    }
    Ok(None)
}

fn run_service_status(
    console: &mut Console,
    service: &str,
) -> Result<Option<ShutdownAction>, String> {
    let result = command_with_console(
        "/sbin/rc-service",
        &[service, "status"],
        console,
        SERVICE_STATUS_TIMEOUT,
    )?;
    let status = match result {
        CommandResult::Exited(status) => status,
        CommandResult::Shutdown(action) => return Ok(Some(action)),
    };
    if status.success() {
        Ok(None)
    } else {
        Err(format!(
            "status command exited with {}",
            describe_exit_status(status)
        ))
    }
}

fn command_with_console(
    program: &str,
    arguments: &[&str],
    console: &mut Console,
    timeout: Duration,
) -> Result<CommandResult, String> {
    let stdin = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console input: {error}"))?;
    let stdout = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console output: {error}"))?;
    let stderr = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console error output: {error}"))?;
    let mut command = Command::new(program);
    command
        .args(arguments)
        .stdin(Stdio::from(stdin))
        .stdout(Stdio::from(stdout))
        .stderr(Stdio::from(stderr));
    // SAFETY: this closure runs between fork and exec, and setpgid is
    // async-signal-safe. A separate group lets PID 1 terminate a whole
    // transition, including service-script descendants.
    unsafe {
        command.pre_exec(|| {
            if libc::setpgid(0, 0) == -1 {
                return Err(io::Error::last_os_error());
            }
            Ok(())
        });
    }
    let child = command
        .spawn()
        .map_err(|error| format!("cannot run {program}: {error}"))?;
    let pid = i32::try_from(child.id()).map_err(|_| "child PID does not fit pid_t".to_owned())?;
    let deadline = Instant::now() + timeout;
    loop {
        if let Some(status) = reap_during_command(console, pid)? {
            return Ok(CommandResult::Exited(status));
        }
        if let Some(action) = take_shutdown_action() {
            terminate_process_group(pid, program, console);
            return Ok(CommandResult::Shutdown(action));
        }
        if Instant::now() >= deadline {
            terminate_process_group(pid, program, console);
            return Err(format!(
                "{program} {} timed out after {} seconds",
                arguments.join(" "),
                timeout.as_secs()
            ));
        }
        thread::sleep(POLL_INTERVAL);
    }
}

enum CommandResult {
    Exited(ExitStatus),
    Shutdown(ShutdownAction),
}

fn take_shutdown_action() -> Option<ShutdownAction> {
    shutdown_action(SHUTDOWN_SIGNAL.swap(0, Ordering::Relaxed))
}

fn reap_during_command(
    console: &mut Console,
    command_pid: libc::pid_t,
) -> Result<Option<ExitStatus>, String> {
    CHILD_EXITED.store(false, Ordering::Relaxed);
    loop {
        let mut status = 0;
        // SAFETY: status is writable and WNOHANG makes the call nonblocking.
        let pid = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) };
        match pid {
            value if value == command_pid => {
                return Ok(Some(ExitStatus::from_raw(status)));
            }
            value if value > 0 => {
                console.log(&format!(
                    "reaped orphan PID {value} ({})",
                    describe_wait_status(status)
                ));
            }
            0 => return Ok(None),
            _ => {
                let error = io::Error::last_os_error();
                return if error.raw_os_error() == Some(libc::ECHILD) {
                    Err("transition child disappeared without an exit status".to_owned())
                } else {
                    Err(format!("waitpid failed during transition: {error}"))
                };
            }
        }
    }
}

fn terminate_process_group(pid: libc::pid_t, program: &str, console: &mut Console) {
    // SAFETY: negative pid addresses the process group created before exec.
    let term_result = unsafe { libc::kill(-pid, libc::SIGTERM) };
    if term_result == -1 && io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH) {
        console.log(&format!("cannot terminate {program} process group {pid}"));
    }
    let deadline = Instant::now() + TERMINATION_GRACE;
    while Instant::now() < deadline {
        match reap_during_command(console, pid) {
            Ok(Some(_)) | Err(_) => return,
            Ok(None) => thread::sleep(POLL_INTERVAL),
        }
    }
    // SAFETY: negative pid addresses the same known process group.
    unsafe {
        libc::kill(-pid, libc::SIGKILL);
    }
    loop {
        match reap_during_command(console, pid) {
            Ok(Some(_)) | Err(_) => return,
            Ok(None) => thread::sleep(POLL_INTERVAL),
        }
    }
}

fn describe_exit_status(status: std::process::ExitStatus) -> String {
    status
        .code()
        .map_or_else(|| "a signal".to_owned(), |code| format!("status {code}"))
}

fn spawn_emergency_shell(console: &mut Console) -> Result<libc::pid_t, String> {
    let stdin = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console input: {error}"))?;
    let stdout = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console output: {error}"))?;
    let stderr = console
        .clone_file()
        .map_err(|error| format!("cannot duplicate console error output: {error}"))?;

    let mut command = Command::new(EMERGENCY_SHELL);
    command
        .arg("-l")
        .env_clear()
        .env("HOME", "/root")
        .env("LOGNAME", "root")
        .env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin")
        .env("SHELL", EMERGENCY_SHELL)
        .env("TERM", "linux")
        .env("USER", "root")
        .stdin(Stdio::from(stdin))
        .stdout(Stdio::from(stdout))
        .stderr(Stdio::from(stderr));

    // SAFETY: this closure runs after fork and before exec in the single-threaded
    // child. It invokes only async-signal-safe system calls and reports failures
    // through a preallocated io::Error.
    unsafe {
        command.pre_exec(|| {
            if libc::setsid() == -1 {
                return Err(io::Error::last_os_error());
            }
            if libc::ioctl(libc::STDIN_FILENO, libc::TIOCSCTTY, 0) == -1 {
                return Err(io::Error::last_os_error());
            }
            Ok(())
        });
    }

    let child = command
        .spawn()
        .map_err(|error| format!("cannot start {EMERGENCY_SHELL}: {error}"))?;

    let pid = i32::try_from(child.id()).map_err(|_| "child PID does not fit pid_t".to_owned())?;
    console.log(&format!("started emergency shell as PID {pid}"));
    Ok(pid)
}

fn reap_children(console: &mut Console, shell_pid: Option<libc::pid_t>) -> bool {
    let mut shell_exited = false;
    loop {
        let mut status = 0;
        // SAFETY: status points to writable storage and WNOHANG makes this nonblocking.
        let pid = unsafe { libc::waitpid(-1, &raw mut status, libc::WNOHANG) };
        match pid {
            value if value > 0 => {
                let description = describe_wait_status(status);
                if Some(value) == shell_pid {
                    console.log(&format!("emergency shell PID {value} {description}"));
                    shell_exited = true;
                } else {
                    console.log(&format!("reaped orphan PID {value} ({description})"));
                }
            }
            0 => break,
            _ => {
                let error = io::Error::last_os_error();
                if error.raw_os_error() != Some(libc::ECHILD) {
                    console.log(&format!("waitpid failed: {error}"));
                }
                break;
            }
        }
    }
    shell_exited
}

fn describe_wait_status(status: libc::c_int) -> String {
    if libc::WIFEXITED(status) {
        format!("exited with status {}", libc::WEXITSTATUS(status))
    } else if libc::WIFSIGNALED(status) {
        format!("was killed by signal {}", libc::WTERMSIG(status))
    } else {
        format!("changed state with wait status {status}")
    }
}

fn perform_shutdown(
    console: &mut Console,
    shell_pid: Option<libc::pid_t>,
    action: ShutdownAction,
) -> ! {
    console.log(&format!("received {action} request"));
    stop_shell(console, shell_pid);
    if let Err(error) = run_openrc(console, "shutdown", SHUTDOWN_TRANSITION_TIMEOUT, true) {
        console.log(&format!("OpenRC shutdown failed: {error}"));
    }

    // SAFETY: sync has no preconditions and is required before the terminal reboot call.
    unsafe {
        libc::sync();
    }
    let command = match action {
        ShutdownAction::Reboot => libc::RB_AUTOBOOT,
        ShutdownAction::Poweroff => libc::RB_POWER_OFF,
    };
    // SAFETY: cheesed is PID 1 and passes one of Linux's defined reboot commands.
    if unsafe { libc::reboot(command) } == -1 {
        console.log(&format!(
            "{action} syscall failed: {}; PID 1 will remain alive",
            io::Error::last_os_error()
        ));
    }

    loop {
        thread::sleep(Duration::from_secs(60));
    }
}

fn stop_shell(console: &mut Console, shell_pid: Option<libc::pid_t>) {
    let Some(pid) = shell_pid else {
        return;
    };

    // SAFETY: pid is a direct child PID and SIGTERM is a valid signal.
    if unsafe { libc::kill(pid, libc::SIGTERM) } == -1 {
        let error = io::Error::last_os_error();
        if error.raw_os_error() != Some(libc::ESRCH) {
            console.log(&format!("cannot stop emergency shell PID {pid}: {error}"));
        }
        return;
    }

    let deadline = Instant::now() + TERMINATION_GRACE;
    while Instant::now() < deadline {
        if wait_for_pid(pid) {
            console.log(&format!("emergency shell PID {pid} stopped"));
            return;
        }
        thread::sleep(POLL_INTERVAL);
    }

    console.log(&format!(
        "emergency shell PID {pid} exceeded shutdown timeout; sending SIGKILL"
    ));
    // SAFETY: pid is the known shell PID and SIGKILL is a valid signal.
    unsafe {
        libc::kill(pid, libc::SIGKILL);
    }
    let _ = wait_for_pid(pid);
}

fn wait_for_pid(pid: libc::pid_t) -> bool {
    let mut status = 0;
    // SAFETY: status points to writable storage, pid is a known child, and WNOHANG
    // makes the check nonblocking.
    let result = unsafe { libc::waitpid(pid, &raw mut status, libc::WNOHANG) };
    result == pid
        || (result == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD))
}