FDS/OS 1.0

This commit is contained in:
2026-09-21 22:29:23 +08:00
commit 99bc3d15c5
430 changed files with 34876 additions and 0 deletions
+533
View File
@@ -0,0 +1,533 @@
//! Linux PID-1 operations. No shell, external mount program or udev is used.
use fds_common::{
Error, Result,
boot::BootOptions,
read_text,
sysfs::{self, BlockPartition, Selection},
trace::{self, Point},
};
use std::{
ffi::CString,
fs,
io::{self, Read, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::{
fs::{FileTypeExt, MetadataExt, PermissionsExt},
process::CommandExt,
},
},
path::Path,
process::{Child, Command, Stdio},
};
fn c(value: &str) -> Result<CString> {
CString::new(value).map_err(|_| Error("NUL in syscall argument".into()))
}
fn checked(value: libc::c_int, operation: &str) -> Result<()> {
if value < 0 {
Err(Error(format!(
"{operation}: {}",
io::Error::last_os_error()
)))
} else {
Ok(())
}
}
fn mount(source: &str, target: &str, kind: &str, flags: libc::c_ulong) -> Result<()> {
let (source, target, kind) = (c(source)?, c(target)?, c(kind)?);
// All strings remain alive for the syscall; no data/options pointer is passed.
checked(
unsafe {
libc::mount(
source.as_ptr(),
target.as_ptr(),
kind.as_ptr(),
flags,
std::ptr::null(),
)
},
"mount",
)
}
fn move_mount(source: &str, target: &str) -> Result<()> {
let (source, target) = (c(source)?, c(target)?);
checked(
unsafe {
libc::mount(
source.as_ptr(),
target.as_ptr(),
std::ptr::null(),
libc::MS_MOVE,
std::ptr::null(),
)
},
"move mount",
)
}
fn unmount(target: &str) -> Result<()> {
checked(unsafe { libc::umount(c(target)?.as_ptr()) }, "unmount")
}
fn prepare() -> Result<()> {
if unsafe { libc::getpid() } != 1 || unsafe { libc::geteuid() } != 0 {
return Err(Error(
"Normal stage0 boot requires root PID 1 in an initramfs".into(),
));
}
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
checked(
unsafe { libc::statfs(c("/")?.as_ptr(), &mut stat) },
"inspect initial root",
)?;
// Linux UAPI linux/magic.h; libc exposes differing signed types by ABI.
if ![0x8584_58f6_u64, 0x0102_1994_u64].contains(&(stat.f_type as u64)) {
return Err(Error(
"Refusing root handoff: initial root is not ramfs/tmpfs".into(),
));
}
for path in ["/proc", "/sys", "/dev", "/newroot"] {
fs::create_dir_all(path)?;
}
mount(
"proc",
"/proc",
"proc",
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
)?;
mount(
"sysfs",
"/sys",
"sysfs",
libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
)?;
mount("devtmpfs", "/dev", "devtmpfs", libc::MS_NOSUID)?;
fs::create_dir_all("/dev/fds-early")?;
fs::set_permissions("/dev/fds-early", fs::Permissions::from_mode(0o700))?;
Ok(())
}
fn netlink() -> Result<OwnedFd> {
let fd = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
libc::NETLINK_KOBJECT_UEVENT,
)
};
checked(fd, "open kernel uevent socket")?;
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
address.nl_family = libc::AF_NETLINK as u16;
address.nl_groups = 1;
checked(
unsafe {
libc::bind(
fd.as_raw_fd(),
(&address as *const libc::sockaddr_nl).cast(),
std::mem::size_of_val(&address) as _,
)
},
"bind kernel uevent socket",
)?;
Ok(fd)
}
fn child_events() -> Result<OwnedFd> {
let mut signals: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut signals);
libc::sigaddset(&mut signals, libc::SIGCHLD);
}
checked(
unsafe { libc::sigprocmask(libc::SIG_BLOCK, &signals, std::ptr::null_mut()) },
"block SIGCHLD",
)?;
let fd = unsafe { libc::signalfd(-1, &signals, libc::SFD_CLOEXEC | libc::SFD_NONBLOCK) };
checked(fd, "open child event descriptor")?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn start_monitor(debug: bool) -> Result<Child> {
let mut command = Command::new("/sbin/dasungd");
command
.args(["--config", "/etc/dasungd-early.toml", "daemon"])
.env_clear();
if !debug {
command.stdout(Stdio::null()).stderr(Stdio::null());
}
command
.spawn()
.map_err(|e| Error(format!("Start early Dasung controller: {e}")))
}
fn stop_monitor(child: &mut Child) -> Result<()> {
if child.try_wait()?.is_some() {
return Ok(());
}
let pid = child.id() as libc::pid_t;
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as libc::c_int;
checked(fd, "open early-controller process handle")?;
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
checked(
unsafe { libc::kill(pid, libc::SIGTERM) },
"stop early monitor controller",
)?;
let mut poll = libc::pollfd {
fd: fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
loop {
// This is a process-exit deadline, not a delay before proceeding.
let ready = unsafe { libc::poll(&mut poll, 1, 2000) };
if ready < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
checked(ready, "wait for early controller exit")?;
if ready == 0 {
child.kill()?;
}
child.wait()?;
return Ok(());
}
}
fn try_root(device: &BlockPartition) -> Result<()> {
let node = fs::File::open(&device.device)?;
let stat = node.metadata()?;
if !stat.file_type().is_block_device()
|| libc::major(stat.rdev()) != device.major
|| libc::minor(stat.rdev()) != device.minor
{
return Err(Error("Block device changed during discovery".into()));
}
mount(
&format!("/proc/self/fd/{}", node.as_raw_fd()),
"/newroot",
"erofs",
libc::MS_RDONLY,
)?;
let verify = || -> Result<()> {
let identity = read_text(Path::new("/newroot/usr/lib/os-release"), 16384)?;
if !identity.lines().any(|line| line == "ID=fds") {
return Err(Error("Selected root is not an FDS image".into()));
}
for path in [
"/newroot/sbin/init",
"/newroot/usr/bin/execlineb",
"/newroot/usr/bin/s6-linux-init",
] {
let meta = fs::metadata(path)?;
if !meta.is_file() || meta.mode() & 0o111 == 0 {
return Err(Error(format!("Missing executable: {path}")));
}
}
for path in ["/newroot/proc", "/newroot/sys", "/newroot/dev"] {
let meta = fs::symlink_metadata(path)?;
if !meta.is_dir() {
return Err(Error(format!("Invalid early mountpoint: {path}")));
}
}
Ok(())
};
if let Err(error) = verify() {
unmount("/newroot")?;
return Err(error);
}
Ok(())
}
fn remove_initial_tree(path: &Path, device: u64) -> Result<()> {
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = fs::symlink_metadata(entry.path())?;
if metadata.dev() != device {
continue;
}
if metadata.is_dir() {
remove_initial_tree(&entry.path(), device)?;
fs::remove_dir(entry.path())?;
} else {
fs::remove_file(entry.path())?;
}
}
Ok(())
}
fn handoff(mut monitor: Child, debug: bool) -> Result<()> {
stop_monitor(&mut monitor)?;
if let Ok(instant) = trace::now() {
trace::mark_early(Point::RootSwitch, instant);
}
for source in ["/proc", "/sys", "/dev"] {
move_mount(source, &format!("/newroot{source}"))?;
}
let old_device = fs::metadata("/")?.dev();
if fs::metadata("/newroot")?.dev() == old_device {
return Err(Error("New root is not a separate filesystem".into()));
}
std::env::set_current_dir("/newroot")?;
// Only the initial ramfs/tmpfs is removed. Moved mounts and EROFS are skipped.
remove_initial_tree(Path::new("/"), old_device)?;
move_mount("/newroot", "/")?;
checked(unsafe { libc::chroot(c(".")?.as_ptr()) }, "enter new root")?;
std::env::set_current_dir("/")?;
let mut empty: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut empty);
}
checked(
unsafe { libc::sigprocmask(libc::SIG_SETMASK, &empty, std::ptr::null_mut()) },
"restore init signal mask",
)?;
if debug {
println!("FDS_STAGE0_HANDOFF: native s6 on read-only SYSTEM");
}
let error = Command::new("/sbin/init")
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("LANG", "en_US.UTF-8")
.exec();
Err(Error(format!("Execute native init: {error}")))
}
fn power(command: &str) -> Result<()> {
let action = match command {
"reboot" => libc::LINUX_REBOOT_CMD_RESTART,
"poweroff" => libc::LINUX_REBOOT_CMD_POWER_OFF,
_ => return Err(Error("Invalid power command".into())),
};
checked(
unsafe { libc::reboot(action) },
"early-userspace power request",
)
}
#[derive(Default)]
struct ConsoleInput {
pending: Vec<u8>,
discard: bool,
}
impl ConsoleInput {
fn accept(&mut self, bytes: &[u8]) -> Vec<String> {
let mut commands = Vec::new();
for &byte in bytes {
if byte == b'\n' || byte == b'\r' {
if !self.discard {
commands.push(String::from_utf8_lossy(&self.pending).trim().to_owned());
}
self.pending.clear();
self.discard = false;
} else if !self.discard && self.pending.len() < 256 {
self.pending.push(byte);
} else {
// Discard the complete overlong line, including any command suffix.
self.pending.clear();
self.discard = true;
}
}
commands
}
}
fn read_console(input: &mut ConsoleInput) -> Result<Option<Vec<String>>> {
let mut bytes = [0; 256];
let count = io::stdin().read(&mut bytes)?;
if count == 0 {
return Ok(None);
}
Ok(Some(input.accept(&bytes[..count])))
}
fn block_event(fd: &OwnedFd) -> Result<bool> {
let mut bytes = [0; 65536];
let mut from: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
let mut length = std::mem::size_of_val(&from) as libc::socklen_t;
let count = unsafe {
libc::recvfrom(
fd.as_raw_fd(),
bytes.as_mut_ptr().cast(),
bytes.len(),
0,
(&mut from as *mut libc::sockaddr_nl).cast(),
&mut length,
)
};
if count < 0 {
// Lost notifications require a fresh scan, never an arbitrary wait.
if io::Error::last_os_error().raw_os_error() == Some(libc::ENOBUFS) {
return Ok(true);
}
return Err(io::Error::last_os_error().into());
}
Ok(from.nl_pid == 0
&& bytes[..count as usize]
.split(|b| *b == 0)
.any(|field| field == b"SUBSYSTEM=block"))
}
pub fn boot() -> Result<()> {
let entered = trace::now().ok();
prepare()?;
if let Some(instant) = entered {
trace::mark_early(Point::Stage0Start, instant);
}
let mut options = BootOptions::parse(&read_text(Path::new("/proc/cmdline"), 65536)?)?;
let events = netlink()?; // Bind before scanning so insertion cannot fall into a gap.
let signals = child_events()?;
let mut monitor = start_monitor(options.debug)?;
println!("FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM 0.1");
println!("Commands while waiting: list, rescan, recovery, reboot, poweroff");
let mut previous = String::new();
let mut pending = ConsoleInput::default();
let mut console_open = true;
let mut restart_count = 0;
loop {
let candidates = sysfs::partitions(Path::new("/sys"))?;
let selection = sysfs::select(&candidates, options.root_label());
let state = match &selection {
Selection::Missing => format!(
"{} MEDIA NOT PRESENT\nINSERT {} CARTRIDGE",
options.root_label(),
options.root_label()
),
Selection::Ambiguous(devices) => format!(
"MULTIPLE {} CARTRIDGES ({})\nREMOVE EXTRA MEDIA OR ENTER recovery",
options.root_label(),
devices.len()
),
Selection::Unique(device) => {
let found = trace::now().ok();
match try_root(device) {
Ok(()) => {
if let Some(instant) = found {
trace::mark_early(Point::SystemFound, instant);
}
if let Ok(instant) = trace::now() {
trace::mark_early(Point::RootMounted, instant);
}
if options.debug {
println!("FDS_STAGE0_ROOT: {}", options.root_label());
}
println!("MEMORY ........ READY\nSYSTEM ........ FDS/OS 0.1");
return handoff(monitor, options.debug);
}
Err(error) => {
format!("SYSTEM CANNOT BE USED: {error}\nREPLACE MEDIA OR ENTER recovery")
}
}
}
};
if state != previous {
println!("{state}");
if options.debug {
println!("FDS_STAGE0_WAIT");
}
io::stdout().flush()?;
previous = state;
}
loop {
let mut fds = [
libc::pollfd {
fd: events.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if console_open { 0 } else { -1 },
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: signals.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
];
let ready = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, -1) };
if ready < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
checked(ready, "wait for boot events")?;
if fds[2].revents & libc::POLLIN != 0 {
let mut info: libc::signalfd_siginfo = unsafe { std::mem::zeroed() };
unsafe {
libc::read(
signals.as_raw_fd(),
(&mut info as *mut libc::signalfd_siginfo).cast(),
std::mem::size_of_val(&info),
);
}
if let Some(status) = monitor.try_wait()? {
if restart_count >= 3 {
return Err(Error(format!(
"Early monitor controller repeatedly exited: {status}"
)));
}
restart_count += 1;
monitor = start_monitor(options.debug)?;
}
}
let mut rescan = fds[0].revents & libc::POLLIN != 0 && block_event(&events)?;
if fds[1].revents & libc::POLLIN != 0 {
let commands = read_console(&mut pending)?;
console_open = commands.is_some();
for command in commands.into_iter().flatten() {
match command.as_str() {
"recovery" => {
options.mode = fds_common::boot::BootMode::Recovery;
rescan = true;
}
"rescan" => rescan = true,
"list" => println!(
"{}",
serde_json::to_string(&candidates).map_err(|e| Error(e.to_string()))?
),
"reboot" | "poweroff" => {
stop_monitor(&mut monitor)?;
power(&command)?;
}
"" => (),
_ => println!("Commands: list, rescan, recovery, reboot, poweroff"),
}
}
}
if fds[1].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
console_open = false;
}
if rescan {
break;
}
}
}
}
pub fn emergency(error: &Error) -> ! {
eprintln!("FDS_STAGE0_ERROR: {error}\nBoot stopped. Enter reboot or poweroff.");
let mut pending = ConsoleInput::default();
let mut console_open = true;
loop {
let mut input = libc::pollfd {
fd: if console_open { 0 } else { -1 },
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut input, 1, -1) };
if ready > 0 && input.revents & libc::POLLIN != 0 {
if let Ok(commands) = read_console(&mut pending) {
console_open = commands.is_some();
for command in commands.into_iter().flatten() {
if matches!(command.as_str(), "reboot" | "poweroff") {
let _ = power(&command);
}
}
}
} else if ready > 0 {
// With no console, wait for an external signal instead of busy looping.
unsafe {
libc::pause();
}
}
}
}
#[cfg(test)]
mod tests {
use super::ConsoleInput;
#[test]
fn console_handles_split_input_and_discards_overlong_command_suffixes() {
let mut input = ConsoleInput::default();
assert!(input.accept(b"reco").is_empty());
assert_eq!(input.accept(b"very\n"), ["recovery"]);
assert!(input.accept(&[b'x'; 256]).is_empty());
assert!(input.accept(b"xpoweroff\n").is_empty());
assert_eq!(input.accept(b"list\n"), ["list"]);
}
}
+104
View File
@@ -0,0 +1,104 @@
mod linux;
use clap::{Parser, ValueEnum};
use fds_common::{Error, MAX_CONFIG_BYTES, Result, boot::BootOptions, read_text, sysfs};
use std::{path::PathBuf, process::ExitCode};
#[derive(Parser)]
#[command(
version,
about = "Early boot discovery and native s6 handoff",
after_help = "With no arguments, root PID 1 boots FDS. Diagnostic options only read their supplied inputs."
)]
struct Cli {
/// Validate a kernel command-line file without booting.
#[arg(long, value_name = "FILE", conflicts_with = "probe")]
check_cmdline: Option<PathBuf>,
/// Inspect SYSTEM or RECOVERY discovery in the supplied sysfs tree.
#[arg(long, value_name = "SYSFS_ROOT")]
probe: Option<PathBuf>,
#[arg(value_enum, requires = "probe", conflicts_with = "check_cmdline")]
label: Option<Label>,
}
#[derive(Clone, Copy, ValueEnum)]
#[value(rename_all = "SCREAMING_SNAKE_CASE")]
enum Label {
FdsSystem,
FdsRecovery,
}
fn run(cli: Cli) -> Result<()> {
if let Some(path) = cli.check_cmdline {
let options = BootOptions::parse(&read_text(&path, MAX_CONFIG_BYTES)?)?;
println!(
"{}",
serde_json::to_string(&options).map_err(|e| Error(e.to_string()))?
);
} else if let Some(path) = cli.probe {
let label = match cli.label.unwrap_or(Label::FdsSystem) {
Label::FdsSystem => "FDS_SYSTEM",
Label::FdsRecovery => "FDS_RECOVERY",
};
let selection = sysfs::select(&sysfs::partitions(&path)?, label);
println!(
"{}",
serde_json::to_string(&selection).map_err(|e| Error(e.to_string()))?
);
} else {
return linux::boot();
}
Ok(())
}
fn main() -> ExitCode {
// PID 1 must enter the emergency path instead of exiting on malformed options.
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(error) => {
if unsafe { libc::getpid() } == 1 {
linux::emergency(&Error(error.to_string()));
}
error.exit();
}
};
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
if unsafe { libc::getpid() } == 1 {
linux::emergency(&error);
}
eprintln!("fds-stage0: {error}");
ExitCode::from(2)
}
}
}
#[cfg(test)]
mod cli_tests {
use super::*;
use clap::{CommandFactory, Parser};
#[test]
fn typed_command_contract() {
Cli::command().debug_assert();
assert!(Cli::try_parse_from(["fds-stage0"]).unwrap().probe.is_none());
for args in [
vec!["fds-stage0", "--probe", "/sys"],
vec!["fds-stage0", "--probe", "/sys", "FDS_RECOVERY"],
vec!["fds-stage0", "--check-cmdline", "cmdline"],
] {
assert!(Cli::try_parse_from(args).is_ok());
}
for args in [
vec!["fds-stage0", "FDS_SYSTEM"],
vec!["fds-stage0", "--probe", "/sys", "other"],
vec![
"fds-stage0",
"--probe",
"/sys",
"--check-cmdline",
"cmdline",
],
vec!["fds-stage0", "--check-cmdline", "cmdline", "FDS_RECOVERY"],
] {
assert!(Cli::try_parse_from(&args).is_err(), "{args:?}");
}
}
}