#![allow(clippy::doc_markdown)]
#![doc = include_str!("../README.md")]
use std::ffi::CString;
use std::io;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
mod syscall;
#[derive(Debug, Clone)]
pub struct KexecRequest {
pub kernel: PathBuf,
pub initrd: Option<PathBuf>,
pub cmdline: String,
}
#[derive(Debug, thiserror::Error)]
pub enum KexecError {
#[error("kernel signature verification failed (KEXEC_SIG rejected the image)")]
SignatureRejected,
#[error("operation refused by kernel lockdown (Secure Boot enforcing)")]
LockdownRefused,
#[error("kernel image format not recognized (kexec_file_load returned ENOEXEC)")]
UnsupportedImage,
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("path contains interior NUL byte: {0}")]
InvalidPath(PathBuf),
#[error("kexec is only available on Linux")]
Unsupported,
}
#[cfg(target_os = "linux")]
pub fn load_dry(req: &KexecRequest) -> Result<(), KexecError> {
let kernel_fd = open_path(&req.kernel)?;
let initrd_fd = req.initrd.as_deref().map(open_path).transpose()?;
let cmdline = CString::new(req.cmdline.as_bytes())
.map_err(|_| KexecError::InvalidPath(PathBuf::from(&req.cmdline)))?;
syscall::kexec_file_load(
kernel_fd.as_raw(),
initrd_fd.as_ref().map(OwnedFd::as_raw),
&cmdline,
)
}
#[cfg(not(target_os = "linux"))]
pub fn load_dry(_req: &KexecRequest) -> Result<(), KexecError> {
Err(KexecError::Unsupported)
}
#[cfg(target_os = "linux")]
pub fn load_and_exec(req: &KexecRequest) -> Result<std::convert::Infallible, KexecError> {
load_dry(req)?;
syscall::reboot_kexec()?;
Err(KexecError::Io(io::Error::other(
"reboot(LINUX_REBOOT_CMD_KEXEC) returned unexpectedly",
)))
}
#[cfg(not(target_os = "linux"))]
pub fn load_and_exec(_req: &KexecRequest) -> Result<std::convert::Infallible, KexecError> {
Err(KexecError::Unsupported)
}
#[cfg(target_os = "linux")]
struct OwnedFd(libc::c_int);
#[cfg(target_os = "linux")]
impl OwnedFd {
fn as_raw(&self) -> libc::c_int {
self.0
}
}
#[cfg(target_os = "linux")]
impl Drop for OwnedFd {
fn drop(&mut self) {
#[allow(unsafe_code)]
unsafe {
libc::close(self.0);
}
}
}
#[cfg(target_os = "linux")]
fn open_path(path: &Path) -> Result<OwnedFd, KexecError> {
let c_path = path_to_cstring(path)?;
#[allow(unsafe_code)]
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(KexecError::Io(io::Error::last_os_error()));
}
Ok(OwnedFd(fd))
}
fn path_to_cstring(path: &Path) -> Result<CString, KexecError> {
use std::os::unix::ffi::OsStrExt;
CString::new(path.as_os_str().as_bytes())
.map_err(|_| KexecError::InvalidPath(path.to_path_buf()))
}
#[must_use]
pub fn classify_errno(errno: i32) -> KexecError {
match errno {
libc::EKEYREJECTED => KexecError::SignatureRejected,
libc::EPERM => KexecError::LockdownRefused,
libc::ENOEXEC => KexecError::UnsupportedImage,
other => KexecError::Io(io::Error::from_raw_os_error(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_signature_rejection() {
assert!(matches!(
classify_errno(libc::EKEYREJECTED),
KexecError::SignatureRejected
));
}
#[test]
fn classify_lockdown() {
assert!(matches!(
classify_errno(libc::EPERM),
KexecError::LockdownRefused
));
}
#[test]
fn classify_bad_image() {
assert!(matches!(
classify_errno(libc::ENOEXEC),
KexecError::UnsupportedImage
));
}
#[test]
fn classify_generic_io_preserves_errno() {
let err = classify_errno(libc::ENOENT);
let KexecError::Io(io_err) = err else {
panic!("expected Io variant");
};
assert_eq!(io_err.raw_os_error(), Some(libc::ENOENT));
}
#[test]
fn path_ok_round_trips() {
let ok = Path::new("/boot/vmlinuz-rescue");
let c = path_to_cstring(ok).unwrap_or_else(|_| panic!("valid path"));
assert_eq!(c.to_bytes(), b"/boot/vmlinuz-rescue");
}
#[test]
fn path_with_nul_byte_rejected() {
let bad = Path::new("/tmp/has\0nul");
assert!(matches!(
path_to_cstring(bad),
Err(KexecError::InvalidPath(_))
));
}
#[test]
#[ignore = "requires root + would kexec the host; opt-in via `cargo test -- --ignored`"]
#[cfg(target_os = "linux")]
fn load_and_exec_rejects_nonexistent_kernel() {
let req = KexecRequest {
kernel: PathBuf::from("/nonexistent/vmlinuz"),
initrd: None,
cmdline: String::new(),
};
let err = load_and_exec(&req).expect_err("must fail");
assert!(matches!(err, KexecError::Io(_)));
}
}