Blob: src/linux.rs
Raw · Blame
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::process::{Command, 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 POLL_INTERVAL: Duration = Duration::from_millis(50);
const SHELL_RESTART_DELAY: Duration = Duration::from_secs(1);
const SHUTDOWN_TIMEOUT: 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 {
SHUTDOWN_SIGNAL.store(signal, 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("C0 bootstrap complete; cheesed is PID 1");
let mut shell_pid = match spawn_emergency_shell(&mut console) {
Ok(pid) => Some(pid),
Err(error) => {
console.log(&error);
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 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())
}
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 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);
// 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() + SHUTDOWN_TIMEOUT;
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))
}