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))
}