diff --git a/Cargo.lock b/Cargo.lock index e7225f92df4..6e9fe25b4db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1306,6 +1306,7 @@ dependencies = [ "toyos-fat32-check", "toyos-gpt", "toyos-manifest", + "toyos-osrelease", ] [[package]] @@ -1955,7 +1956,6 @@ dependencies = [ name = "kernel" version = "0.1.0" dependencies = [ - "bcachefs", "dlmalloc", "hashbrown 0.16.1", "toyos-abi", diff --git a/build/bootlog.rs b/build/bootlog.rs index 00a1e5d14d5..d5910cbb65d 100644 --- a/build/bootlog.rs +++ b/build/bootlog.rs @@ -16,7 +16,7 @@ use std::fmt; /// running to write it there. pub const REBOOTING: &str = "Rebooting."; -/// What `/system/bin/supervisor` says as it asks `logkeeper` to make the log whole, before +/// What the supervisor says as it asks `logkeeper` to make the log whole, before /// it stops the machine: the last line a passing boot's log is owed, because /// nothing after it waits for the file. pub const STOPPING: &str = toyos_logstream::STOPPING; diff --git a/build/build.rs b/build/build.rs index c8395e68a9d..8c57267ccbe 100644 --- a/build/build.rs +++ b/build/build.rs @@ -109,7 +109,7 @@ struct SystemConfig { symlinks: BTreeMap, #[serde(default)] assets: Vec, - /// What `/system/bin/supervisor` starts at boot. Program *keys*, never paths — a path + /// What the supervisor starts at boot. Program *keys*, never paths — a path /// here is a second spelling of a `[programs]` key and is what let a boot /// list smuggle an argument through. Arguments live on the program entry's /// `args` instead. @@ -350,12 +350,16 @@ fn config_crates(root: &Path, config: &SystemConfig) -> Vec { features: Features::Default, }); } - crates.push(ConfigCrate { - name: SUPERVISOR_PROGRAM.to_string(), - dir: program_dir(root, SUPERVISOR_PROGRAM), - built: Built::Member, - features: Features::Default, - }); + for name in BOOT_PROGRAMS { + if !config.programs.contains_key(name) { + crates.push(ConfigCrate { + name: name.to_string(), + dir: program_dir(root, name), + built: Built::Member, + features: Features::Default, + }); + } + } crates } @@ -598,13 +602,35 @@ fn assert_overflow_checked(what: &str, image: &[u8]) { // --- Shared root-image assembly --- -/// Build all programs from a config and assemble the ROOT image. /// The one program the kernel starts, in **every** image whatever `[programs]` -/// says. It reads the manifest below and starts what that names, so a ROOT image +/// says. It reads the manifest below and starts what that names, so an image /// without it is a machine with a kernel and no userland at all. const SUPERVISOR_PROGRAM: &str = "supervisor"; -/// Names `/system/bin/supervisor` serves itself. +/// The file server, which the supervisor serves ROOT with before it reads the +/// manifest, so in every image too. +const FILESERVER_PROGRAM: &str = "fileserver"; + +/// The programs a slot carries beside the kernel, as signed sections of their +/// own, and ROOT does not (`toyos_update::image`). +const BOOT_PROGRAMS: [&str; 2] = [SUPERVISOR_PROGRAM, FILESERVER_PROGRAM]; + +/// What a config's userland build makes: ROOT, and the two programs a slot +/// carries beside it. +pub struct Userland { + pub root: Vec, + pub supervisor: Vec, + pub fileserver: Vec, +} + +impl Userland { + /// The image of this userland and `kernel`. + pub fn release<'a>(&'a self, kernel: &'a [u8]) -> image::Release<'a> { + image::Release { kernel, supervisor: &self.supervisor, fileserver: &self.fileserver, root: &self.root } + } +} + +/// Names the supervisor serves itself. /// /// The supervisor is in every image and is no `[programs]` key, so these have no /// declaration to come from. They travel in the manifest so the supervisor creates @@ -741,7 +767,7 @@ fn receives_reach_a_running_server(config: &SystemConfig) -> Result<(), String> } } -/// The resolved config as the records `/system/bin/supervisor` reads. +/// The resolved config as the records the supervisor reads. /// /// The format, the renderer and the parser are `lib/manifest/`, whose /// round-trip test is what makes "what the build writes is what the supervisor reads" a @@ -761,7 +787,10 @@ fn render_manifest(config: &SystemConfig) -> Vec { let cfg = &config.programs[*name]; toyos_manifest::Program { name: (*name).clone(), - path: format!("/system/bin/{name}"), + source: match name.as_str() { + FILESERVER_PROGRAM => toyos_manifest::Source::BootFileServer, + _ => toyos_manifest::Source::Path(format!("/system/bin/{name}")), + }, args: cfg.args.clone(), serves: cfg.serves.clone(), provides: cfg.provides.clone(), @@ -858,13 +887,18 @@ fn build_and_assemble( quiet: bool, arch: Arch, machines: crate::microcode::Machines, -) -> Vec { +) -> Userland { // Before anything is built, so a file edited during the build is not // credited to the state it was read in. let release = release(root, env.sysroot.identity.of_target(arch.userland()), arch); let mut root_files: Vec<(String, Vec)> = vec![(toyos_osrelease::PATH.to_string(), release.to_string().into_bytes())]; build_programs(root, config, env, quiet, arch, &mut root_files); + let mut take = |name: &str| { + let at = root_files.iter().position(|(file, _)| *file == format!("bin/{name}")).expect("every image builds its boot programs"); + root_files.remove(at).1 + }; + let (supervisor, fileserver) = (take(SUPERVISOR_PROGRAM), take(FILESERVER_PROGRAM)); root_files.push((toyos_manifest::PATH.to_string(), render_manifest(config))); let programs: BTreeSet<&str> = config.programs.keys().map(String::as_str).collect(); @@ -905,10 +939,10 @@ fn build_and_assemble( panic!("{why}"); } - image::create_root_image(&root_files, &symlinks, quiet) + Userland { root: image::create_root_image(&root_files, &symlinks, quiet), supervisor, fileserver } } -/// Build `config`'s programs and the supervisor for `arch`, and add each to `root_files`. +/// Build `config`'s programs and [`BOOT_PROGRAMS`] for `arch`, and add each to `root_files`. fn build_programs( root: &Path, config: &SystemConfig, @@ -970,7 +1004,7 @@ fn symlink_target_name(to: &str) -> &str { } /// The converse of the crate assertion above: a `bin/` entry no name reaches. -/// A name buys authority — `/system/bin/supervisor` builds a `[programs]` row's namespace and +/// A name buys authority — the supervisor builds a `[programs]` row's namespace and /// device claims — and a harness binary has none, holding only what its spawner /// moved in, so it is legal exactly when the config starts something that could /// spawn it. **An inventory over the `bin/` namespace, not a reachability @@ -982,7 +1016,7 @@ fn unnamed_program( ) -> Result<(), String> { for name in names { let Some(program) = name.strip_prefix("bin/") else { continue }; - if program == SUPERVISOR_PROGRAM || programs.contains(program) { + if programs.contains(program) { continue; } if HARNESS_PREFIXES.iter().any(|p| name.starts_with(p)) { @@ -997,7 +1031,7 @@ fn unnamed_program( continue; } return Err(format!( - "the image carries {name} and no `[programs]` row names it, so `/system/bin/supervisor` can build \ + "the image carries {name} and no `[programs]` row names it, so the supervisor can build \ it no namespace and no device claim; add a row or take the file out" )); } @@ -1809,17 +1843,16 @@ pub fn build(root: &Path, boot: Boot, plan: &Plan) -> PathBuf { return image_path; } - let (kernel_bytes, bl_bytes, root_bytes) = shipped_parts(root, &boot, plan); + let (kernel_bytes, bl_bytes, userland) = shipped_parts(root, &boot, plan); let key = said_key(plan); // A machine this image is flashed onto updates itself, so it carries // the second slot an update is written to, with room for a ROOT twice // this one's size. - let second = image::SecondSlot { root_bytes: 2 * root_bytes.len() as u64 }; + let second = image::SecondSlot { root_bytes: 2 * userland.root.len() as u64 }; let disk_bytes = image::create_boot_image( plan.arch, - &kernel_bytes, &bl_bytes, - &root_bytes, + userland.release(&kernel_bytes), &plan.params.join(","), image::Signing { key, version: plan.version }, Some(second), @@ -1840,11 +1873,10 @@ pub fn build(root: &Path, boot: Boot, plan: &Plan) -> PathBuf { /// slot, signed with this run's key at the plan's version. pub fn build_update(root: &Path, boot: &Boot, plan: &Plan, out: &Path) { assert!(!boot.case, "an update image is built from a mode's config, and a case's image is a test's"); - let (kernel_bytes, _, root_bytes) = shipped_parts(root, boot, plan); + let (kernel_bytes, _, userland) = shipped_parts(root, boot, plan); let key = said_key(plan); let bytes = image::update_image( - &kernel_bytes, - &root_bytes, + userland.release(&kernel_bytes), &plan.params.join(","), image::Signing { key, version: plan.version }, ); @@ -1866,8 +1898,8 @@ fn said_key(plan: &Plan) -> &'static crate::signing::Key { key } -/// The kernel, the loader and ROOT a mode's image is made of. -fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Vec) { +/// The kernel, the loader and the userland a mode's image is made of. +fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Userland) { let kernel_features = plan.features.join(","); let arch = plan.arch; @@ -1894,10 +1926,10 @@ fn shipped_parts(root: &Path, boot: &Boot, plan: &Plan) -> (Vec, Vec, Ve ) }; - let root_bytes = build_and_assemble(root, &config, &env, &[], false, arch, plan.machines); + let userland = build_and_assemble(root, &config, &env, &[], false, arch, plan.machines); let bl_bytes = fs::read(&bl_art).expect("Failed to read staged bootloader"); - (kernel_bytes, bl_bytes, root_bytes) + (kernel_bytes, bl_bytes, userland) } /// Create an empty disk image the guest sees at full size and the host pays @@ -1955,21 +1987,21 @@ pub fn designate_for_format(path: &Path, len: u64) { /// What it does not see is a source edit that lands mid-run. A run is a /// measurement of one tree, so that is the behaviour wanted either way; a run /// that *starts* after a kernel edit still rebuilds every variant it uses. -struct Memo(std::sync::Mutex>>>); +struct Memo>(std::sync::Mutex>>); -impl Memo { +impl Memo { const fn new() -> Self { Self(std::sync::Mutex::new(BTreeMap::new())) } - fn get(&self, key: u64) -> Option>> { + fn get(&self, key: u64) -> Option> { self.0.lock().expect("a build panicked holding the artifact memo").get(&key).cloned() } /// The lock is deliberately not held across `make`: a build that panics /// under it would poison the memo, and every later boot would then fail on /// the poison instead of on whatever went wrong with it. - fn get_or_build(&self, key: u64, make: impl FnOnce() -> Vec) -> Arc> { + fn get_or_build(&self, key: u64, make: impl FnOnce() -> T) -> Arc { if let Some(hit) = self.get(key) { return hit; } @@ -1984,7 +2016,7 @@ impl Memo { static KERNEL: Memo = Memo::new(); static BOOTLOADER: Memo = Memo::new(); -static ROOT_IMAGE: Memo = Memo::new(); +static USERLAND: Memo = Memo::new(); /// What the ROOT image is a function of: the config naming the programs, the /// architecture and whether they are built at all, the key its @@ -2020,9 +2052,8 @@ pub fn build_test_image( let parts = build_test_parts(root, plan, quiet, extra_files); image::create_boot_image( plan.arch, - &parts.kernel, &parts.bootloader, - &parts.root, + parts.userland.release(&parts.kernel), &plan.params.join(","), image::Signing { key: crate::signing::key(), version: plan.version }, None, @@ -2033,7 +2064,7 @@ pub fn build_test_image( pub struct Parts { pub kernel: Arc>, pub bootloader: Arc>, - pub root: Arc>, + pub userland: Arc, } /// [`Parts`] for a plan: built, or this process's memo of them. @@ -2067,9 +2098,9 @@ pub fn build_test_parts( // Nothing left to build, so nothing for the lock, the toolchain check or the // staleness sweep to protect. - let memo = (KERNEL.get(kernel_key), BOOTLOADER.get(bl_key), ROOT_IMAGE.get(root_image_key)); - if let (Some(kernel), Some(bootloader), Some(root)) = memo { - return Parts { kernel, bootloader, root }; + let memo = (KERNEL.get(kernel_key), BOOTLOADER.get(bl_key), USERLAND.get(root_image_key)); + if let (Some(kernel), Some(bootloader), Some(userland)) = memo { + return Parts { kernel, bootloader, userland }; } // A cache miss is shared setup, not a property of whichever test happened @@ -2113,13 +2144,13 @@ pub fn build_test_parts( (kernel, bl) }; - let root_bytes = ROOT_IMAGE.get_or_build(root_image_key, || { + let userland = USERLAND.get_or_build(root_image_key, || { build_and_assemble(root, &config, &env, extra_files, quiet, arch, plan.machines) }); drop(building); - Parts { kernel: kernel_bytes, bootloader: bl_bytes, root: root_bytes } + Parts { kernel: kernel_bytes, bootloader: bl_bytes, userland } } /// Build all binaries in a multi-binary crate. Returns vec of (binary_name, bytes). @@ -3364,7 +3395,7 @@ mod tests { } /// The diagnostic image's whole reason for existing: nothing in it can claim - /// the framebuffer, so the kernel's boot log stays on the panel. `/system/bin/supervisor` + /// the framebuffer, so the kernel's boot log stays on the panel. The supervisor /// is in every image and could reach a device, so the property becomes "the /// config declares no `devices`" — checkable here for the first time. #[test] @@ -3379,7 +3410,7 @@ mod tests { } /// `[boot] start` names program keys, so a typo is a build error rather than - /// a refusal `/system/bin/supervisor` reports at boot. + /// a refusal the supervisor reports at boot. fn started_programs_are_declared(cfg: &SystemConfig) -> Result<(), String> { for name in &cfg.boot.start { if !cfg.programs.contains_key(name) { @@ -3408,7 +3439,7 @@ mod tests { let programs = declared(&["shell"]); let started = ["shell".to_string()]; assert!(unnamed_program( - &["bin/supervisor", "bin/shell", "lib/libtls_lib.so", "etc/system.manifest"], + &["bin/shell", "lib/libtls_lib.so", "etc/system.manifest"], &programs, &started, ) diff --git a/build/image.rs b/build/image.rs index 9b4eab87db7..e6d563fef73 100644 --- a/build/image.rs +++ b/build/image.rs @@ -9,8 +9,8 @@ use toyos_fat32::{BlockAccess, Fat32, FatTime, IoError}; use toyos_gpt::Guid; /// The image that goes on the ROOT partition, named by a UUID **derived, never -/// drawn**: two builds of one tree have to agree on the name the kernel -/// argument carries. +/// drawn**: two builds of one tree have to agree on the name the file server +/// serving it says it by (`toyos_osrelease::ROOT_SAID`). /// /// **A set, not a sequence.** Both lists are sorted by name here, so the /// volume's bytes and the UUID over them are a function of what the caller @@ -153,9 +153,8 @@ fn root_uuid(files: &[&(String, Vec)], symlinks: &[&(String, String)]) -> Fs FsUuid(uuid) } -/// The name the ROOT image `bytes` carries, read back out of its superblock, so -/// the kernel argument says what the image says rather than what whoever -/// assembled it meant to stamp. +/// The name the ROOT image `bytes` carries, read back out of its superblock: +/// what the image says rather than what whoever assembled it meant to stamp. pub fn root_uuid_of(bytes: &[u8]) -> FsUuid { superblock_of(bytes).uuid } @@ -190,28 +189,45 @@ pub struct SecondSlot { pub root_bytes: u64, } -/// The sections of one image, as a slot carries them and an update sends them. -struct Sections { - kernel: Vec, +/// What one signed image is made of but its boot parameter: the kernel, the +/// two programs the kernel starts the machine with — the supervisor, and the +/// file server that serves ROOT to it — and ROOT, which carries neither +/// program (`toyos_update::image`). +#[derive(Clone, Copy)] +pub struct Release<'a> { + pub kernel: &'a [u8], + pub supervisor: &'a [u8], + pub fileserver: &'a [u8], + pub root: &'a [u8], +} + +/// The signed header of one image and the boot parameter it names. +struct Signed { cmdline: Vec, signed: [u8; toyos_update::image::SIGNED_BYTES], } -fn sections(kernel: &[u8], root: &[u8], params: &str, signing: Signing<'_>) -> Sections { - let cmdline = cmdline_with_root(root_uuid_of(root), params).into_bytes(); - let header = toyos_update::image::Header::of(signing.version, kernel, &cmdline, root); - Sections { kernel: kernel.to_vec(), signed: signing.key.sign(&header), cmdline } +fn signed(release: Release<'_>, params: &str, signing: Signing<'_>) -> Signed { + let cmdline = params.as_bytes().to_vec(); + let header = toyos_update::image::Header::of(signing.version, §ions(release, &cmdline)); + Signed { signed: signing.key.sign(&header), cmdline } +} + +/// `release` and `cmdline` as the signed header names them. +fn sections<'a>(release: Release<'a>, cmdline: &'a [u8]) -> toyos_update::image::Sections<'a> { + let Release { kernel, supervisor, fileserver, root } = release; + toyos_update::image::Sections { kernel, cmdline, supervisor, fileserver, root } } /// The image `ssh update` takes on its standard input: the signed -/// header, then the kernel, the boot parameter and ROOT, in the order the -/// header names them (`toyos_update::image`). -pub fn update_image(kernel: &[u8], root: &[u8], params: &str, signing: Signing<'_>) -> Vec { - let s = sections(kernel, root, params, signing); +/// header, then every section in the order the header names them +/// (`toyos_update::image`), ROOT last. +pub fn update_image(release: Release<'_>, params: &str, signing: Signing<'_>) -> Vec { + let s = signed(release, params, signing); let mut out = s.signed.to_vec(); - out.extend_from_slice(&s.kernel); - out.extend_from_slice(&s.cmdline); - out.extend_from_slice(root); + for section in sections(release, &s.cmdline).all() { + out.extend_from_slice(section); + } out } @@ -224,9 +240,8 @@ pub fn update_image(kernel: &[u8], root: &[u8], params: &str, signing: Signing<' /// slot A's FAT and ROOT, marked, and slot B's where `second` asks for one. pub fn create_boot_image( arch: Arch, - kernel_bytes: &[u8], bl_bytes: &[u8], - root_bytes: &[u8], + release: Release<'_>, params: &str, signing: Signing<'_>, second: Option, @@ -240,11 +255,12 @@ pub fn create_boot_image( let a = (crate::gptwrite::random_guid(), crate::gptwrite::random_guid()); let b = second.map(|room| (crate::gptwrite::random_guid(), crate::gptwrite::random_guid(), room)); - let s = sections(kernel_bytes, root_bytes, params, signing); - // Room for a kernel twice this one's size beside it, so an update can - // write a new kernel into a slot's volume before the old one is gone. - let slot_bytes = round_up_sectors(((kernel_bytes.len() * 2 + ESP_FREE_BYTES) * 64 / 63).max(FAT32_MIN_BYTES)); - let a_boot = create_slot_volume(Some(&s), slot_bytes); + let s = signed(release, params, signing); + // Room for the slot's files twice over, so an update can write new ones + // into a slot's volume before the old ones are gone. + let files = release.kernel.len() + release.supervisor.len() + release.fileserver.len(); + let slot_bytes = round_up_sectors(((files * 2 + ESP_FREE_BYTES) * 64 / 63).max(FAT32_MIN_BYTES)); + let a_boot = create_slot_volume(Some(SlotFiles { release, cmdline: &s.cmdline, signed: &s.signed }), slot_bytes); let slot = |(boot, root): (Guid, Guid), version| toyos_update::slots::Slot { boot: boot.0, root: root.0, version }; let table = toyos_update::slots::Table { sequence: 1, @@ -264,7 +280,7 @@ pub fn create_boot_image( // Finder, in Windows and in Linux on plug-in, with nothing configured. Part::full("log partition", "ToyOS log", Guid::MICROSOFT_BASIC, log_guid, create_log_volume(), Some(Volume::Fat32)), Part::full("slot A's volume", "ToyOS slot A", Guid::TOYOS_BOOT, a.0, a_boot, Some(Volume::Fat32)), - Part::full("slot A's ROOT", "ToyOS root A", Guid::TOYOS_ROOT, a.1, root_bytes.to_vec(), Some(Volume::Root)), + Part::full("slot A's ROOT", "ToyOS root A", Guid::TOYOS_ROOT, a.1, release.root.to_vec(), Some(Volume::Root)), ]; if let Some((boot, root, room)) = b { parts.push(Part::full("slot B's volume", "ToyOS slot B", Guid::TOYOS_BOOT, boot, create_slot_volume(None, slot_bytes), Some(Volume::Fat32))); @@ -274,23 +290,10 @@ pub fn create_boot_image( create_gpt_disk(&parts) } -/// The boot parameter as a slot carries it: ROOT's name, then `params`. -fn cmdline_with_root(root: FsUuid, params: &str) -> String { - if params.is_empty() { - format!("root={root}") - } else { - format!("root={root},{params}") - } -} - /// The actuator list an image is armed with, read back off the image. -/// -/// `root=` is not an actuator and is on every image: this answers what a boot -/// would *arm*, which is the other reading of the same string -/// (`toyos_abi::boot::actuators`). pub fn params_of(path: &Path) -> Result, String> { let text = cmdline_of(path)?; - Ok(toyos_abi::boot::actuators(&text).map(str::to_string).collect()) + Ok(toyos_abi::boot::tokens(&text).map(str::to_string).collect()) } /// The whole boot parameter the image's marked slot carries, read back off @@ -628,22 +631,32 @@ fn create_esp_volume(arch: Arch, bootloader: &[u8], log_guid: Guid) -> Vec { volume } -/// A slot's FAT volume of `bytes`: the kernel, its boot parameter and the -/// signed header naming both and ROOT (`toyos_update::slots`), or nothing for -/// a slot no image has been installed in yet. +/// What a slot's volume carries of one image. +struct SlotFiles<'a> { + release: Release<'a>, + cmdline: &'a [u8], + signed: &'a [u8], +} + +/// A slot's FAT volume of `bytes`: the kernel, its boot parameter, the two +/// boot programs and the signed header naming them and ROOT +/// (`toyos_update::slots`), or nothing for a slot no image has been installed +/// in yet. /// -/// The parameter is ROOT's name and then the actuators this boot arms, -/// comma-separated: handed to the kernel in `KernelArgs`, because the earliest -/// actuator fires before `mm::init` and there is nowhere later to fetch it -/// from. [`params_of`] is how the host asks a finished image which of them a -/// guest booting it would arm. -fn create_slot_volume(image: Option<&Sections>, bytes: usize) -> Vec { +/// The parameter is the actuators this boot arms, comma-separated: handed to +/// the kernel in `KernelArgs`, because the earliest actuator fires before +/// `mm::init` and there is nowhere later to fetch it from. [`params_of`] is +/// how the host asks a finished image which of them a guest booting it would +/// arm. +fn create_slot_volume(image: Option>, bytes: usize) -> Vec { let mut volume = format_fat32(bytes, "TOYOS-SLOT"); let files: Vec<(&str, &[u8])> = match image { Some(s) => vec![ - (toyos_update::slots::KERNEL_FILE, &s.kernel), - (toyos_update::slots::CMDLINE_FILE, &s.cmdline), - (toyos_update::slots::SIGNED_FILE, &s.signed), + (toyos_update::slots::KERNEL_FILE, s.release.kernel), + (toyos_update::slots::CMDLINE_FILE, s.cmdline), + (toyos_update::slots::SUPERVISOR_FILE, s.release.supervisor), + (toyos_update::slots::FILESERVER_FILE, s.release.fileserver), + (toyos_update::slots::SIGNED_FILE, s.signed), ], None => Vec::new(), }; @@ -962,7 +975,17 @@ mod tests { /// A ROOT image with one file in it, for the tests that need a real one /// rather than a placeholder: the assembler reads its superblock. fn tiny_root() -> Vec { - create_root_image(&[("bin/supervisor".to_string(), b"supervisor".to_vec())], &[], true) + create_root_image(&[("etc/os-release".to_string(), b"release".to_vec())], &[], true) + } + + /// Placeholder programs: what these tests judge is where each goes. + fn release<'a>(kernel: &'a [u8], root: &'a [u8]) -> Release<'a> { + Release { kernel, supervisor: b"\x7fELF supervisor", fileserver: b"\x7fELF fileserver", root } + } + + /// What a slot's volume carries of `release`. + fn slot_files<'a>(release: Release<'a>, s: &'a Signed) -> SlotFiles<'a> { + SlotFiles { release, cmdline: &s.cmdline, signed: &s.signed } } /// A fixed key: what these tests judge is the image, not whose it is. @@ -1019,10 +1042,11 @@ mod tests { #[test] fn the_volumes_this_build_writes_break_no_format_rule() { let key = key(); - let s = sections(b"kernel", &tiny_root(), "", signing(&key)); + let root = tiny_root(); + let s = signed(release(b"kernel", &root), "", signing(&key)); for (what, volume) in [ ("ESP", create_esp_volume(Arch::X86_64, b"bootloader", crate::gptwrite::random_guid())), - ("slot volume", create_slot_volume(Some(&s), FAT32_MIN_BYTES)), + ("slot volume", create_slot_volume(Some(slot_files(release(b"kernel", &root), &s)), FAT32_MIN_BYTES)), ("empty slot volume", create_slot_volume(None, FAT32_MIN_BYTES)), ("log volume", create_log_volume()), ] { @@ -1047,7 +1071,7 @@ mod tests { fn a_damaged_image_is_refused_by_the_reader_that_caught_it() { let root_image = tiny_root(); let key = key(); - let disk = create_boot_image(Arch::X86_64, b"kernel", b"bootloader", &root_image, "", signing(&key), None); + let disk = create_boot_image(Arch::X86_64, b"bootloader", release(b"kernel", &root_image), "", signing(&key), None); let log = only(&disk, toyos_gpt::Guid::MICROSOFT_BASIC); let root = root_partition_guid_of(&disk); let parts = @@ -1112,27 +1136,35 @@ mod tests { ["EFI/BOOT/BOOTx64.EFI", "toyos/log.guid"] ); let key = key(); - let s = sections(b"kernel", &tiny_root(), "", signing(&key)); + let root = tiny_root(); + let s = signed(release(b"kernel", &root), "", signing(&key)); assert_eq!( - files_of(create_slot_volume(Some(&s), FAT32_MIN_BYTES)), - [toyos_update::slots::CMDLINE_FILE, toyos_update::slots::SIGNED_FILE, toyos_update::slots::KERNEL_FILE] + files_of(create_slot_volume(Some(slot_files(release(b"kernel", &root), &s)), FAT32_MIN_BYTES)), + [ + toyos_update::slots::CMDLINE_FILE, + toyos_update::slots::FILESERVER_FILE, + toyos_update::slots::SIGNED_FILE, + toyos_update::slots::KERNEL_FILE, + toyos_update::slots::SUPERVISOR_FILE, + ] ); assert!(files_of(create_slot_volume(None, FAT32_MIN_BYTES)).is_empty()); } /// **The host's own reading of what the loader checks**: the marked slot's /// signed header verifies under the key the image was signed with and names - /// the kernel, the boot parameter and ROOT the image carries, byte for - /// byte; the table names the slot's partitions; and an update image for the - /// same parts splits into the same header and sections. A second slot is - /// present, empty, and not marked. + /// the kernel, the boot parameter, the two programs and ROOT the image + /// carries, byte for byte, and ROOT carries neither program; the table names + /// the slot's partitions; and an update image for the same parts splits into + /// the same header and sections. A second slot is present, empty, and not + /// marked. #[test] fn an_image_s_marked_slot_is_signed_over_exactly_what_it_carries() { let key = key(); let root = tiny_root(); let dir = toyos_tmpdir::TempDir::new("image-slot"); let path = dir.join("slotted.img"); - let disk = create_boot_image(Arch::X86_64, b"\x7fELF kernel", b"bootloader", &root, "sched-fast-health", signing(&key), Some(SecondSlot { root_bytes: 8 << 20 })); + let disk = create_boot_image(Arch::X86_64, b"bootloader", release(b"\x7fELF kernel", &root), "sched-fast-health", signing(&key), Some(SecondSlot { root_bytes: 8 << 20 })); std::fs::write(&path, &disk).expect("write the image"); let mut file = std::fs::File::open(&path).expect("open the image"); let table = slot_table_of(&mut file).expect("the slot table"); @@ -1150,18 +1182,24 @@ mod tests { toyos_update::sig::verify(&key.public(), &bytes, &toyos_update::image::signature_of(&signed)).expect("the signature"); let kernel = read_file_on(&mut file, a.boot, toyos_update::slots::KERNEL_FILE).expect("the kernel"); let cmdline = read_file_on(&mut file, a.boot, toyos_update::slots::CMDLINE_FILE).expect("the cmdline"); + let supervisor = read_file_on(&mut file, a.boot, toyos_update::slots::SUPERVISOR_FILE).expect("the supervisor"); + let fileserver = read_file_on(&mut file, a.boot, toyos_update::slots::FILESERVER_FILE).expect("the file server"); let (at, _) = partition_extent(&mut file, a.root).expect("slot A's ROOT"); let on_disk = &disk[at as usize..at as usize + header.root().len as usize]; assert_eq!(kernel, b"\x7fELF kernel"); + assert_eq!(cmdline, b"sched-fast-health", "the boot parameter is the image's parameters and names no filesystem"); + let placeholder = release(b"", b""); + assert_eq!((&supervisor[..], &fileserver[..]), (placeholder.supervisor, placeholder.fileserver)); assert_eq!(on_disk, &root[..]); - assert_eq!(header, toyos_update::image::Header::of(7, &kernel, &cmdline, on_disk)); + let carried = toyos_update::image::Sections { kernel: &kernel, cmdline: &cmdline, supervisor: &supervisor, fileserver: &fileserver, root: on_disk }; + assert_eq!(header, toyos_update::image::Header::of(7, &carried)); drop(file); drop(dir); - let update = update_image(b"\x7fELF kernel", &root, "sched-fast-health", signing(&key)); + let update = update_image(release(b"\x7fELF kernel", &root), "sched-fast-health", signing(&key)); let parts = toyos_update::image::Parts::split(&update).expect("an update image splits"); assert_eq!(parts.header, header); - assert_eq!(toyos_update::image::Header::of(7, parts.kernel, parts.cmdline, parts.root), header); + assert_eq!(parts.sections, carried); assert_eq!(parts.signed, &signed); } @@ -1198,7 +1236,7 @@ mod tests { fn an_install_makes_its_disk_and_refuses_one_that_exists() { const DATA: u64 = 4 << 20; let key = key(); - let image = create_boot_image(Arch::X86_64, b"kernel", b"bootloader", &tiny_root(), "", signing(&key), None); + let image = create_boot_image(Arch::X86_64, b"bootloader", release(b"kernel", &tiny_root()), "", signing(&key), None); let dir = toyos_tmpdir::TempDir::new("image-install"); let path = dir.join("disk.img"); @@ -1226,16 +1264,14 @@ mod tests { assert_eq!(std::fs::read(&theirs).expect("read it back"), b"somebody's"); } - /// **One ordering of one set is one image.** The judge below compares - /// `root=` against the superblock the same build stamped, so it is blind to - /// this: a `root_uuid` returning a constant satisfies it. This is the arm - /// that is not — the same files handed over backwards have to come out as - /// one UUID and one byte string, or two builds of one tree are two images - /// and the kernel argument names a filesystem the next build has not got. + /// **One ordering of one set is one image.** The same files handed over + /// backwards have to come out as one UUID and one byte string, or two + /// builds of one tree are two images, and the name the file server says + /// ROOT by names a filesystem the next build has not got. #[test] fn one_ordering_of_one_set_is_one_image() { let files: Vec<(String, Vec)> = vec![ - ("bin/supervisor".to_string(), b"supervisor-binary".to_vec()), + ("bin/shell".to_string(), b"shell-binary".to_vec()), ("bin/toybox".to_string(), (0..40_000u32).map(|i| (i ^ 0x5A) as u8).collect()), ("etc/system.manifest".to_string(), b"[start]\nsupervisor\n".to_vec()), ("share/empty".to_string(), Vec::new()), @@ -1279,18 +1315,16 @@ mod tests { /// **The independent oracle for ROOT**, asked of the finished image by /// readers that did not write it: `toyos-gpt` finds the partition by type - /// where the table placed it, `bcachefs`'s mount-and-read path lists - /// what its format-and-write path put there, and `toyos-fat32` reads the - /// boot parameter off the ESP. + /// where the table placed it, and `bcachefs`'s mount-and-read path lists + /// what its format-and-write path put there. /// /// Every name, size, content hash and symlink target against the list - /// handed to [`create_root_image`] — a listing that merely parsed would - /// pass a far weaker claim — and `root=` against the superblock UUID the - /// reader found, which is the whole of how a boot picks its ROOT. + /// handed to [`create_root_image`]: a listing that merely parsed would + /// pass a far weaker claim. #[test] fn the_root_partition_reads_back_as_the_files_the_build_put_in_it() { let files: Vec<(String, Vec)> = vec![ - ("bin/supervisor".to_string(), b"supervisor-binary".to_vec()), + ("bin/shell".to_string(), b"shell-binary".to_vec()), // Multi-block, so an extent list that stopped after the first is // caught by the hash rather than by the size. ("bin/toybox".to_string(), (0..40_000u32).map(|i| (i ^ 0x5A) as u8).collect()), @@ -1301,7 +1335,7 @@ mod tests { let root_image = create_root_image(&files, &symlinks, true); let key = key(); - let disk = create_boot_image(Arch::X86_64, b"kernel", b"bootloader", &root_image, "", signing(&key), None); + let disk = create_boot_image(Arch::X86_64, b"bootloader", release(b"kernel", &root_image), "", signing(&key), None); // Located by *type*, through the parser the kernel uses, at the offset // the table gives — never at the one the writer computed. @@ -1341,32 +1375,15 @@ mod tests { .unwrap_or_else(|e| panic!("read_link {name}: {e:?}")); assert_eq!(read.as_deref(), Some(target.as_str())); } - - // And the kernel argument names *this* filesystem: the parameter comes - // off the ESP through the FAT driver, the UUID out of the superblock - // the mount above read. - let scratch = toyos_tmpdir::TempDir::new("root-oracle"); - let path = scratch.join("root-oracle.img"); - std::fs::write(&path, &disk).expect("stage the image"); - let cmdline = cmdline_of(&path).expect("the ESP carries a boot parameter"); - assert_eq!( - toyos_abi::boot::root_uuid(&cmdline), - Some(fs.uuid().to_string().as_str()), - "the boot parameter {cmdline:?} does not name the ROOT this image carries" - ); } - /// **The independent oracle for the hierarchy.** ROOT mounts at `/system`, so - /// every absolute path the shipped image declares is `/system/` plus a name - /// ROOT carries, the two the kernel opens by hard-coded path included. Asked - /// of a finished image by readers that did not write it: `toyos-gpt` finds - /// ROOT by type where the table says, `bcachefs` mounts it, and - /// `toyos-manifest` parses the records out of the volume. - /// - /// `SUPERVISOR_PATH` comes from the kernel source, and that is a **text scan of - /// one spelling** — the `pub const SUPERVISOR_PATH: &str = "…";` line — so a - /// kernel computing its spawn path otherwise is walked past. Not finding - /// the line fails here rather than skipping. + /// **The independent oracle for the hierarchy.** ROOT is served at + /// `/system`, so every absolute path the shipped image declares is + /// `/system/` plus a name ROOT carries; the file server's row names no + /// path, its image being the slot's. Asked of a + /// finished image by readers that did not write it: `toyos-gpt` finds ROOT + /// by type where the table says, `bcachefs` mounts it, and `toyos-manifest` + /// parses the records out of the volume. #[test] fn every_declared_path_resolves_inside_the_root_partition() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); @@ -1374,26 +1391,21 @@ mod tests { let declared = toyos_manifest::parse( std::str::from_utf8(&manifest).expect("the manifest this build renders is text"), ); + let on_root: Vec<&str> = declared.programs.iter().filter_map(|p| p.source.path()).collect(); + assert_eq!( + declared.programs.len() - on_root.len(), + 1, + "the shipped image declares one program run from a boot image, the file server" + ); // Placeholders: what this judges is the path scheme, and the bytes of a // real binary say nothing about it. - let mut files: Vec<(String, Vec)> = declared - .programs - .iter() - .map(|p| (under_system(&p.path).to_string(), b"placeholder".to_vec())) - .collect(); - files.push(("bin/supervisor".to_string(), b"supervisor".to_vec())); + let mut files: Vec<(String, Vec)> = + on_root.iter().map(|path| (under_system(path).to_string(), b"placeholder".to_vec())).collect(); files.push((toyos_manifest::PATH.to_string(), manifest.clone())); - let disk = create_boot_image( - Arch::X86_64, - b"kernel", - b"bootloader", - &create_root_image(&files, &symlinks, true), - "", - signing(&key()), - None, - ); + let root_image = create_root_image(&files, &symlinks, true); + let disk = create_boot_image(Arch::X86_64, b"bootloader", release(b"kernel", &root_image), "", signing(&key()), None); let part = only_partition(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT) .expect("a boot image carries exactly one TOYOS-ROOT partition"); @@ -1405,45 +1417,31 @@ mod tests { .expect("the ROOT partition mounts"); // As the volume holds it, not as the renderer returned it. - let on_root = fs + let on_volume = fs .read_file(toyos_manifest::PATH) .expect("ROOT carries the manifest where the guest path names it"); - assert_eq!(on_root, manifest, "the manifest on ROOT is not the one the build rendered"); + assert_eq!(on_volume, manifest, "the manifest on ROOT is not the one the build rendered"); assert_eq!( toyos_manifest::GUEST_PATH, format!("/system/{}", toyos_manifest::PATH), "the path a process opens is not where ROOT carries the manifest" ); - let supervisor = supervisor_path_of_the_kernel(root); let names: std::collections::BTreeSet = fs .list(usize::MAX, &|_| true) .expect("list the ROOT partition") .into_iter() .map(|(name, _)| name) .collect(); - for path in std::iter::once(&supervisor).chain(declared.programs.iter().map(|p| &p.path)) { + for &path in &on_root { let name = under_system(path); - assert_ne!(name, path.as_str(), "{path} is not under the mount ROOT gets"); + assert_ne!(name, path, "{path} is not under the mount ROOT gets"); assert!(names.contains(name), "ROOT has no {name} for the declared path {path}"); } - assert!(names.contains("bin/supervisor"), "ROOT carries {names:?} and no bin/supervisor"); } /// A declared absolute path as ROOT carries it; unchanged off that mount. fn under_system(path: &str) -> &str { path.strip_prefix("/system/").unwrap_or(path) } - - /// The literal in `kernel/src/loader/mod.rs`'s `SUPERVISOR_PATH`. - fn supervisor_path_of_the_kernel(root: &Path) -> String { - const ITEM: &str = "pub const SUPERVISOR_PATH: &str = \""; - let source = std::fs::read_to_string(root.join("kernel/src/loader/mod.rs")) - .expect("kernel/src/loader/mod.rs"); - source - .lines() - .find_map(|line| line.trim_start().strip_prefix(ITEM)?.split('"').next()) - .expect("no `pub const SUPERVISOR_PATH: &str = \"…\";` in kernel/src/loader/mod.rs") - .to_string() - } } diff --git a/build/signing.rs b/build/signing.rs index 0fd2b9c4426..91a9316fd27 100644 --- a/build/signing.rs +++ b/build/signing.rs @@ -331,7 +331,10 @@ mod tests { let key = Key::mint(); let text = openssh_text(&key.seed); assert_eq!(toyos_ssh::hostkey::openssh_seed(&text), Ok(key.seed)); - let header = Header::of(3, b"k", b"c", &[0; 4096]); + let header = Header::of( + 3, + &toyos_update::image::Sections { kernel: b"k", cmdline: b"c", supervisor: b"s", fileserver: b"f", root: &[0; 4096] }, + ); let signed = key.sign(&header); let sig: [u8; 64] = signed[HEADER_BYTES..].try_into().unwrap(); let bytes: [u8; HEADER_BYTES] = signed[..HEADER_BYTES].try_into().unwrap(); diff --git a/images/diag.toml b/images/diag.toml index 05f9a36a350..77c5c795205 100644 --- a/images/diag.toml +++ b/images/diag.toml @@ -7,7 +7,7 @@ # claimed it tens of milliseconds after the last checkpoint, so every line the # kernel logged was on screen for about a tenth of a second and then gone. # -# `/system/bin/supervisor` is in every image and holds the one `SysCap`, so the +# The supervisor is in every image and holds the one `SysCap`, so the # guarantee is that this config declares no `devices`, which # `no_diag_program_claims_the_screen` checks. [boot] diff --git a/images/system.toml b/images/system.toml index 9b192900b00..7936a240b19 100644 --- a/images/system.toml +++ b/images/system.toml @@ -11,7 +11,7 @@ # day delete it. assets = ["assets"] -# What `/system/bin/supervisor` starts at boot. Program keys, not paths, and the order means +# What the supervisor starts at boot. Program keys, not paths, and the order means # nothing: every port exists before any server runs. [boot] # `filepicker` is a `serves` daemon like the three beside it: a machine-wide diff --git a/issues/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md b/issues/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md index c83910d8784..1b292cc6f84 100644 --- a/issues/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md +++ b/issues/a-supervisor-panic-before-logkeeper-starts-reaches-no-console.md @@ -6,17 +6,23 @@ opened: 2026-10-04 # A supervisor panic before logkeeper starts reaches no console -`/system/bin/supervisor` makes its own lines records in its log ring first thing +The supervisor makes its own lines records in its log ring first thing (`Log::open` in `system/supervisor/src/main.rs`), and only `logkeeper` drains a ring. So a panic before `logkeeper` runs leaves its message in a -ring nobody reads. The refusals of a ROOT without -`/system/etc/os-release`, or without a readable `/system/etc/system.manifest`, -are such panics. +ring nobody reads. -Measured with ROOT carrying no os-release (`machine_shutdown`, x86-64, QEMU): -the console's last kernel record is `exit: supervisor pid=0 code=101`, the -panic's words appear nowhere, and the harness waits out its 63 s boot ceiling -for `===READY===`. It does not treat the supervisor's end as a death. +The bootstrap's own refusals are said on the console first +(`bootstrap_fails`): the SYSTEM file server not starting, `/system/etc/os-release` +or `/system/etc/system.manifest` not read, and an os-release the build did not +write. What is left: a manifest `toyos_manifest::parse` panics on (a build +whose two halves disagree), and every other panic between `Log::open` and +`logkeeper`'s start. + +Measured before `bootstrap_fails`, with ROOT carrying no os-release +(`machine_shutdown`, x86-64, QEMU): the console's last kernel record is +`exit: supervisor pid=0 code=101`, the panic's words appear nowhere, and the +harness waits out its 63 s boot ceiling for `===READY===`. It does not treat +the supervisor's end as a death. **Exit**: a supervisor panic before `logkeeper` serves puts its message on the console, and the harness ends a boot whose supervisor exited, naming the exit. diff --git a/issues/an-ended-detached-thread-is-reaped-only-at-its-processs-next-spawn.md b/issues/an-ended-detached-thread-is-reaped-only-at-its-processs-next-spawn.md new file mode 100644 index 00000000000..5d325bcb14a --- /dev/null +++ b/issues/an-ended-detached-thread-is-reaped-only-at-its-processs-next-spawn.md @@ -0,0 +1,42 @@ +--- +status: open +kind: defect +opened: 2026-10-22 +--- + +# An ended detached thread is reaped only at its process's next spawn + +The kernel releases a thread only when another thread joins it +(`SYS_THREAD_JOIN`). Nothing can be told that no thread ever will. A thread std +detaches can't free the stack it runs on, so as it ends it pushes its block +onto `REAPED` (`sdk/std/sys/thread.rs`). The next `Thread::new` in the same +process joins it, which frees its 2 MiB stack and its `MAX_THREADS` place. + +Until that spawn, the ended thread stays in its process's table. `ps` shows it +as `Z`, and its stack stays allocated. A process that detaches threads and then +never spawns another keeps all of them for its whole life: any program that +drops a `JoinHandle` and starts no thread after it. The cost is bounded by the threads detached since +the last spawn, not by the number of spawns. #987 made it bounded; before #987 +nothing reaped these threads at all. + +Measured on QEMU's x86-64 machine booting `tests/images/testcases.toml`, with +toybox `ps` after the boot: + +- At `ff9b4b1c5`, which has neither #987 nor a join, the supervisor's two + `bootstrap` reader threads, which the supervisor had detached, show `Z` for + the rest of the boot. +- At `25d787482`, which has #987 but not that commit's join (a scratch revert), + no `bootstrap` thread is listed. The supervisor's later thread spawns had + reaped them. + +The case this record is about, a process that never spawns again, was not +measured. It follows from `Thread::new` being the only reaper. + +*Exit:* the kernel releases an ended thread that nobody will join. That means +a detach call, or a flag at thread creation, so the thread's place goes when +it ends. std's detach then frees the stack at once: by the kernel unmapping it +as part of the thread's end, or by std freeing it from a thread that is still +alive. `REAPED` is deleted. libc's `pthread_detach` (`sdk/libc/src/pthread.rs`) +uses the same pattern and moves with it. + +Owner: the orchestrator. diff --git a/issues/every-program-sees-only-the-files-it-was-given.md b/issues/every-program-sees-only-the-files-it-was-given.md index fa324aa2906..323a29cb359 100644 --- a/issues/every-program-sees-only-the-files-it-was-given.md +++ b/issues/every-program-sees-only-the-files-it-was-given.md @@ -98,9 +98,10 @@ nameable by every program and make confused-deputy bugs structural. access that the file server enforces on every request that would change what it holds, and a package launched from `/apps` is minted its own directory read-only and its own folder of the session's home read-write, - and nothing else a file server serves (`toyos_manifest::Program::view`). - It still reaches `/tmp` and `/system`, which the kernel serves to every - program. Every row the image declares still sees the whole tree + `/system` read-only, and nothing else a file server serves + (`toyos_manifest::Program::view`). It still reaches `/tmp`, which the + kernel serves to every program. Every row the image declares still sees + the whole tree, `/system` read-only and the rest read-write, `/apps` included, so a shell and everything it starts can rewrite an installed package; whether the installer alone writes `/apps` is `issues/whether-pkg-alone-writes-apps.md`. diff --git a/issues/libc-answers-einval-for-every-error-it-does-not-name.md b/issues/libc-answers-einval-for-every-error-it-does-not-name.md index d4290ff0f26..eb018bfe1b4 100644 --- a/issues/libc-answers-einval-for-every-error-it-does-not-name.md +++ b/issues/libc-answers-einval-for-every-error-it-does-not-name.md @@ -14,9 +14,7 @@ sends `Unknown`, `BadAddress`, `ResourceExhausted` and `NotSupported` through full (`fd_errno`, `sdk/libc/src/posix_io.rs`), but a full kernel handle table (`kernel/src/object/handle.rs`) refuses the `open` or `pipe` beneath it first with `ResourceExhausted`, which reaches the caller as `EINVAL` where POSIX has -`EMFILE`; a full -bcachefs volume (`kernel/src/bcachefs_adapter.rs`) `write` `EINVAL` where it -has `ENOSPC`, and an operation a filesystem lacks `EINVAL` where it has +`EMFILE`; and an operation a filesystem lacks `EINVAL` where it has `ENOTSUP`. A refusal that is truly `EINVAL` reads the same as each of them, so a case asserting `EINVAL` passes whichever happened. diff --git a/issues/root-in-memory-costs-its-whole-size.md b/issues/root-in-memory-costs-its-whole-size.md index d7ce767fa31..7c767fefa18 100644 --- a/issues/root-in-memory-costs-its-whole-size.md +++ b/issues/root-in-memory-costs-its-whole-size.md @@ -8,7 +8,8 @@ opened: 2026-09-25 The loader reads ROOT's filesystem whole, the extent its superblock states (`loader/src/rootimage.rs`), and the kernel keeps it for the machine's -life. `Superblock::check` holds that extent equal to the partition, and +life, read-only, for the supervisor's SYSTEM file server to serve +(`kernel/src/bootimage.rs`, `system/fileserver/src/root.rs`). `Superblock::check` holds that extent equal to the partition, and `build/image.rs` builds the volume at the blocks its contents use, rounded up to the 1 MiB partition alignment, so what is read and kept is what ROOT carries. The cost is the payload. @@ -21,10 +22,41 @@ in turn, each carrying at most `SHARED_BOOT_BYTES` (64 MiB) of test binaries; carries `tests/images/testcases.toml`'s own programs, 13 MiB (3328 blocks), where every test boot carried the whole 619 MiB catalogue before. -**Every resident `/system` page is in RAM twice.** `ReadOnlyBacking::read_page` -(`kernel/src/file_backing.rs`) copies each page a program touches out of the -image into a file-cache page, so the image's copy and the cache's are both -held. +**A running `/system` program's pages are in RAM three times.** Its spawn +reads the whole binary through SYSTEM into a memory object of the spawner's +(`toyos::process::Image`, rounded up to 2 MiB), which lives as long as the +child pages from it, and `SharedImage::read_page` +(`kernel/src/file_backing.rs`) copies each page the child touches out of that +object: ROOT's copy, the image's and the page's are all held. Before ROOT +left the kernel, the kernel paged a program from ROOT and the image's copy was +not there. Measured on QEMU's x86-64 machine booting +`tests/images/testcases.toml`, toybox's `free` after the boot: 330 MiB in use +at `b9e4c5862`, 384 MiB once ROOT left the kernel, one boot each, the same +session; the 54 MiB are the running services' images, each rounded up to +2 MiB, and the SYSTEM file server. + +**Per held child, the image is 2 MiB of a 16 MiB cost, and it is what puts +255 children past a 4 GiB guest.** `libc_children`'s `spawn_children_cap` +holds 255 unwaited children of one `/system` program. Measured on QEMU's x86-64 +machine, `tests/images/libccase.toml`, 4 GiB, toybox's `free` every 50 held +children, one boot each in the same session (a scratch patch to the case, not +committed): + +| held | used at `6ae79ccaa` (kernel pages from ROOT) | used at `25d787482` (spawn reads an image) | +|---|---|---| +| 0 | 126 MiB | 178 MiB | +| 50 | 836 MiB | 988 MiB | +| 100 | 1546 MiB | 1798 MiB | +| 150 | 2254 MiB | 2606 MiB | +| 200 | 2964 MiB | 3416 MiB | +| 250 | 3674 MiB | refused at 241 (`ENOMEM`) | + +That is 14.2 MiB a child at the base and 16.2 MiB with the image, the +difference one image rounded up to 2 MiB. Each child's exit record says +`peak=2MB allocs=2` in both: `peak` counts what a process allocated, and neither +its image nor what the kernel holds for it. So the image costs the case its +last 14 children, and the 14.2 MiB beside it are 2 MiB pages, which +`process-memory-is-2-mib-pages-and-that-caps-the-process-count.md` removes. **On the dev host the fast tier's peak host memory is now below `main`'s.** An alternating A/B of `cargo test` against `origin/main` at `b0adc600`, three arms @@ -46,6 +78,11 @@ complete K ms`; a test boot on QEMU reads 1390, 108 (14) and 344. Not measured: the T14. What that run owes is that line off the stick and off NVMe, against the base's `Boot: complete`. -Exit: either measured and accepted by the owner as the price, or the file -cache serving the image's own page rather than a copy, before the self-update's signature check makes the whole -read a fixed cost. +Exit: a program's pages mapped from ROOT's own read-only pages rather than +copied, before the self-update's signature check makes the whole read a fixed +cost. The orchestrator ruled the mechanism (2026-10-22): the memory design's +stages 2, 4 and 6, namely 4 KiB pages, so an image is a program's size and not +2 MiB; memory-object subranges; and immutable file objects shared across +spawns, so that SYSTEM's object for a ROOT file is a subrange of the boot +image. No dedicated call over part of a boot image is to be built. +`spawn_children_cap` holds all 255 children again once that has landed. diff --git a/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md b/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md index 4d9997039aa..44bad8b3e9f 100644 --- a/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md +++ b/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md @@ -22,7 +22,7 @@ still panics on any refusal. What answered `WouldBlock` was never found. The same run red `log_reserve_window` on a ROOT read that missed its 2000 ms budget in the kernel's usb-storage driver. That half is gone: the kernel reads -ROOT only from the image the loader read into memory (`kernel/src/rootfs.rs`) +no ROOT, which the supervisor's file server serves from the loader's image, and drives no USB (`13ed7b4f4`). **Both tests are deleted**, as flaky tests are: `aedcf17dc` took diff --git a/issues/storage-is-layers-and-a-role-is-a-filesystem.md b/issues/storage-is-layers-and-a-role-is-a-filesystem.md index 8d58c06fba6..b3d3f1c45bd 100644 --- a/issues/storage-is-layers-and-a-role-is-a-filesystem.md +++ b/issues/storage-is-layers-and-a-role-is-a-filesystem.md @@ -10,8 +10,10 @@ What the installed product's disks look like, from the block device up to a path. **Where each layer runs is the small-kernel track's** (`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, stages 3 and 4, owner ruling 2026-09-26): block services and file servers are -userland programs, the VFS is a client library, and the kernel keeps ROOT's -in-memory read path and nothing else of storage. The plan this track carried +userland programs and the VFS is a client library. The kernel keeps no +filesystem of ROOT: owner ruling 2026-10-21, which superseded the in-memory read +path the 2026-09-26 ruling kept, hands ROOT's bytes to the supervisor and its +file server serves them (Filesystem, below). The plan this track carried before that ruling — a kernel VFS forwarding to userland servers through a mount protocol — is superseded, and nothing here builds it. @@ -45,7 +47,7 @@ volume, handed to whichever server recognises its superblock. | partition | filesystem | mounts as | writable | |---|---|---|---| | ESP | FAT32, firmware's rule | `/boot` | kernel only | -| ROOT | bcachefs image the build writes | `/system` | no; versioned per release | +| ROOT | bcachefs image the build writes | `/system` | no; signed per release | | DATA | bcachefs, formatted on first boot | `/apps`, `/config`, `/home`, `/state` | yes | | LOG | FAT32 while a Mac has to read the dev stick | `/log` | yes | @@ -55,18 +57,20 @@ Roles come from partitions, so both shapes are the same code. ## Filesystem -**The kernel reads ROOT and nothing more.** The loader reads the selected -slot's ROOT into memory and the kernel mounts it read-only with bcachefs's read -half; no code that changes a disk is in the kernel. `/system` is immutable by -design: the installer and the updater write it offline, checkpoint it and mark -the journal clean; a kernel that finds a dirty journal under `/system` refuses -by name, because replay is a write-path job and a system image needing one is a -broken install. - -**Every other filesystem is a file server per role** — LOG, DATA, BOOT — over -its block service's sessions, linking the whole crate it serves: a server that -crashes loses its own role and nothing else, and no foreign parser runs in the -kernel on bytes another OS wrote. +**ROOT is opaque to the kernel.** The loader reads the selected slot's ROOT, +the supervisor and the file server into memory, each a section of the slot's +signed header, and the kernel keeps their pages read-only, starts the +supervisor from its image and hands it the other two. The supervisor starts the +file server's SYSTEM role from its image, serving ROOT read-only out of that +memory, before it reads a file. `/system` is immutable by design: the installer +and the updater write it offline, checkpoint it and mark the journal clean; a +server that finds a dirty journal under `/system` refuses by name, because +replay is a write-path job and a system image needing one is a broken install. + +**Every filesystem is a file server per role** — SYSTEM, LOG, DATA, BOOT — +SYSTEM over ROOT's bytes in memory and every other over its block service's +sessions, linking the whole crate it serves: a server that crashes loses its +own role and nothing else, and no filesystem parser runs in the kernel. **Multi-device is a filesystem property, not a block-layer subsystem.** Pooling across disks, replicas per subvolume, tiering with a fast foreground device, @@ -124,8 +128,9 @@ what is left here is built on them. 5. Multi-device, replicas, tiering, snapshots, as the bcachefs crate grows into them. 6. A full secure boot chain, the end state of `/system`'s immutability: - firmware verifies the bootloader, the bootloader the kernel, the kernel the - ROOT image it mounts, and a link that fails is refused by name. + firmware verifies the bootloader, the bootloader every section of the slot + it boots (built: the signed header binds the kernel, the two boot programs + and ROOT), and a link that fails is refused by name. 7. NTFS read-only, postponed here by the owner. LOG stays its own FAT32 partition through the dev phase, because a Mac has to diff --git a/issues/sys-debug-actions-and-two-loader-words-that-nothing-calls.md b/issues/sys-debug-actions-and-two-loader-words-that-nothing-calls.md index 5571eef96a9..6add4546a14 100644 --- a/issues/sys-debug-actions-and-two-loader-words-that-nothing-calls.md +++ b/issues/sys-debug-actions-and-two-loader-words-that-nothing-calls.md @@ -20,8 +20,7 @@ finds no caller of: `heap_ceiling`'s, for stage C's `heap_ceiling_bounds`; - `LOCK_ACROSS_SWITCH` (2), `test_panic_child`'s. Its stage C item is a type (`lock_across_switch_halts`), which calls no `SYS_DEBUG`, so no stage brings a caller back; -- `boot::WRITE_NO_LAYOUT_PARAM` and `boot::WITHHOLD_ROOT_PARAM`, for stage A's - `kernel_args_layout_refused` and `root_withheld_refused`. +- `boot::WRITE_NO_LAYOUT_PARAM`, for stage A's `kernel_args_layout_refused`. `LOG_PATTERNED` (21) has one caller, `panic_halts_first`, which wants a record and reads none of the pattern `kernel/src/log/storm.rs` writes into it: the checksum and payload had test-runner's diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 66cb1858668..f9f00eefeed 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -34,9 +34,11 @@ name is a `#[test]` in the named crate or a `METAL` row, and reverting the behav - host `broken_data_volume_is_absent`, `data_candidate_with_bad_geometry_is_absent`: a bad DATA candidate is absent, not formatted. - host `block_duplicate_id`: two disks with one identity are refused. -- metal `root_from_memory`: ROOT mounts from the loader's image with no storage command before init. -- metal `root_withheld_refused`: a handoff with no ROOT image is refused by name; brings back - `loader-withholds-root` in the loader as well as the kernel. +- metal `root_from_memory`: the supervisor's SYSTEM file server serves ROOT out of the loader's + image, with no storage command before the supervisor reads the manifest. +- metal `boot_image_withheld_refused`: a handoff whose supervisor, file server or ROOT image is + empty or outside the loader's memory is refused by name before `mm::init`; brings in a loader + word that withholds one, read by the loader and the kernel. - metal `kernel_args_layout_refused`: a loader of another `KernelArgs` layout is refused first; brings back `loader-writes-no-layout` in the loader as well as the kernel. - metal `boot_from_power_on`: the power-on spans are the loader's counts at the kernel's rate. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index b5642a446ad..d02d34e6ab2 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -19,7 +19,8 @@ kernel's part of a restart, and named module tests hold each program's 1. **Step 9's file half** (stage 4). libc is a file server's client through `toyos::files`, as std is. 2. **Step 8's exit** (stage 4). Its empty view needs step 9's file half: - until then the kernel serves `/system` and `/tmp` to every process. + until then the kernel serves `/tmp` to every process, and every view holds + `/system` read-only. 3. **P1 of the Wi-Fi track** (`issues/there-is-no-wifi.md`): a BAR stays where firmware put it, handed over in 4 KiB pages with its MSI-X table's page, under the owner's Q2 ruling. The orchestrator ruled on #900 that @@ -41,8 +42,9 @@ Beside that order, each waiting on something outside it: 1. **The system image is loaded into memory by the loader.** The UEFI loader reads the selected slot's ROOT through the firmware's own block I/O, checks its signature on the bytes in memory (the self-update's signing, so one - check covers both), and hands the kernel its address and length. The kernel - mounts ROOT from memory and needs no storage driver to boot. + check covers both), and hands the kernel its address and length. No storage + driver is needed to boot: the supervisor's file server serves ROOT out of + that memory (owner ruling 2026-10-21, step 9). **Exit**: the machine boots in QEMU and on the T14 with the kernel's NVMe and USB storage paths never touched before the supervisor runs. The boot-time cost on the stick and on NVMe is measured. @@ -83,8 +85,9 @@ Beside that order, each waiting on something outside it: file server per role (LOG, DATA, BOOT), so one crashing cannot take another down. A file-server crash is survivable, not invisible: data survives on disk, clients reopen, and the few states that cannot be recovered answer - `Gone`; invisible restart can come later. The kernel keeps ROOT's - in-memory read path and exec from it. USB storage stays in the kernel until + `Gone`; invisible restart can come later. The kernel keeps no filesystem + of ROOT (owner ruling 2026-10-21, step 9, which replaced this ruling's + in-memory read path and exec from it). USB storage stays in the kernel until stage 5 moves the whole xHCI out, because its one IOMMU domain is shared with the keyboard, and the panic console never depends on a userland USB program. Ctrl+Alt+D is removed (owner, 2026-10-10: "i dont want it. i want @@ -151,22 +154,28 @@ Beside that order, each waiting on something outside it: one port, and `fs_escape` is refused a symlink that climbs out, a `..` on the wire and a directory it was not given. **Left**: the rest of the escape suite, a rename racing a lookup among it, shown red on a - global-tree resolver; and the empty view, which names `/system` and - `/tmp` while the kernel serves them (step 9's file half). + global-tree resolver; and the empty view, which names `/tmp` while the + kernel serves it (step 9's file half). 9. **Delete the kernel storage stack.** **Partly done** (#536). Two halves are left, and they land apart: - - **The file half**: the VFS down to ROOT's resolver, tmpfs (`tmpfs.rs`, - `fs_rename.rs`), the page cache (`file_cache.rs`) and the file - syscalls. `/tmp` leaves the kernel per session, as the owner ruled - ("Per session", 2026-10-03, in the isolation track), and `/system` is - read over the file protocol, which std and libc both reach through - `toyos::files`. **Exit**: `tmpfs.rs`, - `fs_rename.rs` and `file_cache.rs` are gone, `vfs.rs` resolves ROOT - for exec and `SYS_DLOPEN` alone, and `sdk/abi` has no `SYS_OPEN` (9), + - **The file half**: the VFS, tmpfs (`tmpfs.rs`, `fs_rename.rs`), the + page cache (`file_cache.rs`) and the file syscalls. `/tmp` leaves the + kernel per session, as the owner ruled ("Per session", 2026-10-03, in + the isolation track). **`/system` is read over the file protocol**, + which std and libc both reach through `toyos::files` (owner ruling + 2026-10-21, "Removing the boot filesystem exception"): + the loader hands the kernel the supervisor's and the file server's + executables beside ROOT, each a section of the slot's signed header; + the kernel keeps their pages read-only, starts the supervisor from its + image and hands it the other two, and the supervisor starts the file + server's SYSTEM role from its image, serving ROOT out of that memory, + before it reads the manifest. The kernel's ROOT mount, its bcachefs + adapter and its spawn by `/system` path are deleted. **Exit**: + `tmpfs.rs`, `fs_rename.rs`, `file_cache.rs` and `vfs.rs` are gone, and + `sdk/abi` has no `SYS_OPEN` (9), `SYS_STAT` (16), `SYS_READDIR` (17), `SYS_DELETE` (18), `SYS_CHDIR` (20), `SYS_GETCWD` (21), `SYS_RENAME` (52), `SYS_MKDIR` (53), `SYS_RMDIR` (54), `SYS_SYMLINK` (81) or `SYS_READLINK` (82). - `SYS_DLOPEN` (55) is step 5's. - **The block half**, with step 10: the block layer (`block.rs`), GPT (`gpt.rs`), USB storage (`drivers/usb_storage.rs`), the refusal chain and `SYS_PARTITION_READ/WRITE`. **Exit**: `git grep -n -e diff --git a/issues/the-kernel-still-parses-what-userland-writes.md b/issues/the-kernel-still-parses-what-userland-writes.md index f95d7df37bf..b67e0425433 100644 --- a/issues/the-kernel-still-parses-what-userland-writes.md +++ b/issues/the-kernel-still-parses-what-userland-writes.md @@ -53,8 +53,9 @@ before that day. Independent of everything else; may run as soon as a slot frees **Move 2 — filesystem daemons**, sequenced after the completion architecture. A crafted image attacks the kernel rather than a sandboxed daemon. The FS daemon needs the blocking story to be efficient, and the boot path needs its story told -first: the kernel mounts ROOT itself, but what mounts `/boot` and `/home`, and -with what authority, is the open question. +first. Told (owner ruling 2026-10-21): the kernel parses no filesystem of +ROOT; the supervisor's file server serves it out of the memory the loader +verified, and every other role is a file server too. **Move 3 — the panic-time symbol resolver**, small and independent. Ruled (owner, 2026-10-03, `issues/toyos-explains-itself.md`): he chose diff --git a/issues/the-loader-parses-roots-filesystem-to-find-microcode.md b/issues/the-loader-parses-roots-filesystem-to-find-microcode.md new file mode 100644 index 00000000000..06c723e9d69 --- /dev/null +++ b/issues/the-loader-parses-roots-filesystem-to-find-microcode.md @@ -0,0 +1,24 @@ +--- +status: open +kind: finding +opened: 2026-10-22 +--- + +# The loader parses ROOT's filesystem to find the microcode patch + +The kernel parses no filesystem of ROOT: ROOT is an opaque signed section the +supervisor's file server serves. The loader still does. `loader/src/microcode.rs` +mounts ROOT read-only with the `bcachefs` crate after the slot's header has +verified it, and reads the patch `toyos_microcode` names for this CPU out of +`/system/share/microcode/`, so a filesystem reader of ROOT's whole format runs +in the loader on every x86-64 boot. It is outside the kernel scope of the +owner's 2026-10-21 ruling and was left as it is. + +Owner: the orchestrator. + +The candidate exit: the image's patches as a signed boot section of their own +beside the kernel and the two boot programs (`toyos_update::image`), which the +loader picks this CPU's patch out of without reading ROOT, so ROOT is opaque to +every boot stage. It has to be weighed against +`issues/a-new-microcode-patch-reaches-a-machine-only-in-a-new-image.md`, whose +exit delivers a new patch as a file on ROOT. diff --git a/issues/the-machine-updates-itself-without-ubuntu.md b/issues/the-machine-updates-itself-without-ubuntu.md index aebe610eb48..541ad817d37 100644 --- a/issues/the-machine-updates-itself-without-ubuntu.md +++ b/issues/the-machine-updates-itself-without-ubuntu.md @@ -15,11 +15,13 @@ stdin — and the machine installs nothing the owner did not sign. ## Stage 1 — slots, signatures and `update`, proven in QEMU (done) - **A signed image** (`toyos-update`): a header naming a monotonic version and - the SHA-256 of the kernel, its boot parameter and ROOT, signed with Ed25519 + the SHA-256 of the kernel, its boot parameter, the supervisor, the file server + and ROOT, each by role and length, signed with Ed25519 over OpenSSH's SSHSIG message in the `toyos-image` namespace — so `ssh-keygen -Y sign` signs one and `ssh-keygen -Y verify` checks ours. The signature covers hashes, never bytes. -- **Two slots**, each a FAT volume (kernel, parameter, signed header) and a +- **Two slots**, each a FAT volume (kernel, parameter, the two boot programs, + signed header) and a ROOT, named by a slot table (two copies, a sequence, the writer writes the copy that is not current). Every image carries slot A; an image built for a machine carries an empty slot B. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 214b26d1c58..0b5e894190d 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -499,7 +499,6 @@ toyos-abi = { path = "../sdk/abi" } # The binary's alone, so a host build of the library compiles none of them. [target.'cfg(target_os = "none")'.dependencies] -bcachefs = { path = "../lib/bcachefs", default-features = false } toyos-acpi = { path = "../lib/acpi" } toyos-amdvi = { path = "amdvi" } toyos-dma = { path = "dma" } diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index a6c2324be7c..c4c79a0b14c 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -218,7 +218,7 @@ static ARMED: [AtomicU64; ARM_WORDS] = [const { AtomicU64::new(0) }; ARM_WORDS]; #[cfg(feature = "boot-actuators")] pub fn init(cmdline: &str) { let mut armed = [0u64; ARM_WORDS]; - for token in toyos_abi::boot::actuators(cmdline).filter(|t| !crate::params::claims(t)) { + for token in toyos_abi::boot::tokens(cmdline).filter(|t| !crate::params::claims(t)) { arm(&mut armed, token); } for (name, implied) in IMPLIES { @@ -257,7 +257,7 @@ const fn at(index: usize) -> (usize, u64) { /// Refuses any actuator: a kernel with none must not boot looking like one that was given none. #[cfg(not(feature = "boot-actuators"))] pub fn init(cmdline: &str) { - let mut named = toyos_abi::boot::actuators(cmdline).filter(|t| !crate::params::claims(t)); + let mut named = toyos_abi::boot::tokens(cmdline).filter(|t| !crate::params::claims(t)); assert!( named.next().is_none(), "this kernel carries no actuators and was handed the boot parameter {cmdline:?}" diff --git a/kernel/src/bcachefs_adapter.rs b/kernel/src/bcachefs_adapter.rs deleted file mode 100644 index 48d1ce30170..00000000000 --- a/kernel/src/bcachefs_adapter.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! ROOT as the VFS reads it: the signed image, a read-only bcachefs volume in -//! memory. The only filesystem this kernel mounts; every writable one is a -//! file server's (`/system/bin/fileserver`). - -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::sync::Arc; -use alloc::vec::Vec; - -use bcachefs::{FsError, Mounted, ReadOnly}; -use crate::file_backing::{FileBacking, ReadOnlyBacking}; -use crate::file_cache::{self, FileId}; -use crate::rootfs::MemoryImage; -use toyos_abi::syscall::SyscallError; - -use crate::vfs::FileSystem; - -/// Exhaustive match: corruption maps to `Io`, never `NotFound` — a btree that won't decode isn't "not there". -fn as_syscall_error(err: &FsError) -> SyscallError { - match err { - FsError::NotFound => SyscallError::NotFound, - FsError::NoSpace { .. } | FsError::EntryTooLarge { .. } | FsError::ListTooLong { .. } => { - SyscallError::ResourceExhausted - } - FsError::NameTooLong { .. } => SyscallError::InvalidArgument, - FsError::DeviceRead(..) - | FsError::DeviceWrite(..) - | FsError::DeviceSync(_) - | FsError::BadMagic { .. } - | FsError::UnsupportedVersion(_) - | FsError::ChecksumMismatch { .. } - | FsError::CorruptedKey(_) - | FsError::CorruptedNode(_) - | FsError::BlockOffDevice { .. } - | FsError::NotEnoughBlocks { .. } - | FsError::TreeTooDeep(_) - | FsError::BadSuperblock { .. } - | FsError::NodeOverfull { .. } - | FsError::TargetTooLong { .. } => SyscallError::Io, - } -} - -/// Ceiling on the one allocation `read_link` materialises from a disk-declared -/// size: the kernel's heap ceiling, which `bcachefs` cannot know and the -/// volume bound does not reach. -const MAX_LINK_TARGET: u64 = crate::user_ptr::MAX_USER_STR; -const _: () = assert!(MAX_LINK_TARGET <= crate::mm::MAX_HEAP_ALLOC as u64); - -/// Logs the error's detail, then maps it to the `SyscallError` a caller can act on. -fn mapped(op: &str, name: &str, result: Result) -> Result { - result.map_err(|err| { - log!("bcachefs: {} of '{}' failed: {:?}", op, name, err); - as_syscall_error(&err) - }) -} - -/// The same, for a `bcachefs` call whose `Ok(None)` means the name is absent. -fn present(op: &str, name: &str, result: Result, FsError>) -> Result { - mapped(op, name, result)?.ok_or(SyscallError::NotFound) -} - - -/// What a name on ROOT is numbered by, once asked. -#[derive(Clone, Copy)] -enum Node { - /// A file: the `FileId` every open of it answers. - File(FileId), - /// A symlink: a number `file_cache::draw_node` drew, which no file holds. - Symlink(u64), -} - -/// VFS adapter for ROOT, a read-only bcachefs volume in memory. -/// -/// Every backing it hands out reads the image the mount was opened over, so a -/// file cannot be served off bytes its filesystem does not occupy. -pub struct ReadOnlyBcacheFsAdapter { - fs: Mounted, - /// Every name's node once asked. The volume never changes, so a name is - /// one entry for the boot. - nodes: BTreeMap, -} - -impl ReadOnlyBcacheFsAdapter { - pub fn new(fs: Mounted) -> Self { - Self { fs, nodes: BTreeMap::new() } - } - - /// The `FileId` of the file `name`, `size` bytes long, made with no - /// reference held when this is its first ask; `Io` for a name the volume - /// answered as a symlink before and as a file now, which only a corrupt - /// image can. - fn file_id(&mut self, name: &str, size: u64) -> Result { - match self.nodes.get(name) { - Some(&Node::File(file_id)) => return Ok(file_id), - Some(&Node::Symlink(_)) => { - log!("bcachefs: '{}' has extents and was a symlink", name); - return Err(SyscallError::Io); - } - None => {} - } - // Its mtime is the volume's (`file_mtime`), never the cache's. - let file_id = file_cache::create_file(true, 0); - file_cache::set_size(file_id, size); - file_cache::release(file_id); - self.nodes.insert(String::from(name), Node::File(file_id)); - Ok(file_id) - } -} - -impl FileSystem for ReadOnlyBcacheFsAdapter { - fn list(&mut self, dir: &str, limit: usize) -> Result, SyscallError> { - let listed = mapped("list", dir, self.fs.list(limit, &|name| crate::vfs::under_directory(name, dir)))?; - Ok(listed.into_iter().map(|(name, _)| name).collect()) - } - - fn node(&mut self, name: &str) -> Result { - match self.nodes.get(name) { - Some(&(Node::File(node) | Node::Symlink(node))) => return Ok(node), - None => {} - } - if mapped("is_symlink", name, self.fs.is_symlink(name))? { - let node = file_cache::draw_node(); - self.nodes.insert(String::from(name), Node::Symlink(node)); - return Ok(node); - } - let (_, size) = present("node", name, self.fs.file_extents(name))?; - self.file_id(name, size) - } - - fn is_dir(&mut self, dir: &str) -> Result { - mapped("is_dir", dir, self.fs.is_dir(dir)) - } - - fn file_mtime(&mut self, name: &str) -> Result { - present("file_mtime", name, self.fs.file_mtime(name)) - } - - fn read_link(&mut self, name: &str) -> Result, SyscallError> { - mapped("read_link", name, self.fs.read_link(name, MAX_LINK_TARGET)) - } - - fn open_file(&mut self, name: &str) -> Result<(FileId, Option>), SyscallError> { - let (extents, size) = present("open", name, self.fs.file_extents(name))?; - let file_id = self.file_id(name, size)?; - file_cache::open(file_id).commit(); - let backing = Arc::new(ReadOnlyBacking::new(*self.fs.io(), extents, size)); - Ok((file_id, Some(backing))) - } - - fn create(&mut self, _name: &str, _mtime: u64) -> Result { - Err(SyscallError::PermissionDenied) - } - - fn delete(&mut self, _name: &str) -> Result<(), SyscallError> { - Err(SyscallError::PermissionDenied) - } - - fn rename(&mut self, _old: &str, _new: &str) -> Result<(), SyscallError> { - Err(SyscallError::PermissionDenied) - } - - // `NotSupported`, not a refusal: no directory representation here, so the - // VFS carries created directories itself. - fn create_dir(&mut self, _name: &str) -> Result<(), SyscallError> { - Err(SyscallError::NotSupported) - } - - fn remove_dir(&mut self, _name: &str) -> Result<(), SyscallError> { - Err(SyscallError::NotSupported) - } - - fn create_symlink(&mut self, _name: &str, _target: &str) -> Result<(), SyscallError> { - Err(SyscallError::PermissionDenied) - } - - fn open_backing(&mut self, name: &str) -> Result, SyscallError> { - let (extents, size) = present("open_backing", name, self.fs.file_extents(name))?; - Ok(Arc::new(ReadOnlyBacking::new(*self.fs.io(), extents, size))) - } -} - diff --git a/kernel/src/bootimage.rs b/kernel/src/bootimage.rs new file mode 100644 index 00000000000..2d44fa43fce --- /dev/null +++ b/kernel/src/bootimage.rs @@ -0,0 +1,97 @@ +//! The payloads the loader hands the kernel beside its own ELF: the +//! supervisor's executable, the file server's and ROOT's bytes +//! (`toyos_abi::boot::BootImage`). The kernel parses none of them but the +//! supervisor's program headers, which it spawns the supervisor from; the +//! other two it hands the supervisor, whose file server serves `/system` out +//! of ROOT. +//! +//! [`init`] runs before `mm::init` and refuses the boot by name unless each +//! image is non-empty, page-aligned and whole pages inside one `LoaderData` +//! descriptor; `kernel_main` then refuses any two reserved regions that share +//! a byte. The loader is trusted and verified each image's hash, so a refusal +//! here is its bug, not an input to survive. The regions it returns are kept +//! out of the allocator and charged to no process, for the machine's life. +//! +//! [`objects`] makes each one a [`SharedMemObject`] over its pages +//! ([`Memory::Boot`]), which every mapping maps read-only, so no holder writes +//! it whatever rights its handle carries. + +use alloc::sync::Arc; + +use toyos_abi::boot::{BootImage, KernelArgs, MemoryMapEntry}; +use toyos_rootimage::handoff::{held, Descriptor}; + +use crate::mm::policy::CachePolicy; +use crate::mm::{DirectMap, Region, PAGE_SIZE}; +use crate::object::shm::{Memory, SharedMemObject}; + +/// The three images, as the loader handed them and [`init`] judged them. +#[derive(Clone, Copy)] +pub struct Handed { + supervisor: BootImage, + fileserver: BootImage, + root: BootImage, +} + +/// Every image in the order [`init`] returns their regions, by name. +fn named(handed: &Handed) -> [(&'static str, BootImage); 3] { + [("supervisor", handed.supervisor), ("fileserver", handed.fileserver), ("ROOT", handed.root)] +} + +/// The pages `image` occupies. +fn pages(image: BootImage) -> Region { + Region { start: image.addr, end: image.addr + image.len.next_multiple_of(PAGE_SIZE) } +} + +/// Take the images out of the handoff, refusing the boot by name on one the +/// kernel may not keep, and answer them with their regions for `mm::init` to +/// withhold. Runs before `mm::init`, so it neither allocates nor logs. +pub fn init(args: &KernelArgs, map: &[MemoryMapEntry]) -> (Handed, [Region; 3]) { + let handed = Handed { supervisor: args.supervisor_image, fileserver: args.fileserver_image, root: args.root_image }; + for (what, image) in named(&handed) { + assert!(image.len != 0, "boot: the loader handed no {what} image"); + let rounded = image.len.checked_next_multiple_of(PAGE_SIZE); + let kept = rounded.and_then(|len| { + held( + map.iter().map(|e| Descriptor { ty: e.uefi_type, start: e.start, end: e.end }), + toyos_bootmap::EFI_LOADER_DATA, + image.addr, + len, + PAGE_SIZE, + ) + }); + assert!( + kept.is_some(), + "boot: the {what} image at {:#x}+{:#x} is not whole {PAGE_SIZE}-byte pages inside one LoaderData \ + descriptor", + image.addr, + image.len + ); + } + (handed, named(&handed).map(|(_, image)| pages(image))) +} + +/// The objects the supervisor is spawned from and handed. +pub struct Objects { + pub supervisor: Arc, + pub fileserver: Arc, + pub root: Arc, +} + +/// One object over each image, once `mm::init` has run, for the supervisor's +/// spawn. +pub fn objects(handed: Handed) -> Objects { + let object = |image: BootImage| { + let region = pages(image); + SharedMemObject::over(crate::object::shm::Region { + phys: DirectMap::from_phys(region.start), + size: region.end - region.start, + cache: CachePolicy::Normal, + memory: Memory::Boot { len: image.len }, + }) + }; + for (what, image) in named(&handed) { + log!("boot: {what} image {:#x}+{:#x}, kept read-only for the machine's life", image.addr, image.len); + } + Objects { supervisor: object(handed.supervisor), fileserver: object(handed.fileserver), root: object(handed.root) } +} diff --git a/kernel/src/drivers/gop.rs b/kernel/src/drivers/gop.rs index a733cff43cd..7a7a383a2a0 100644 --- a/kernel/src/drivers/gop.rs +++ b/kernel/src/drivers/gop.rs @@ -42,7 +42,7 @@ pub fn init( phys: DirectMap::from_phys(addr), size: aligned_size, cache: CachePolicy::WriteCombining, - pages: None, + memory: crate::object::shm::Memory::Foreign, }; log!("GOP: {}x{} stride={} format={} at {:#x}", width, height, stride, pixel_format, addr); diff --git a/kernel/src/file_backing.rs b/kernel/src/file_backing.rs index c6926bb0812..e175efeb0b0 100644 --- a/kernel/src/file_backing.rs +++ b/kernel/src/file_backing.rs @@ -1,14 +1,10 @@ use alloc::sync::Arc; -use alloc::vec::Vec; -use bcachefs::Extent; use crate::object::shm::SharedMemObject; -use crate::rootfs::MemoryImage; use toyos_abi::syscall::SyscallError; /// `mm::PAGE_SIZE`: `usize` for buffer sizing, `u64` for file offsets. const BLOCK_SIZE: usize = crate::mm::PAGE_SIZE as usize; -const BLOCK_SIZE_U64: u64 = crate::mm::PAGE_SIZE; /// Why a page a backing holds could not be read. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -17,17 +13,10 @@ pub enum BlockError { Device, } -/// Every failure here was attempted: the bcachefs crate retries none. -impl bcachefs::TransferError for BlockError { - fn refused_before_attempt(&self) -> bool { - false - } -} - pub type BlockResult = Result<(), BlockError>; -/// Backing store for a memory-mapped file: ROOT's image, an image a caller -/// handed over, or a tmpfs file; callers do not know which. +/// Backing store for a memory-mapped file: an image a caller handed over, or a +/// tmpfs file; callers do not know which. pub trait FileBacking: Send + Sync { /// Reads one page of file data at `file_offset` into `buf`, zero-filling past EOF. #[must_use = "a failed read left the buffer zeroed; it does not hold the file's bytes"] @@ -37,68 +26,10 @@ pub trait FileBacking: Send + Sync { fn file_size(&self) -> u64; } -/// The block holding `file_offset`, if the extents reach that far. -fn offset_to_block(extents: &[Extent], file_offset: u64) -> Option { - let block_idx = file_offset / BLOCK_SIZE_U64; - let mut cursor = 0u64; - for ext in extents { - let count = ext.block_count as u64; - if block_idx < cursor + count { - return Some(ext.start_block + (block_idx - cursor)); - } - cursor += count; - } - None -} - -/// File on ROOT, backed by a fixed extent list over the image in memory. -/// -/// No revocation cell: nothing can delete or truncate a -/// file here, so the blocks a backing was opened over stay that file's for as -/// long as it lives. A block outside the image is refused by -/// [`MemoryImage::read`], which is where every bound on a number the image -/// chose belongs. -pub struct ReadOnlyBacking { - image: MemoryImage, - extents: Vec, - size: u64, -} - -impl ReadOnlyBacking { - pub fn new(image: MemoryImage, extents: Vec, size: u64) -> Self { - Self { image, extents, size } - } -} - -impl FileBacking for ReadOnlyBacking { - fn read_page(&self, file_offset: u64, buf: &mut [u8; BLOCK_SIZE]) -> BlockResult { - buf.fill(0); - if file_offset >= self.size { - return Ok(()); - } - // Past the extent list: a hole, and zeros are the file's own bytes. - let Some(block) = offset_to_block(&self.extents, file_offset) else { - return Ok(()); - }; - let mut raw = [0u8; BLOCK_SIZE]; - // `buf` is already zeroed, so a failed read returns a hole, not stale data. - if let Err(e) = self.image.read(block, &mut raw) { - log!("root: read of block {block} failed"); - return Err(e); - } - let valid = BLOCK_SIZE.min((self.size - file_offset) as usize); - buf[..valid].copy_from_slice(&raw[..valid]); - Ok(()) - } - - fn file_size(&self) -> u64 { - self.size - } -} - -/// An executable a caller read into a shared memory object of its -/// own and handed over by handle: what a program in `/apps` is spawned and -/// paged from, since no file server's volume is the kernel's. +/// An executable in a shared memory object handed over by handle: one a +/// caller read into an object of its own, which is what every program a file +/// server holds is spawned and paged from, since no file server's volume is +/// the kernel's; or a boot image the loader verified. /// /// **Nothing is copied at the call, and every page is copied once when it is /// read.** The object is the caller's memory, charged to it and alive while @@ -114,10 +45,10 @@ pub struct SharedImage { impl SharedImage { /// The first `len` bytes of `object`. Refused unless they are ordinary - /// memory the kernel allocated — a device aperture is no program, and a - /// read of one is a device access — and the object holds them all. + /// memory — a device aperture is no program, and a read of one is a device + /// access — and the object holds them all. pub fn over(object: Arc, len: u64) -> Result { - if len > object.size() || object.ram().is_none() { + if len > object.size() || !object.is_ordinary() { return Err(SyscallError::InvalidArgument); } Ok(Self { object, size: len }) @@ -132,9 +63,11 @@ impl FileBacking for SharedImage { } let valid = BLOCK_SIZE.min((self.size - file_offset) as usize); // SAFETY: `over` held `size` inside the object, which is one physically - // contiguous run the kernel allocated (`ram`) and keeps while `object` - // lives, so `file_offset + valid <= size` bytes from its direct-map - // address are mapped; `buf` is the reader's own and never the object. + // contiguous run of ordinary memory (`is_ordinary`: pages the kernel + // allocated, or a boot image it keeps for the machine's life) alive + // while `object` lives, so `file_offset + valid <= size` bytes from its + // direct-map address are mapped; `buf` is the reader's own and never + // the object. // No reference is formed over the object's bytes, which its holders // may be writing: this is the one fetch of each. unsafe { diff --git a/kernel/src/loaded.rs b/kernel/src/loaded.rs index 2cc6a4ec82a..fccc75dc25c 100644 --- a/kernel/src/loaded.rs +++ b/kernel/src/loaded.rs @@ -1,8 +1,8 @@ //! The partitions the loader named: the ROOT it read, the booted slot's //! volume, the log's, and the ESP firmware loaded it from (`Role::Boot`). -//! This kernel reads none of them but ROOT, which is in memory (`rootfs`); it -//! answers their GUIDs in the inventory, so the log and boot file servers find -//! theirs and the supervisor knows the running slot, whoever drives the disk. +//! This kernel reads none of them; it answers their GUIDs in the inventory, so +//! the log and boot file servers find theirs and the supervisor knows the +//! running slot, whoever drives the disk. use alloc::vec::Vec; diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index e139a9b4476..fcfd9056790 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -434,16 +434,17 @@ pub fn spawn( Ok((pid, held)) } -/// The one program the kernel starts. `build/build.rs` puts this binary in every -/// image, so a missing one is a bad build, not a different boot. -pub const SUPERVISOR_PATH: &str = "/system/bin/supervisor"; +/// The name the supervisor runs under: no file, since it runs from its boot image. +const SUPERVISOR: &str = "supervisor"; -/// Start `/system/bin/supervisor`, holding the machine's one full-rights `SysCap`. +/// Start the supervisor from its boot image, holding the machine's one +/// full-rights `SysCap` and the file server's image and ROOT's +/// (`toyos_abi::syscall::FILESERVER_IMAGE_LABEL`, `ROOT_IMAGE_LABEL`). /// /// Nothing else can construct one: what the supervisor endows is the entire set of /// processes that can ever claim a device, enter the RT band, or power off. /// Panics on failure: a boot that cannot start the supervisor has nowhere to report to. -pub fn spawn_supervisor() -> Pid { +pub fn spawn_supervisor(images: crate::bootimage::Objects) -> Pid { let mut handles = HandleTable::new(); let mut entries = Vec::new(); let mut labels = Vec::new(); @@ -486,12 +487,34 @@ pub fn spawn_supervisor() -> Pid { _pad: 0, }); labels.extend_from_slice(label.as_bytes()); + for (label, object) in [ + (toyos_abi::syscall::FILESERVER_IMAGE_LABEL, &images.fileserver), + (toyos_abi::syscall::ROOT_IMAGE_LABEL, &images.root), + ] { + let object = KObjectRef::SharedMem(Arc::clone(object)); + let rights = ops::initial_rights(&object); + let handle = handles + .install(crate::object::HandleEntry::new(object, rights)) + .expect("spawn_supervisor: a table of five handles refused a boot image"); + entries.push(toyos_abi::syscall::EndowEntry { + label_off: labels.len() as u32, + label_len: label.len() as u32, + handle, + _pad: 0, + }); + labels.extend_from_slice(label.as_bytes()); + } + let len = images.supervisor.immutable().expect("a boot image is immutable"); + let image: Arc = Arc::new( + crate::file_backing::SharedImage::over(images.supervisor, len) + .expect("spawn_supervisor: a boot image is ordinary memory, its length inside it"), + ); // Built by the kernel and owing nobody anything: no table but the supervisor's own holds it. let commit = |own: start::Own| { own.endow(&mut handles, &mut entries, &mut labels); Ok((handles, Endowments::new(entries, labels), crate::object::job::new_job(), ())) }; - match spawn(SUPERVISOR_PATH, &[SUPERVISOR_PATH], commit, String::from("/"), Vec::new(), None, Parent::Root) { + match spawn(SUPERVISOR, &[SUPERVISOR], commit, String::from("/"), Vec::new(), Some(image), Parent::Root) { Ok((pid, ())) => pid, Err(crate::object::Refusal::Error(e)) => panic!("spawn_supervisor: failed to spawn: {e:?}"), Err(crate::object::Refusal::Handle(e)) => panic!("spawn_supervisor: {e}"), diff --git a/kernel/src/log/mod.rs b/kernel/src/log/mod.rs index 291b2d3a7ac..fecbfb3d0bc 100644 --- a/kernel/src/log/mod.rs +++ b/kernel/src/log/mod.rs @@ -74,7 +74,7 @@ const TAIL_HEAD: &str = "log: this boot's newest records follow, newest first"; /// records a stop writes is the machine's to decide, by its CPUs and its disks. /// /// **The kernel does not wait for `/system/bin/logkeeper`, so it does not know what -/// reached `/log`.** `/system/bin/supervisor` has `logkeeper` flush before it asks for the +/// reached `/log`.** The supervisor has `logkeeper` flush before it asks for the /// stop; everything committed after that — the stop's own record, the last /// word — is on the console, and here, where the next /// loader pass prints it into `loader.log`. diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 625f448714a..baca858726e 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -45,14 +45,13 @@ mod power; mod inventory; mod loaded; mod rollback; -mod rootfs; +mod bootimage; mod file_cache; #[cfg(feature = "boot-actuators")] #[cfg(feature = "boot-actuators")] mod revoke_selftest; mod tmpfs; mod file_backing; -mod bcachefs_adapter; mod fs_rename; mod vfs; mod elf; @@ -265,7 +264,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { params::init(cmdline); deadline::claim(cmdline); actuator::init(cmdline); - let root_image = rootfs::init(cmdline, &kernel_args, maps); + let (boot_images, [supervisor_image, fileserver_image, root_image]) = bootimage::init(&kernel_args, maps); arch::boot::after_console(&kernel_args, maps); @@ -310,13 +309,6 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { "boot: cmdline {:#x}+{}", kernel_args.cmdline_addr, kernel_args.cmdline_len ); - // ROOT's name hashes every file it carries, `/system/etc/os-release` among - // them: the build, named before anything is mounted. - match toyos_abi::boot::root_uuid(cmdline).map(bcachefs::FsUuid::parse) { - Some(Some(root)) => log!("boot: root={root}"), - Some(None) => log!("boot: root= names no filesystem this kernel can parse"), - None => log!("boot: the cmdline carries no root="), - } // Before `mm::init`, which may hand the parameter's memory out. This record // is how a slot that died or was refused reaches the next boot's `/log`. match params::slot(cmdline) { @@ -351,8 +343,9 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // memory the allocator would otherwise hand out. Empty on a boot whose // parameter line names none. blackbox::reserved_region(), - // ROOT's image, `LoaderData` like the black box's page. Empty on a - // boot the loader handed none. + // The boot images (`bootimage`), `LoaderData` like the black box's page. + supervisor_image, + fileserver_image, root_image, // Firmware's memory map as the loader copied it, which `mm` keeps // (`mm::firmware_map`): `LoaderData` too. @@ -382,8 +375,17 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // name, rather than indexing it, means a region added to `loader` fails // to compile here instead of compiling and being silently dropped from // what `mm::init` withholds. - let [image, elf, black_box, root, map, microcode] = loader; - let reserved = [image, elf, black_box, root, map, microcode, arch::boot::reserved()]; + let [image, elf, black_box, supervisor, fileserver, root, map, microcode] = loader; + let reserved = [image, elf, black_box, supervisor, fileserver, root, map, microcode, arch::boot::reserved()]; + // A region another one shares is two allocations the loader made of one + // byte, and whichever writes it second corrupts the first. + let extents = reserved.map(|r| r.start..r.end); + if let Some((a, b)) = toyos_rootimage::handoff::overlapping(&extents) { + panic!( + "boot: the reserved regions {:#x}..{:#x} and {:#x}..{:#x} share memory", + reserved[a].start, reserved[a].end, reserved[b].start, reserved[b].end + ); + } let layout = toyos_elf::Layout::parse(kernel_elf, arch::ELF_MACHINE) .unwrap_or_else(|e| panic!("boot: the loader's kernel.elf: {e}")); let image = mm::Image::new(&layout, image); @@ -446,22 +448,15 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { scheduler::init(); pipe::init(); - // ROOT is the loader's image in memory, so nothing from here to the supervisor's - // spawn asks a disk for anything: every storage driver comes up after it. - use vfs::UserAccess; - let root_fs = rootfs::mount(); - vfs::lock().mount( - &["system"], - Box::new(bcachefs_adapter::ReadOnlyBcacheFsAdapter::new(root_fs)), - UserAccess::KernelOnly, - ); - vfs::lock().mount(&["tmp"], Box::new(crate::tmpfs::TmpFs::new()), UserAccess::ReadWrite); + vfs::lock().mount(&["tmp"], Box::new(crate::tmpfs::TmpFs::new())); boot_phase!("subsystems ready", t_subsys); + // Off the loader's images, so nothing from here to the supervisor's + // spawn asks a disk for anything: every storage driver comes up after it. // The supervisor reads /system/etc/system.manifest itself; the boot config never names the program it starts. - let pid = process::spawn_supervisor(); - log!("spawned {} pid={pid}", process::SUPERVISOR_PATH); + let pid = process::spawn_supervisor(bootimage::objects(boot_images)); + log!("spawned the supervisor pid={pid}"); #[cfg(feature = "boot-actuators")] if actuator::revoked_backing_selftest() { diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index 1e5ff32d035..ca3eee5229e 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -677,8 +677,12 @@ pub fn fstat(object: &KObjectRef) -> Stat { KObjectRef::Acceptor(_) => plain(FileType::Pipe), KObjectRef::SharedMem(m) => Stat { // The verdict `pcidev::dma_map` lends a device by: a receiver judges a peer's region on it before mapping. - file_type: if m.ram().is_some() { FileType::SharedMemory } else { FileType::DeviceMemory } as u64, - size: m.size(), + file_type: match (m.ram(), m.immutable()) { + (Some(_), _) => FileType::SharedMemory, + (None, Some(_)) => FileType::ImmutableMemory, + (None, None) => FileType::DeviceMemory, + } as u64, + size: m.immutable().unwrap_or(m.size()), mtime: 0, volume: 0, node: 0, diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index 4e3b1e47e03..344b0e883a2 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -7,8 +7,13 @@ //! a handle holds an `Arc`. An `Arc` a syscall cloned out of its table can //! outlive the last handle, so a map through one finds no list and is refused //! under the lock the teardown took it with. +//! +//! **A mapping does not keep its process's address space**: the list holds it +//! weakly, so a process that ended while another still holds the region has +//! its space, and everything the space holds, freed with it; its entry is dead +//! and is passed over. -use alloc::sync::Arc; +use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; use toyos_abi::syscall::SyscallError; @@ -17,6 +22,7 @@ use crate::mm::policy::{CachePolicy, Prot}; use crate::mm::paging::Detached; use crate::mm::{align_2m_checked, pmm, Unmapped, PAGE_2M, PAGE_SIZE}; use crate::process::{PageTables, Pid}; +use crate::sync::Lock; use crate::{DirectMap, UserAddr}; use super::{Held, KObjectVariant, ObjectCore, ZeroHandles}; @@ -37,16 +43,41 @@ pub struct Region { pub phys: DirectMap, pub size: u64, pub cache: CachePolicy, - /// The pages, when the kernel owns them; `None` for firmware's - /// framebuffer or an MMIO aperture it does not own. - pub pages: Option>, + pub memory: Memory, +} + +/// Whose memory a region is. +#[derive(Clone)] +pub enum Memory { + /// Pages the kernel allocated, freed with the last `Arc`, which the + /// field only keeps alive. + Owned { _pages: Arc }, + /// A payload the loader verified and handed over, `len` bytes of the + /// region's pages: kept for the machine's life and written by nobody, so + /// every mapping of it is read-only (`crate::bootimage`). + Boot { len: u64 }, + /// Firmware's framebuffer or an MMIO aperture the kernel does not own. + Foreign, } pub struct SharedMemObject { pub(super) core: ObjectCore, region: Region, /// Where this region is mapped, per process; released by the zero-handle hook. - mapped_in: Held>, + mapped_in: Held>, +} + +/// One process's mapping of a region: dead once its address space is gone. +struct Mapping { + pid: Pid, + space: Weak>, + at: UserAddr, +} + +impl Mapping { + fn live(&self) -> Option { + self.space.upgrade() + } } impl SharedMemObject { @@ -84,7 +115,7 @@ impl SharedMemObject { phys, size: aligned, cache: CachePolicy::Normal, - pages: Some(Arc::new(Pages(pages))), + memory: Memory::Owned { _pages: Arc::new(Pages(pages)) }, })) } @@ -98,10 +129,25 @@ impl SharedMemObject { /// a second device's domain would be one device reaching another's /// registers. pub fn ram(&self) -> Option<(u64, u64)> { - (self.region.pages.is_some() && self.region.cache == CachePolicy::Normal) + (matches!(self.region.memory, Memory::Owned { .. }) && self.region.cache == CachePolicy::Normal) .then(|| (self.region.phys.phys(), self.region.size)) } + /// The payload's length, for a region the loader handed over: memory + /// nobody writes. + pub fn immutable(&self) -> Option { + match self.region.memory { + Memory::Boot { len } => Some(len), + Memory::Owned { .. } | Memory::Foreign => None, + } + } + + /// Whether a program may be paged from it: ordinary memory, never a + /// device's, whose read is a device access. + pub fn is_ordinary(&self) -> bool { + self.ram().is_some() || self.immutable().is_some() + } + /// The kernel's own view of the pages, through the direct map; a mapped /// region is memory two writers can share — see `inbox.rs`. pub fn phys(&self) -> DirectMap { @@ -122,19 +168,25 @@ impl SharedMemObject { } fn mapped_nowhere(&self) -> bool { - self.mapped_in.with(|mapped| mapped.is_empty()).unwrap_or(true) + self.mapped_in.with(|mapped| mapped.iter().all(|m| m.space.strong_count() == 0)).unwrap_or(true) } /// Map into `pt`, or answer the address it is already mapped at; /// idempotent per process. `Gone` once the last handle has gone. pub fn map_into(&self, pid: Pid, pt: &PageTables) -> Result { - let map = |mapped: &mut Vec<(Pid, PageTables, UserAddr)>| { - if let Some((_, _, vaddr)) = mapped.iter().find(|(p, _, _)| *p == pid) { - return Ok(vaddr.raw()); + let map = |mapped: &mut Vec| { + // A dead entry's pid may be this process's, reused. + mapped.retain(|m| m.space.strong_count() > 0); + if let Some(m) = mapped.iter().find(|m| m.space.as_ptr() == Arc::as_ptr(pt)) { + return Ok(m.at.raw()); } + let prot = match self.region.memory { + Memory::Boot { .. } => Prot::Read, + Memory::Owned { .. } | Memory::Foreign => Prot::ReadWrite, + }; let (addr, _) = pt .lock() - .alloc_and_map(self.region.phys.phys(), self.region.size, Prot::ReadWrite, self.region.cache) + .alloc_and_map(self.region.phys.phys(), self.region.size, prot, self.region.cache) .ok_or(SyscallError::ResourceExhausted)?; // Logged only for a non-default policy: this process is the one // paying for it. Read back the installed policy, not the request, @@ -148,7 +200,7 @@ impl SharedMemObject { pid ); } - mapped.push((pid, Arc::clone(pt), addr)); + mapped.push(Mapping { pid, space: Arc::downgrade(pt), at: addr }); Ok(addr.raw()) }; self.mapped_in.with_mut(map).unwrap_or(Err(SyscallError::Gone)) @@ -159,9 +211,10 @@ impl SharedMemObject { #[must_use = "the caller owes a shootdown before the address can be reissued"] pub fn unmap_from(&self, pid: Pid) -> Option>> { self.mapped_in.with_mut(|mapped| { - let pos = mapped.iter().position(|(p, _, _)| *p == pid)?; - let (_, pt, vaddr) = mapped.swap_remove(pos); - let detached = pt.lock().free_and_unmap(vaddr); + mapped.retain(|m| m.space.strong_count() > 0); + let pos = mapped.iter().position(|m| m.pid == pid)?; + let m = mapped.swap_remove(pos); + let detached = m.live().and_then(|pt| pt.lock().free_and_unmap(m.at)); Some(Unmapped::new(detached)) })? } @@ -169,14 +222,15 @@ impl SharedMemObject { /// Every mapping goes, flushed here, and with the list gone no later map can /// add one; the pages do not, since a handle holds an `Arc` and -/// `Region::pages` frees them only when the last one drops. +/// [`Memory::Owned`] frees them only when the last one drops. impl ZeroHandles for SharedMemObject { fn on_zero_handles(&self) { let mapped = self.mapped_in.take().expect("the zero-handle hook runs once"); if mapped.is_empty() { return; } - let detached: Vec<_> = mapped.iter().filter_map(|(_, pt, vaddr)| pt.lock().free_and_unmap(*vaddr)).collect(); + let detached: Vec<_> = + mapped.iter().filter_map(|m| m.live().and_then(|pt| pt.lock().free_and_unmap(m.at))).collect(); drop(Unmapped::new((mapped, detached))); } } diff --git a/kernel/src/object/syscap.rs b/kernel/src/object/syscap.rs index 3a2f736e003..2965eb18ba8 100644 --- a/kernel/src/object/syscap.rs +++ b/kernel/src/object/syscap.rs @@ -3,7 +3,7 @@ //! Minting a device claim, entering the RT band, and turning a pid into a //! process handle are each reachable only through one bit on a handle to //! this. The kernel creates exactly one full-rights `SysCap`, at boot, for -//! `/system/bin/supervisor`; nothing else can construct one, so the set of processes +//! the supervisor; nothing else can construct one, so the set of processes //! that can ever do the three is exactly what the supervisor endowed. use alloc::sync::Arc; diff --git a/kernel/src/params.rs b/kernel/src/params.rs index 8c98cd8feef..3877be06188 100644 --- a/kernel/src/params.rs +++ b/kernel/src/params.rs @@ -18,7 +18,7 @@ static EARLY_PANEL_NAMED: AtomicBool = AtomicBool::new(false); static PARSED: AtomicBool = AtomicBool::new(false); pub fn init(cmdline: &str) { - for token in toyos_abi::boot::actuators(cmdline) { + for token in toyos_abi::boot::tokens(cmdline) { if let Some((_, named)) = PARAMS.iter().find(|(name, _)| *name == token) { named.store(true, Ordering::Relaxed); } @@ -49,7 +49,7 @@ pub const SLOT_RECORD: &str = "boot: slot"; /// loader wrote them: the last of each, because the loader appends its words /// after the slot's own parameter and so has the last one. pub fn slot(cmdline: &str) -> (Option<&str>, Option<&str>) { - let word = |prefix: &str| toyos_abi::boot::actuators(cmdline).filter_map(|t| t.strip_prefix(prefix)).last(); + let word = |prefix: &str| toyos_abi::boot::tokens(cmdline).filter_map(|t| t.strip_prefix(prefix)).last(); (word(toyos_abi::boot::SLOT_PARAM), word(toyos_abi::boot::SLOT_REFUSED_PARAM)) } diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 028d0be056f..0dfd7b10353 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -1360,7 +1360,7 @@ pub fn bar_object(binding: &Binding, index: u64) -> Result, // Registers, and the memory type is not firmware's to decide. cache: CachePolicy::Uncacheable, // The kernel owns no pages here: this is a device's aperture. - pages: None, + memory: crate::object::shm::Memory::Foreign, })) }) } diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 9c113af12b2..8ee82da6e08 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -53,7 +53,7 @@ use kernel::proclife::{join, reap, spawn as proclife_spawn, teardown as proclife /// One `EndowEntry` on the wire; `loader::start` and [`Endowments::encode`] both index by it. pub const ENDOW_ENTRY_LEN: usize = core::mem::size_of::(); -pub use crate::loader::{build_child_handles, spawn, spawn_supervisor, SUPERVISOR_PATH}; +pub use crate::loader::{build_child_handles, spawn, spawn_supervisor}; /// Page tables shared between a process and all its threads. pub type PageTables = Arc>; diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs deleted file mode 100644 index 28c1bce039f..00000000000 --- a/kernel/src/rootfs.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! ROOT: the filesystem the boot parameter names, mounted read-only at -//! `/system` from the image the loader read into memory. -//! -//! The loader picks the partition and reads its filesystem whole; this kernel -//! reads ROOT from nowhere else. [`init`] takes the image out of the handoff -//! and keeps it only when `toyos_rootimage::handoff` finds it whole blocks -//! inside one `LoaderData` descriptor of firmware's map, returning the region -//! `mm::init` keeps out of the allocator. [`mount`] refuses the boot by name on -//! a handoff with no image, and on an image whose superblock is not the one -//! `root=` names. The image's bytes crossed a -//! trust boundary like any disk's, so every read of it is bounds-checked and a -//! block outside it is a refused read, never a panic. - -use bcachefs::{BlockBuf, BlockIO, BlockNum, DeviceError, FsUuid, Mounted, ReadOnly}; -use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; -use toyos_rootimage::handoff::{held, Descriptor}; - -use crate::file_backing::BlockError; -use crate::mm::{DirectMap, Region}; -use crate::sync::Lock; - -/// The unit ROOT's filesystem is written in, which is the page. -const BLOCK: usize = crate::mm::PAGE_SIZE as usize; - -/// The record a ROOT mounted off the loader's image is reported on. -pub const MOUNTED_FROM_MEMORY: &str = "root: mounted read-only from memory at"; - -/// What the handoff said about ROOT, decided before `mm::init` may hand the -/// loader's pool memory back: nothing may hold a borrow of it this far into a boot. -#[derive(Clone, Copy)] -enum Handed { - /// The loader handed no image. - Nothing, - /// An extent that is not whole blocks inside one `LoaderData` descriptor. - Unheld { at: u64, len: u64 }, - Image(MemoryImage), -} - -#[derive(Clone, Copy)] -struct Boot { - named: Option, - handed: Handed, -} - -static BOOT: Lock = Lock::new(Boot { named: None, handed: Handed::Nothing }); - -/// ROOT's bytes: whole blocks, read-only, in memory the kernel never frees. -#[derive(Clone, Copy)] -pub struct MemoryImage(&'static [u8]); - -impl MemoryImage { - /// Where the image is, as the physical address and length the loader handed. - pub fn extent(&self) -> (u64, u64) { - (DirectMap::phys_of(self.0.as_ptr()), self.0.len() as u64) - } - - /// Copy block `block` into `into`; a block outside the image is a refused read. - pub fn read(&self, block: u64, into: &mut [u8; BLOCK]) -> Result<(), BlockError> { - let bytes = usize::try_from(block) - .ok() - .and_then(|block| block.checked_mul(BLOCK)) - .and_then(|at| self.0.get(at..at.checked_add(BLOCK)?)) - .ok_or(BlockError::Device)?; - into.copy_from_slice(bytes); - Ok(()) - } -} - -impl BlockIO for MemoryImage { - fn read_block(&self, block: BlockNum, buf: &mut BlockBuf) -> Result<(), DeviceError> { - self.read(block.raw(), &mut buf.0).map_err(|e| DeviceError::classify(&e)) - } - - /// A `ReadOnly` mount has no path to this; reaching it is a kernel bug. - fn write_block(&self, block: BlockNum, _buf: &BlockBuf) -> Result<(), DeviceError> { - panic!("rootfs: a read-only mount wrote block {} of the in-memory ROOT", block.raw()); - } - - fn block_count(&self) -> u64 { - (self.0.len() / BLOCK) as u64 - } -} - -/// Take ROOT's name out of the boot parameter and its image out of the -/// handoff, returning the region `mm::init` must keep: the image, or an empty -/// one where there is none to keep. Runs before `mm::init`, so it neither -/// allocates nor logs. -pub fn init(cmdline: &str, args: &KernelArgs, map: &[MemoryMapEntry]) -> Region { - let (at, len) = (args.root_image_addr, args.root_image_len); - let descriptors = map.iter().map(|entry| Descriptor { ty: entry.uefi_type, start: entry.start, end: entry.end }); - let none = Region { start: 0, end: 0 }; - let (handed, region) = match held(descriptors, toyos_bootmap::EFI_LOADER_DATA, at, len, BLOCK as u64) { - _ if len == 0 => (Handed::Nothing, none), - None => (Handed::Unheld { at, len }, none), - Some(extent) => ( - // SAFETY: the extent is whole pages of `LoaderData` the loader - // allocated and wrote before the jump, and the region returned here - // keeps `mm::init`'s allocator off it, so it is live, never written, - // and mapped at PHYS_OFFSET for the kernel's life. - Handed::Image(MemoryImage(unsafe { - core::slice::from_raw_parts(DirectMap::from_phys(at).as_ptr::(), len as usize) - })), - Region { start: extent.start, end: extent.end }, - ), - }; - *BOOT.lock() = Boot { - named: toyos_abi::boot::root_uuid(cmdline).and_then(FsUuid::parse), - handed, - }; - region -} - -/// Mount the filesystem the boot parameter named, off the image the loader -/// handed. Panics when there is no image, or it is not that filesystem. -pub fn mount() -> Mounted { - let Boot { named, handed, .. } = *BOOT.lock(); - let Some(named) = named else { - panic!("boot: the kernel argument names no root filesystem this kernel can parse"); - }; - let image = match handed { - Handed::Nothing => panic!( - "boot: the loader handed no ROOT image, and this kernel reads ROOT from memory and nowhere else" - ), - Handed::Unheld { at, len } => panic!( - "boot: the ROOT image at {at:#x}+{len:#x} is not whole {BLOCK}-byte blocks inside one \ - LoaderData descriptor" - ), - Handed::Image(image) => image, - }; - let fs = Mounted::<_, ReadOnly>::open(image) - .unwrap_or_else(|e| panic!("boot: the ROOT image holds no filesystem this kernel can mount: {e:?}")); - if fs.uuid() != named { - panic!("boot: root={named} and the ROOT image the loader handed holds {}", fs.uuid()); - } - let (at, len) = image.extent(); - log!("{MOUNTED_FROM_MEMORY} {at:#x}+{len:#x}, filesystem {named}, {} blocks", image.block_count()); - fs -} diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index 0512ac6884e..b5701c74a52 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -2,7 +2,7 @@ //! //! [`sys_log_read`], [`sys_trace_read`], the roster half of [`sys_sysinfo`], and both of //! [`sys_shutdown`] and [`sys_reboot`] each require a `SysCap` bit from -//! `/system/bin/supervisor`'s `system.toml`; `SYS_SYSINFO`'s header is ambient. +//! the supervisor's `system.toml`; `SYS_SYSINFO`'s header is ambient. use alloc::vec::Vec; @@ -114,7 +114,7 @@ fn quiesce(last: &str) -> Result<(crate::quiesce::Stopping, StopWire), SyscallEr // First: what follows outlasts a feed cadence, and no pass runs to feed again. crate::arch::watchdog::disarm(); // Every userland thread stops here, the log's writer with the rest: - // `/system/bin/supervisor` had it flush before it asked for this stop. + // the supervisor had it flush before it asked for this stop. let (stopped, stopping) = crate::quiesce::stop(); // From here on nothing carries a record to a file: the seal below takes // every one stamped after this one, the newest the stop found. diff --git a/kernel/src/vfs.rs b/kernel/src/vfs.rs index 6d0e24fa008..2109c520916 100644 --- a/kernel/src/vfs.rs +++ b/kernel/src/vfs.rs @@ -47,7 +47,7 @@ pub trait FileSystem: Send { /// Whether `dir` (`""` is the mount root) is a directory: a working /// directory is judged by this, on every spawn and `SYS_CHDIR`, under the /// VFS lock. Nothing beneath it is materialised and nothing caps it; what it costs is - /// the mount's — a bcachefs answer of no reads the whole tree. + /// the mount's. fn is_dir(&mut self, dir: &str) -> Result; /// When `name` was last written, as `toyos_abi::syscall::Stat::mtime` says, @@ -80,14 +80,6 @@ pub trait FileSystem: Send { } -/// Whether userland may modify a mount; stated at every `mount`, defaulted nowhere. -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum UserAccess { - ReadWrite, - /// Readable, and every userland syscall that would change it is refused. - KernelOnly, -} - /// The entries `/` has, in listing order. `/` is synthesized rather than /// mounted: it is no filesystem, and nothing outside this set can be mounted. pub const ROOT_ENTRIES: [&str; 9] = @@ -95,7 +87,6 @@ pub const ROOT_ENTRIES: [&str; 9] = struct Mount { fs: Box, - access: UserAccess, } /// A name at `/`, and where in its filesystem that name begins. @@ -217,9 +208,9 @@ impl Vfs { /// Mount one filesystem under `names`, each a [`ROOT_ENTRIES`] name. Several /// names put each under its own directory of it. - pub fn mount(&mut self, names: &[&'static str], fs: Box, access: UserAccess) { + pub fn mount(&mut self, names: &[&'static str], fs: Box) { let index = self.mounts.len(); - self.mounts.push(Mount { fs, access }); + self.mounts.push(Mount { fs }); for name in names { let prefix = if names.len() == 1 { "" } else { *name }; let slot = Self::entry(name).expect("a mount name is one of the root's entries"); @@ -240,7 +231,7 @@ impl Vfs { /// and a bare mount point, answer no: the read-only refusal, not a missing name. pub fn user_may_modify(&self, path: &str) -> bool { let (mount, _) = self.resolve_path("/", path); - self.point(&mount).is_some_and(|p| self.mounts[p.fs].access == UserAccess::ReadWrite) + self.point(&mount).is_some() } fn resolve_fs(&mut self, mount: &str, file: &str) -> Option<(&mut dyn FileSystem, String)> { diff --git a/lib/bcachefs/src/fs.rs b/lib/bcachefs/src/fs.rs index 418cee4118e..662615095b7 100644 --- a/lib/bcachefs/src/fs.rs +++ b/lib/bcachefs/src/fs.rs @@ -552,7 +552,7 @@ impl Formatted { Ok(Self(Mounted { io, sb, alloc, _mode: PhantomData })) } - /// Name this filesystem, so a role's kernel argument can select it. + /// Name this filesystem, so whoever serves it can say which one it is. /// /// A separate act from formatting: a volume nothing names is legal, and /// nothing here invents a name for one. Persisted by [`Self::into_io`]. diff --git a/lib/bcachefs/src/superblock.rs b/lib/bcachefs/src/superblock.rs index f02c0853a09..ace3abef5b2 100644 --- a/lib/bcachefs/src/superblock.rs +++ b/lib/bcachefs/src/superblock.rs @@ -37,7 +37,7 @@ const _: () = assert!( ); /// A filesystem's name: sixteen bytes in its superblock, printed as 32 hex -/// digits, and what a role's kernel argument names it by. +/// digits. /// /// Not a partition GUID and never mixed with one — a partition is where a /// filesystem's bytes are, and one filesystem may occupy several. diff --git a/lib/logstream/src/lib.rs b/lib/logstream/src/lib.rs index a3add819e13..622c71a9001 100644 --- a/lib/logstream/src/lib.rs +++ b/lib/logstream/src/lib.rs @@ -1,5 +1,5 @@ //! A boot's log as `logkeeper` writes it and serves it: the form a program's line -//! takes beside the kernel's records, the frame `/system/bin/supervisor` hands `logkeeper` +//! takes beside the kernel's records, the frame the supervisor hands `logkeeper` //! a program's output on, the replay a reader who connects late is served //! from, and the one reader of a line's head ([`parse`]). //! diff --git a/lib/manifest/Cargo.toml b/lib/manifest/Cargo.toml index d64fea30828..7126e4555b4 100644 --- a/lib/manifest/Cargo.toml +++ b/lib/manifest/Cargo.toml @@ -7,6 +7,6 @@ license = "MIT OR Apache-2.0" # The one place a `devices` class name and a `syscap` right name are decoded. # Both are ABI vocabulary, and a manifest crate holding its own copy of either -# would be a config the build system accepts and `/system/bin/supervisor` cannot act on. +# would be a config the build system accepts and the supervisor cannot act on. [dependencies] toyos-abi = { path = "../../sdk/abi" } diff --git a/lib/manifest/src/consent.rs b/lib/manifest/src/consent.rs index 3da41be035d..0ffe28ecbbd 100644 --- a/lib/manifest/src/consent.rs +++ b/lib/manifest/src/consent.rs @@ -1,4 +1,4 @@ -//! The question `/system/bin/supervisor` asks the person at the screen before +//! The question the supervisor asks the person at the screen before //! an installed package's launch, and the answer it honours. //! //! **Only the supervisor asks, and only its question grants anything.** It diff --git a/lib/manifest/src/grants.rs b/lib/manifest/src/grants.rs index 9b4a7e68923..0f96168f25f 100644 --- a/lib/manifest/src/grants.rs +++ b/lib/manifest/src/grants.rs @@ -1,5 +1,5 @@ //! Which folder of the session user's home an installed package is granted, -//! and the store `/system/bin/supervisor` keeps those grants in. +//! and the store the supervisor keeps those grants in. //! //! **The supervisor is the only writer of [`STORE`].** A grant reaches it as a //! request on [`PORT`], which the build gives to [`HOLDER`] alone and the diff --git a/lib/manifest/src/launch.rs b/lib/manifest/src/launch.rs index 03645af006c..2c573c70e1e 100644 --- a/lib/manifest/src/launch.rs +++ b/lib/manifest/src/launch.rs @@ -1,4 +1,4 @@ -//! Who may start what through `/system/bin/supervisor`'s launcher. +//! Who may start what through the supervisor's launcher. //! //! **A caller starts only the rows its own row's `starts` lists**: program //! keys, and [`APPS`] for any installed package. **A row that replaces what the @@ -259,7 +259,7 @@ mod tests { use super::*; fn row(name: &str) -> Program { - Program { name: name.into(), path: format!("/system/bin/{name}"), ..Program::default() } + Program { name: name.into(), source: crate::Source::Path(format!("/system/bin/{name}")), ..Program::default() } } fn swap() -> Program { diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index ffb9c97ab2d..2db3b6eb0ff 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -1,5 +1,5 @@ //! What every program in an image is allowed to hold, written by the build -//! system and read by `/system/bin/supervisor`. +//! system and read by the supervisor. //! //! **One definition of the format, used by both halves.** `build/build.rs` //! resolves `system.toml` into a [`Manifest`] and [`render`]s it into the @@ -13,7 +13,8 @@ //! job already done. //! //! ```text -//! program starts a program's records +//! program starts a program's records: the executable at +//! boot-program starts one whose process runs the file server's boot image //! arg argv after argv[0] //! serve the supervisor makes one machine-wide port and endows the acceptor //! provide this program makes its own port, once per instance @@ -52,7 +53,7 @@ pub mod package; /// spelling. [`GUEST_PATH`] is what a process opens. pub const PATH: &str = "etc/system.manifest"; -/// The path `/system/bin/supervisor` opens. +/// The path the supervisor opens. pub const GUEST_PATH: &str = "/system/etc/system.manifest"; /// A program key may be this long. Policy on the primitive: the launcher @@ -60,7 +61,7 @@ pub const GUEST_PATH: &str = "/system/etc/system.manifest"; /// truncated into some other program's. pub const MAX_PROGRAM_NAME: usize = 32; -/// The port `/system/bin/supervisor` serves swaps on: its one holder replaces a +/// The port the supervisor serves swaps on: its one holder replaces a /// running service's binary, so a row receiving it starts only in a login /// session ([`launch`]). pub const SWAP_PORT: &str = "swap"; @@ -100,18 +101,25 @@ pub struct RoleDir { /// What every program's namespace calls it, after `fs:`: `/home`. pub dir: &'static str, /// Where it is on the role's volume, which a grant on it names - /// (`toyos::fs::Grant`): DATA keeps its directories side by side, and LOG - /// and BOOT are each served whole. + /// (`toyos::fs::Grant`): DATA keeps its directories side by side, and + /// SYSTEM, LOG and BOOT are each served whole. pub root: &'static str, + /// Whether a grant on it may change it: SYSTEM's ROOT is the signed + /// image, which nothing does. + pub write: bool, } const fn dir(dir: &'static str, root: &'static str) -> RoleDir { - RoleDir { dir, root } + RoleDir { dir, root, write: true } } +/// The role serving ROOT, which the supervisor starts before it reads a file. +pub const SYSTEM: &str = "system"; + /// The file-server roles, and the directories each serves: one port per role, /// and one grant on it per directory. -pub const ROLES: [(&str, &[RoleDir]); 3] = [ +pub const ROLES: [(&str, &[RoleDir]); 4] = [ + (SYSTEM, &[RoleDir { dir: "/system", root: "", write: false }]), ("data", &[dir("/apps", "apps"), dir("/config", "config"), dir("/home", "home"), dir("/state", "state")]), ("log", &[dir("/log", "")]), ("boot", &[dir("/boot", "")]), @@ -135,17 +143,23 @@ pub struct View { pub write: bool, } -/// Every directory every role serves, each read-write: the view of a program -/// no narrower one is declared for. +/// Every directory every role serves, each read-write but `/system`: the view +/// of a program no narrower one is declared for. pub fn whole_tree() -> Vec { ROLES .iter() .flat_map(|(role, dirs)| { - dirs.iter().map(|d| View { role, dir: d.dir.to_string(), root: d.root.to_string(), write: true }) + dirs.iter().map(|d| View { role, dir: d.dir.to_string(), root: d.root.to_string(), write: d.write }) }) .collect() } +/// `/system`, read-only: in every view. +fn system() -> View { + let [only] = role_dirs(SYSTEM).expect("SYSTEM is a role") else { panic!("SYSTEM serves one directory") }; + View { role: SYSTEM, dir: only.dir.to_string(), root: only.root.to_string(), write: false } +} + /// The DATA directory `dir`, beneath one DATA serves. fn data_dir(dir: String, write: bool) -> View { let role = "data"; @@ -229,10 +243,46 @@ pub fn syscap_rights(names: &[String]) -> Result { Ok(rights) } +/// What a row's process runs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Source { + /// The executable at this path: a file of `/system`, or an installed + /// package's. + Path(String), + /// The file server's boot image, which the kernel hands the supervisor + /// (`toyos_abi::boot::BootImage`): what serves `/system` runs from no file. + BootFileServer, +} + +impl Default for Source { + fn default() -> Self { + Self::Path(String::new()) + } +} + +impl core::fmt::Display for Source { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Path(path) => f.write_str(path), + Self::BootFileServer => f.write_str("the file server's boot image"), + } + } +} + +impl Source { + /// The executable's path; `None` for a boot image. + pub fn path(&self) -> Option<&str> { + match self { + Self::Path(path) => Some(path), + Self::BootFileServer => None, + } + } +} + #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Program { pub name: String, - pub path: String, + pub source: Source, pub args: Vec, /// Machine-wide ports the supervisor creates and endows the **acceptor** of. pub serves: Vec, @@ -293,7 +343,7 @@ impl Program { /// The installed package this row launches: a row [`Manifest::app_row`] /// made. pub fn package(&self) -> Option<&str> { - package::package_of(&self.path) + package::package_of(self.source.path()?) } /// The `HOME` the supervisor starts this row with. A location grants nothing: what @@ -307,9 +357,9 @@ impl Program { } /// The directories this row's program is endowed. **An installed package - /// sees its own directory read-only, its own folder of the home - /// read-write and the folder its launch was granted, and nothing else any - /// role serves**: no other package, no other part of the home, no + /// sees `/system` and its own directory read-only, its own folder of the + /// home read-write and the folder its launch was granted, and nothing else + /// any role serves**: no other package, no other part of the home, no /// `/config`, `/state`, `/log` or `/boot`. /// Every other row sees the whole tree, until each declares its own /// (`issues/every-program-sees-only-the-files-it-was-given.md`, stage 2). @@ -317,7 +367,10 @@ impl Program { match self.package() { Some(name) => { let granted = self.folder.iter().map(|f| data_dir(f.path.clone(), f.access == grants::Access::ReadWrite)); - [data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)].into_iter().chain(granted).collect() + [system(), data_dir(package::Package::dir(name), false), data_dir(app_home(name), true)] + .into_iter() + .chain(granted) + .collect() } None => whole_tree(), } @@ -362,7 +415,7 @@ impl Manifest { } Ok(Program { name: name.to_string(), - path: program.to_string(), + source: Source::Path(program.to_string()), receives: self.apps.clone(), ..Program::default() }) @@ -423,11 +476,16 @@ pub fn render(manifest: &Manifest) -> Result, RenderError> { return Err(RenderError::NameTooLong(program.name.clone())); } check(&program.name, "name", &program.name)?; - check(&program.name, "path", &program.path)?; if !program.serves.is_empty() && !program.service { return Err(RenderError::ServesWithoutService(program.name.clone())); } - out.push_str(&format!("program {} {}\n", program.name, program.path)); + match &program.source { + Source::Path(path) => { + check(&program.name, "path", path)?; + out.push_str(&format!("program {} {path}\n", program.name)); + } + Source::BootFileServer => out.push_str(&format!("boot-program {}\n", program.name)), + } for arg in &program.args { reject_newline(&program.name, "args", arg)?; out.push_str(&format!("arg {arg}\n")); @@ -577,7 +635,18 @@ pub fn parse(text: &str) -> Manifest { ); manifest.programs.push(Program { name: name.to_string(), - path: path.to_string(), + source: Source::Path(path.to_string()), + ..Program::default() + }); + } + "boot-program" => { + assert!( + rest.len() <= MAX_PROGRAM_NAME, + "manifest: program name longer than {MAX_PROGRAM_NAME}: {rest}" + ); + manifest.programs.push(Program { + name: rest.to_string(), + source: Source::BootFileServer, ..Program::default() }); } @@ -625,7 +694,7 @@ mod tests { programs: vec![ Program { name: "compositor".into(), - path: "/system/bin/compositor".into(), + source: Source::Path("/system/bin/compositor".into()), serves: vec!["compositor".into()], receives: vec!["soundserver".into()], devices: vec!["framebuffer".into(), "acpi".into()], @@ -636,7 +705,7 @@ mod tests { }, Program { name: "soundserver".into(), - path: "/system/bin/soundserver".into(), + source: Source::Path("/system/bin/soundserver".into()), serves: vec!["soundserver".into()], devices: vec!["pci:04:01:00/1af4:1059".into()], syscap: vec!["rt".into()], @@ -645,13 +714,13 @@ mod tests { }, Program { name: "update".into(), - path: "/system/bin/update".into(), + source: Source::Path("/system/bin/update".into()), slots: true, ..Program::default() }, Program { name: "fileserver".into(), - path: "/system/bin/fileserver".into(), + source: Source::BootFileServer, receives: vec!["block".into()], roles: vec!["data".into(), "log".into()], restart: true, @@ -659,14 +728,14 @@ mod tests { }, Program { name: "vdagent".into(), - path: "/system/bin/vdagent".into(), + source: Source::Path("/system/bin/vdagent".into()), receives: vec!["clipboard".into()], drives: vec!["pci:07:80:00/1af4:1043".into()], ..Program::default() }, Program { name: "terminal".into(), - path: "/system/bin/terminal".into(), + source: Source::Path("/system/bin/terminal".into()), args: vec!["--login shell".into()], provides: vec!["surface".into()], receives: vec!["compositor".into()], @@ -690,7 +759,7 @@ mod tests { assert!(row.syscap.is_empty()); assert!(row.serves.is_empty()); assert!(row.provides.is_empty()); - assert_eq!(row.path, "/apps/gbae/gbae"); + assert_eq!(row.source.path(), Some("/apps/gbae/gbae")); } /// The one property both halves depend on, and the reason they live here. @@ -757,15 +826,22 @@ mod tests { row.view().into_iter().map(|v| (v.role, v.dir, v.root, v.write)).collect() } - /// **An installed package sees its own directory read-only and its own - /// folder read-write, and nothing else any role serves.** Spelled out - /// whole, so a third directory, a wider root or a writable package is red. + /// `/system`, read-only, as every view holds it. + fn system() -> (&'static str, String, String, bool) { + (SYSTEM, "/system".into(), String::new(), false) + } + + /// **An installed package sees `/system` and its own directory read-only + /// and its own folder read-write, and nothing else any role serves.** + /// Spelled out whole, so a fourth directory, a wider root or a writable + /// package is red. #[test] fn a_package_s_view_is_its_own_directory_read_only_and_its_own_folder() { let row = sample().app_row("gbae", "/apps/gbae/gbae").unwrap(); assert_eq!( views(&row), [ + system(), ("data", "/apps/gbae".into(), "apps/gbae".into(), false), ("data", "/home/toy/Apps/gbae".into(), "home/toy/Apps/gbae".into(), true), ] @@ -774,7 +850,7 @@ mod tests { let longest = "n".repeat(MAX_PROGRAM_NAME); let row = sample().app_row(&longest, &format!("/apps/{longest}/{longest}")).unwrap(); assert_eq!( - views(&row).into_iter().map(|(_, _, root, write)| (root, write)).collect::>(), + views(&row).into_iter().skip(1).map(|(_, _, root, write)| (root, write)).collect::>(), [(format!("apps/{longest}"), false), (format!("home/toy/Apps/{longest}"), true)] ); } @@ -789,6 +865,7 @@ mod tests { assert_eq!( views(&row), [ + system(), ("data", "/apps/gbae".into(), "apps/gbae".into(), false), ("data", "/home/toy/Apps/gbae".into(), "home/toy/Apps/gbae".into(), true), ("data", "/home/toy/Games".into(), "home/toy/Games".into(), write), @@ -797,17 +874,20 @@ mod tests { } } - /// Every row the image declares sees the whole tree read-write, the - /// installer and the shell included: neither has authority over `/apps` - /// the other lacks. + /// Every row the image declares sees the whole tree read-write but + /// `/system`, the installer and the shell included: neither has authority + /// over `/apps` the other lacks, and nobody writes the signed image. #[test] fn every_declared_row_sees_the_whole_tree_read_write() { let m = parse("program pkg /system/bin/pkg\nprogram shell /system/bin/shell\n"); - let whole: Vec<_> = ["/apps", "/config", "/home", "/state", "/log", "/boot"] - .iter() - .zip(["apps", "config", "home", "state", "", ""]) - .zip(["data", "data", "data", "data", "log", "boot"]) - .map(|((dir, root), role)| (role, dir.to_string(), root.to_string(), true)) + let whole: Vec<_> = std::iter::once(system()) + .chain( + ["/apps", "/config", "/home", "/state", "/log", "/boot"] + .iter() + .zip(["apps", "config", "home", "state", "", ""]) + .zip(["data", "data", "data", "data", "log", "boot"]) + .map(|((dir, root), role)| (role, dir.to_string(), root.to_string(), true)), + ) .collect(); let s = sample(); for row in [m.program("pkg").unwrap(), m.program("shell").unwrap(), s.program("compositor").unwrap()] { @@ -933,7 +1013,7 @@ mod tests { .iter() .map(|(name, devices, drives)| Program { name: name.to_string(), - path: format!("/system/bin/{name}"), + source: Source::Path(format!("/system/bin/{name}")), devices: devices.iter().map(|d| d.to_string()).collect(), drives: drives.iter().map(|d| d.to_string()).collect(), ..Program::default() diff --git a/lib/quiesce/src/lib.rs b/lib/quiesce/src/lib.rs index 20ac4949bb2..1e83dd25284 100644 --- a/lib/quiesce/src/lib.rs +++ b/lib/quiesce/src/lib.rs @@ -7,7 +7,7 @@ //! here; `kernel/src/quiesce.rs` marks the threads and spends the time. //! //! **Nothing is carved out.** The log's writer stops with every other thread: -//! `/system/bin/supervisor` has it flush before it asks for the stop, and what the +//! the supervisor has it flush before it asks for the stop, and what the //! kernel says after that goes to its console and its black box, never to a //! process it would have to keep running. //! diff --git a/lib/rootimage/src/handoff.rs b/lib/rootimage/src/handoff.rs index 0b0c3270849..306be2e5df6 100644 --- a/lib/rootimage/src/handoff.rs +++ b/lib/rootimage/src/handoff.rs @@ -1,7 +1,8 @@ -//! Whether the extent the loader names as ROOT's image is memory the kernel -//! may keep: whole blocks, every byte of it inside one descriptor of the type -//! the loader allocated it as. The kernel then keeps it out of its allocator -//! by address, as it keeps the black box's page. +//! Whether an extent the loader hands the kernel is memory the kernel may +//! keep: whole blocks, every byte of it inside one descriptor of the type the +//! loader allocated it as, and no byte of it another handed extent's. The +//! kernel then keeps it out of its allocator by address, as it keeps the black +//! box's page. /// `[start, end)` of UEFI memory type `ty`, as a memory map describes it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -32,3 +33,12 @@ pub fn held( .any(|entry| entry.ty == ty && entry.start <= at && end <= entry.end) .then_some(at..end) } + +/// The first two of `extents` that share a byte, by index; `None` when no two +/// do. An empty extent shares nothing. +pub fn overlapping(extents: &[core::ops::Range]) -> Option<(usize, usize)> { + let shares = |a: &core::ops::Range, b: &core::ops::Range| { + !a.is_empty() && !b.is_empty() && a.start < b.end && b.start < a.end + }; + (0..extents.len()).find_map(|i| (i + 1..extents.len()).find(|&j| shares(&extents[i], &extents[j])).map(|j| (i, j))) +} diff --git a/lib/rootimage/src/lib.rs b/lib/rootimage/src/lib.rs index d5daa0f5616..ef50816e39e 100644 --- a/lib/rootimage/src/lib.rs +++ b/lib/rootimage/src/lib.rs @@ -1,7 +1,8 @@ -//! ROOT in memory: every decision the loader and the kernel make about the -//! image, pure. The loader reads the partition its slot names in bounded -//! [`chunk`]s; the kernel keeps the extent it is handed only once [`handoff`] -//! finds it inside memory the loader allocated. +//! ROOT and the boot programs in memory: every decision the loader and the +//! kernel make about the images, pure. The loader reads the partition its slot +//! names in bounded [`chunk`]s; the kernel keeps each extent it is handed only +//! once [`handoff`] finds it inside memory the loader allocated and no other +//! extent's. //! //! No `alloc`, no firmware, no `unsafe`: the loader and the kernel supply the //! device, the candidates and the map. diff --git a/lib/rootimage/tests/handoff.rs b/lib/rootimage/tests/handoff.rs index 774a572a551..e489151bbd0 100644 --- a/lib/rootimage/tests/handoff.rs +++ b/lib/rootimage/tests/handoff.rs @@ -1,7 +1,7 @@ //! The handoff's bounds: an extent is kept only when it is whole blocks inside //! one descriptor of the loader's type, and each edge of that is its own test. -use toyos_rootimage::handoff::{held, Descriptor}; +use toyos_rootimage::handoff::{held, overlapping, Descriptor}; const PAGE: u64 = 4096; const LOADER_DATA: u32 = 2; @@ -63,3 +63,16 @@ fn a_start_that_is_not_block_aligned_is_refused() { fn an_extent_inside_a_descriptor_of_another_type_is_refused() { assert_eq!(held(map(), LOADER_DATA, 2 * PAGE, 4 * PAGE, PAGE), None); } + +/// Two handed extents that share a byte are named by index, whichever end +/// overlaps; ones that only touch, or are empty, share nothing. +#[test] +fn extents_that_share_a_byte_are_named() { + let (a, b, c) = (16 * PAGE..20 * PAGE, 20 * PAGE..24 * PAGE, 30 * PAGE..32 * PAGE); + assert_eq!(overlapping(&[a.clone(), b.clone(), c.clone()]), None, "touching extents share nothing"); + assert_eq!(overlapping(&[a.clone(), b, 23 * PAGE..31 * PAGE]), Some((1, 2))); + assert_eq!(overlapping(&[c, a.clone(), 31 * PAGE..33 * PAGE]), Some((0, 2))); + assert_eq!(overlapping(&[a.clone(), 17 * PAGE..18 * PAGE]), Some((0, 1)), "one inside another"); + assert_eq!(overlapping(&[a.clone(), a.clone()]), Some((0, 1)), "the same extent twice"); + assert_eq!(overlapping(&[a, 18 * PAGE..18 * PAGE]), None, "an empty extent"); +} diff --git a/lib/swap/src/lib.rs b/lib/swap/src/lib.rs index 3b92967f7f1..bb57a626403 100644 --- a/lib/swap/src/lib.rs +++ b/lib/swap/src/lib.rs @@ -1,5 +1,5 @@ //! Replacing a running service's binary: every word `/system/bin/swap` and -//! `/system/bin/supervisor` say about it, and every decision the supervisor makes +//! the supervisor say about it, and every decision the supervisor makes //! about one. Pure. //! //! **A swap is asked by `/system/bin/swap` and done by the supervisor, and nothing else diff --git a/lib/update/src/image.rs b/lib/update/src/image.rs index 14b04330199..321531cb55a 100644 --- a/lib/update/src/image.rs +++ b/lib/update/src/image.rs @@ -10,9 +10,11 @@ //! Little-endian throughout. **The sections are exactly [`SECTIONS`], in that //! order**, and a header naming anything else is refused rather than read //! around: a loader that skipped a section it did not know would boot bytes -//! nothing vouched for. The header is the whole of what is signed, so a -//! section's bytes are judged by its hash and never by being signed -//! themselves — which is what lets a later format fetch them as +//! nothing vouched for. A section's name is its role, so one version binds the +//! kernel, its parameter, the two programs the kernel starts the machine with +//! and ROOT, each by role, length and hash. The header is the whole of what is +//! signed, so a section's bytes are judged by its hash and never by being +//! signed themselves — which is what lets a later format fetch them as //! content-addressed chunks under this same header. use crate::Digest; @@ -21,17 +23,22 @@ use crate::Digest; pub const MAGIC: [u8; 8] = *b"TOYOSIMG"; /// This layout. A header of any other format is refused by name. -pub const FORMAT: u32 = 1; +pub const FORMAT: u32 = 2; /// The kernel ELF. pub const KERNEL: &str = "kernel"; /// The boot parameter the loader hands the kernel. pub const CMDLINE: &str = "cmdline"; +/// The supervisor, the one program the kernel starts. +pub const SUPERVISOR: &str = "supervisor"; +/// The file server the supervisor serves ROOT with before it reads a file. +pub const FILESERVER: &str = "fileserver"; /// ROOT's filesystem, whole. pub const ROOT: &str = "root"; -/// Every section an image carries, in the order it carries them. -pub const SECTIONS: [&str; 3] = [KERNEL, CMDLINE, ROOT]; +/// Every section an image carries, in the order it carries them. ROOT is last, +/// so an update streams it after every other section is held. +pub const SECTIONS: [&str; 5] = [KERNEL, CMDLINE, SUPERVISOR, FILESERVER, ROOT]; /// Bytes a section's name takes, NUL-padded. const NAME_BYTES: usize = 16; @@ -50,8 +57,9 @@ pub const SIGNED_BYTES: usize = HEADER_BYTES + SIGNATURE_BYTES; /// The largest section of each kind an image may carry, so a header is a /// bound on what a reader allocates before a single hash is checked. The /// kernel's is the loader's own bound on an ESP file; ROOT's is well above -/// any ROOT this tree builds and below what a slot is. -pub const MAX_BYTES: [u64; 3] = [1 << 30, 4096, 4 << 30]; +/// any ROOT this tree builds and below what a slot is; each program's is far +/// above what this tree builds (2 MiB) and a bound on the pages the kernel keeps. +pub const MAX_BYTES: [u64; 5] = [1 << 30, 4096, 64 << 20, 64 << 20, 4 << 30]; /// ROOT is whole 4 KiB blocks: it is written to a partition and read back in them. pub const ROOT_BLOCK: u64 = 4096; @@ -69,7 +77,7 @@ pub struct Section { pub struct Header { pub version: u64, /// In [`SECTIONS`] order. - pub sections: [Section; 3], + pub sections: [Section; 5], } /// Why bytes are not an image. @@ -84,6 +92,8 @@ pub enum Malformed { SectionName(usize), /// The section at this index is longer than [`MAX_BYTES`] allows. TooLong(usize, u64), + /// The program section at this index is empty. + Empty(usize), /// ROOT is not whole [`ROOT_BLOCK`]s, or is empty. RootNotBlocks(u64), /// The image's bytes do not add up to the header's sections. @@ -99,6 +109,7 @@ impl core::fmt::Display for Malformed { Self::SectionCount(n) => write!(f, "the header names {n} sections, and an image has {}", SECTIONS.len()), Self::SectionName(i) => write!(f, "section {i} is not `{}`", SECTIONS[i]), Self::TooLong(i, len) => write!(f, "the `{}` section is {len} bytes, past its bound of {}", SECTIONS[i], MAX_BYTES[i]), + Self::Empty(i) => write!(f, "the `{}` section is empty", SECTIONS[i]), Self::RootNotBlocks(len) => write!(f, "ROOT is {len} bytes, which is not whole {ROOT_BLOCK}-byte blocks"), Self::Length { have, want } => write!(f, "the image is {have} bytes and its header adds up to {want}"), } @@ -139,7 +150,7 @@ impl Header { return Err(Malformed::SectionCount(count)); } let version = u64_at(bytes, 16); - let mut sections = [Section { len: 0, sha256: [0; 32] }; 3]; + let mut sections = [Section { len: 0, sha256: [0; 32] }; 5]; for (i, section) in sections.iter_mut().enumerate() { let at = FIXED_BYTES + i * ENTRY_BYTES; if bytes[at..at + NAME_BYTES] != padded(SECTIONS[i]) { @@ -152,7 +163,12 @@ impl Header { section.len = len; section.sha256.copy_from_slice(&bytes[at + NAME_BYTES + 8..at + ENTRY_BYTES]); } - let root = sections[2].len; + for program in [2, 3] { + if sections[program].len == 0 { + return Err(Malformed::Empty(program)); + } + } + let root = sections[4].len; if root == 0 || !root.is_multiple_of(ROOT_BLOCK) { return Err(Malformed::RootNotBlocks(root)); } @@ -176,9 +192,8 @@ impl Header { } /// The header naming exactly these sections' bytes. - pub fn of(version: u64, kernel: &[u8], cmdline: &[u8], root: &[u8]) -> Self { - let section = |bytes: &[u8]| Section { len: bytes.len() as u64, sha256: crate::sha256(bytes) }; - Self { version, sections: [section(kernel), section(cmdline), section(root)] } + pub fn of(version: u64, sections: &Sections<'_>) -> Self { + Self { version, sections: sections.all().map(|bytes| Section { len: bytes.len() as u64, sha256: crate::sha256(bytes) }) } } pub fn kernel(&self) -> Section { @@ -189,10 +204,18 @@ impl Header { self.sections[1] } - pub fn root(&self) -> Section { + pub fn supervisor(&self) -> Section { self.sections[2] } + pub fn fileserver(&self) -> Section { + self.sections[3] + } + + pub fn root(&self) -> Section { + self.sections[4] + } + /// The bytes the sections take after the signed header. pub fn payload_len(&self) -> u64 { self.sections.iter().map(|s| s.len).sum() @@ -204,15 +227,30 @@ pub fn signature_of(signed: &[u8; SIGNED_BYTES]) -> [u8; SIGNATURE_BYTES] { signed[HEADER_BYTES..].try_into().expect("a signed header ends in a signature") } +/// Each section's bytes, by role. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Sections<'a> { + pub kernel: &'a [u8], + pub cmdline: &'a [u8], + pub supervisor: &'a [u8], + pub fileserver: &'a [u8], + pub root: &'a [u8], +} + +impl<'a> Sections<'a> { + /// In [`SECTIONS`] order. + pub fn all(&self) -> [&'a [u8]; 5] { + [self.kernel, self.cmdline, self.supervisor, self.fileserver, self.root] + } +} + /// An image whose layout adds up, split into its parts. Nothing here is /// vouched for until the header's signature ([`crate::sig::verify`]) and /// each section's hash against its header entry are checked. pub struct Parts<'a> { pub header: Header, pub signed: &'a [u8; SIGNED_BYTES], - pub kernel: &'a [u8], - pub cmdline: &'a [u8], - pub root: &'a [u8], + pub sections: Sections<'a>, } impl<'a> Parts<'a> { @@ -224,9 +262,14 @@ impl<'a> Parts<'a> { return Err(Malformed::Length { have: bytes.len() as u64, want }); } let signed: &[u8; SIGNED_BYTES] = bytes[..SIGNED_BYTES].try_into().expect("checked above"); - let (k, c) = (header.kernel().len as usize, header.cmdline().len as usize); - let rest = &bytes[SIGNED_BYTES..]; - Ok(Self { header, signed, kernel: &rest[..k], cmdline: &rest[k..k + c], root: &rest[k + c..] }) + let mut rest = &bytes[SIGNED_BYTES..]; + let mut take = |section: Section| { + let (bytes, after) = rest.split_at(section.len as usize); + rest = after; + bytes + }; + let [kernel, cmdline, supervisor, fileserver, root] = header.sections.map(&mut take); + Ok(Self { header, signed, sections: Sections { kernel, cmdline, supervisor, fileserver, root } }) } } @@ -234,8 +277,20 @@ impl<'a> Parts<'a> { mod tests { use super::*; + const ROOT_BYTES: [u8; 8192] = [0x5A; 8192]; + + fn sections() -> Sections<'static> { + Sections { + kernel: b"\x7fELF kernel", + cmdline: b"slot=A", + supervisor: b"\x7fELF supervisor", + fileserver: b"\x7fELF fileserver", + root: &ROOT_BYTES, + } + } + fn header() -> Header { - Header::of(7, b"\x7fELF kernel", b"root=x", &[0x5A; 8192]) + Header::of(7, §ions()) } fn signed(header: &Header) -> [u8; SIGNED_BYTES] { @@ -244,11 +299,18 @@ mod tests { out } + /// The entry of section `i`: where its name, then its length, begin. + fn entry(i: usize) -> usize { + FIXED_BYTES + i * ENTRY_BYTES + } + #[test] fn a_header_reads_back_as_it_was_written() { let h = header(); assert_eq!(Header::parse(&signed(&h)), Ok(h)); - assert_eq!(HEADER_BYTES, 192); + assert_eq!(HEADER_BYTES, 304); + assert_eq!(h.supervisor().sha256, crate::sha256(b"\x7fELF supervisor")); + assert_eq!(h.fileserver().len, 15); } /// Every field a reader decides on, bent one at a time, is refused by the @@ -263,28 +325,44 @@ mod tests { }; assert_eq!(Header::parse(&good[..SIGNED_BYTES - 1]), Err(Malformed::Short(SIGNED_BYTES - 1))); assert_eq!(bend(0, b"TOYOSIMH"), Err(Malformed::Magic)); - assert_eq!(bend(8, &2u32.to_le_bytes()), Err(Malformed::Format(2))); - assert_eq!(bend(12, &4u32.to_le_bytes()), Err(Malformed::SectionCount(4))); - assert_eq!(bend(FIXED_BYTES, b"kernal"), Err(Malformed::SectionName(0))); + assert_eq!(bend(8, &1u32.to_le_bytes()), Err(Malformed::Format(1))); + assert_eq!(bend(12, &3u32.to_le_bytes()), Err(Malformed::SectionCount(3))); + assert_eq!(bend(entry(0), b"kernal"), Err(Malformed::SectionName(0))); // The name is the whole padded field, so a longer one is not a prefix match. - assert_eq!(bend(FIXED_BYTES + ENTRY_BYTES + 7, b"x"), Err(Malformed::SectionName(1))); - let at = FIXED_BYTES + ENTRY_BYTES + NAME_BYTES; - assert_eq!(bend(at, &4097u64.to_le_bytes()), Err(Malformed::TooLong(1, 4097))); - let root = FIXED_BYTES + 2 * ENTRY_BYTES + NAME_BYTES; - assert_eq!(bend(root, &4095u64.to_le_bytes()), Err(Malformed::RootNotBlocks(4095))); - assert_eq!(bend(root, &0u64.to_le_bytes()), Err(Malformed::RootNotBlocks(0))); + assert_eq!(bend(entry(1) + 7, b"x"), Err(Malformed::SectionName(1))); + assert_eq!(bend(entry(1) + NAME_BYTES, &4097u64.to_le_bytes()), Err(Malformed::TooLong(1, 4097))); + let len = |i| entry(i) + NAME_BYTES; + assert_eq!(bend(len(4), &4095u64.to_le_bytes()), Err(Malformed::RootNotBlocks(4095))); + assert_eq!(bend(len(4), &0u64.to_le_bytes()), Err(Malformed::RootNotBlocks(0))); + for program in [2, 3] { + assert_eq!(bend(len(program), &0u64.to_le_bytes()), Err(Malformed::Empty(program))); + let past = MAX_BYTES[program] + 1; + assert_eq!(bend(len(program), &past.to_le_bytes()), Err(Malformed::TooLong(program, past))); + } + } + + /// **A role is its place**: the two programs' entries swapped are refused + /// by name, so a header cannot hand the kernel the file server as the + /// supervisor. + #[test] + fn two_programs_swapped_are_refused() { + let good = signed(&header()); + let mut swapped = good; + swapped[entry(2)..entry(3)].copy_from_slice(&good[entry(3)..entry(4)]); + swapped[entry(3)..entry(4)].copy_from_slice(&good[entry(2)..entry(3)]); + assert_eq!(Header::parse(&swapped), Err(Malformed::SectionName(2))); } #[test] fn an_image_splits_into_exactly_its_sections() { - let (kernel, cmdline, root) = (b"\x7fELF kernel".to_vec(), b"root=x".to_vec(), vec![0x5A; 8192]); - let h = Header::of(7, &kernel, &cmdline, &root); + let s = sections(); + let h = Header::of(7, &s); let mut bytes = signed(&h).to_vec(); - bytes.extend_from_slice(&kernel); - bytes.extend_from_slice(&cmdline); - bytes.extend_from_slice(&root); + for section in s.all() { + bytes.extend_from_slice(section); + } let parts = Parts::split(&bytes).expect("an image"); - assert_eq!((parts.kernel, parts.cmdline, parts.root), (&kernel[..], &cmdline[..], &root[..])); + assert_eq!(parts.sections, s); let want = bytes.len() as u64; bytes.push(0); assert_eq!(Parts::split(&bytes).err(), Some(Malformed::Length { have: want + 1, want })); diff --git a/lib/update/src/lib.rs b/lib/update/src/lib.rs index 3ede32a18fc..f9c304975c0 100644 --- a/lib/update/src/lib.rs +++ b/lib/update/src/lib.rs @@ -4,8 +4,8 @@ //! about it. Pure: no firmware, no device, and no allocation but [`repo`]'s. //! //! **The contract.** An [`image`] is a header naming a monotonic version and -//! the SHA-256 of each of its sections — the kernel, its boot parameter and -//! ROOT — followed by an Ed25519 signature over that header, and then the +//! the SHA-256 of each of its sections — the kernel, its boot parameter, the +//! supervisor, the file server that serves ROOT, and ROOT — followed by an Ed25519 signature over that header, and then the //! sections. The signature covers the hashes and never the bytes, so the //! bytes may arrive by any path and in any order: an `ssh … update < image` //! today, a pull of content-addressed chunks tomorrow, the same signature over @@ -14,7 +14,7 @@ //! ordinary Ed25519 key and a second implementation can sign or check an image. //! //! A machine carries two [`slots`], each a FAT partition holding the kernel, -//! its boot parameter and the signed header, and a ROOT partition holding +//! its boot parameter, the two programs and the signed header, and a ROOT partition holding //! ROOT; the slot table names both slots and marks one. The loader boots the //! marked slot, and falls back to the other where the marked one is refused or //! died on its last boot ([`record`]); the updater writes only the slot the diff --git a/lib/update/src/slots.rs b/lib/update/src/slots.rs index 97ffeed8616..07a45754bbb 100644 --- a/lib/update/src/slots.rs +++ b/lib/update/src/slots.rs @@ -31,10 +31,12 @@ const BODY_BYTES: usize = 8 + 4 + 4 + 8 + 2 * SLOT_BYTES; /// A copy's bytes, checksum included; the rest of its block is zero. pub const TABLE_BYTES: usize = BODY_BYTES + 4; -/// Where each slot's kernel, boot parameter and signed header are on its FAT -/// partition, as paths from the volume's root. +/// Where each slot's kernel, boot parameter, two programs and signed header +/// are on its FAT partition, as paths from the volume's root. pub const KERNEL_FILE: &str = "toyos/kernel.elf"; pub const CMDLINE_FILE: &str = "toyos/cmdline"; +pub const SUPERVISOR_FILE: &str = "toyos/supervisor.elf"; +pub const FILESERVER_FILE: &str = "toyos/fileserver.elf"; pub const SIGNED_FILE: &str = "toyos/image.sig"; /// A slot's two partitions, by their unique GUIDs as a GPT entry stores them. @@ -217,7 +219,7 @@ pub fn next_write(current: (Table, usize), mut next: Table) -> (usize, [u8; BLOC (1 - current.1, next.encode()) } -/// The labels `/system/bin/supervisor` endows a `slots` grant under, and +/// The labels the supervisor endows a `slots` grant under, and /// `/system/bin/update` takes it by: the slot table's partition, and the idle /// slot's FAT volume and ROOT. pub const TABLE_LABEL: &str = "slots:table"; diff --git a/lib/update/tests/fixtures/ssh-keygen-signed-header.sig b/lib/update/tests/fixtures/ssh-keygen-signed-header.sig index 28f128ee2ae..4b4fee33830 100644 --- a/lib/update/tests/fixtures/ssh-keygen-signed-header.sig +++ b/lib/update/tests/fixtures/ssh-keygen-signed-header.sig @@ -1,6 +1,6 @@ -----BEGIN SSH SIGNATURE----- -U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgWDeuq6ibeR5EIpzG6ISSbfZh1g -6o8enc39xZTEnS/7IAAAALdG95b3MtaW1hZ2UAAAAAAAAABnNoYTUxMgAAAFMAAAALc3No -LWVkMjU1MTkAAABAscB9wiZe8D62StbkOswkgTxduwAmOj2jSGN3IAtZddlG8sBiLjtQ0E -SIarUv0tfed85GrCQitXdLzQ0a4f58Bg== +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgkB4U+wgKrR3HDyW75PXkzvD/rC +Zuo/RqJaq8IjoNP68AAAALdG95b3MtaW1hZ2UAAAAAAAAABnNoYTUxMgAAAFMAAAALc3No +LWVkMjU1MTkAAABAMPXcS8H94CSimDN3du917D2kZsQW5RWZqVwEly0162x6En7sclRkhu +geXFOW1vmQucXblPGXWM1NDBb5yHEbAg== -----END SSH SIGNATURE----- diff --git a/lib/update/tests/fixtures/ssh-keygen-signer.pub b/lib/update/tests/fixtures/ssh-keygen-signer.pub index ab01549eb03..9f8816db1fb 100644 --- a/lib/update/tests/fixtures/ssh-keygen-signer.pub +++ b/lib/update/tests/fixtures/ssh-keygen-signer.pub @@ -1 +1 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFg3rquom3keRCKcxuiEkm32YdYOqPHp3N/cWUxJ0v+y toyos-image-oracle +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJAeFPsICq0dxw8lu+T15M7w/6wmbqP0aiWqvCI6DT+v toyos-image-oracle diff --git a/lib/update/tests/sshsig_oracle.rs b/lib/update/tests/sshsig_oracle.rs index 369e942c483..35701cb8159 100644 --- a/lib/update/tests/sshsig_oracle.rs +++ b/lib/update/tests/sshsig_oracle.rs @@ -9,7 +9,7 @@ //! the fixture is a signature over bytes this crate writes, not over a file it //! only carries. Nothing runs `ssh-keygen` at test time. -use toyos_update::image::{Header, HEADER_BYTES}; +use toyos_update::image::{Header, Sections, HEADER_BYTES}; const SIGNATURE: &str = include_str!("fixtures/ssh-keygen-signed-header.sig"); const PUBLIC: &str = include_str!("fixtures/ssh-keygen-signer.pub"); @@ -18,9 +18,13 @@ const PUBLIC: &str = include_str!("fixtures/ssh-keygen-signer.pub"); fn oracle_header() -> [u8; HEADER_BYTES] { Header::of( 20260926, - b"\x7fELF oracle kernel", - b"root=00000000-0000-0000-0000-000000000000", - &[0x5A; 4096], + &Sections { + kernel: b"\x7fELF oracle kernel", + cmdline: b"slot=A", + supervisor: b"\x7fELF oracle supervisor", + fileserver: b"\x7fELF oracle fileserver", + root: &[0x5A; 4096], + }, ) .encode() } diff --git a/loader/src/main.rs b/loader/src/main.rs index ed4e7a76719..bfb5eee8bf7 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -36,6 +36,7 @@ mod floor; mod gcd; mod loaderlog; mod microcode; +mod payload; mod rootbridge; mod rootimage; mod seed; @@ -532,8 +533,15 @@ fn report_reach(what: &str, at: u64, len: u64) { ); } +/// What the chosen slot hands the kernel beside its ELF and parameter. +struct Handed { + root: rootimage::RootImage, + supervisor: payload::Payload, + fileserver: payload::Payload, +} + #[allow(clippy::too_many_arguments)] -fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, slot_volume_guid: [u8; 16], microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! { +fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], handed: Handed, slot_volume_guid: [u8; 16], microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! { // Said before it is refused, for `report_reach`'s reason. match arch::cpu_as_entered() { Ok(None) => {} @@ -628,7 +636,9 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v None => ([0u8; 16], 0, 0, 0), }; - let (root_image_addr, root_image_len, root_partition_guid, root_read_ticks) = root_image.handoff(); + let root_image = handed.root.pages.handoff(); + let (root_partition_guid, root_read_ticks) = (handed.root.partition, handed.root.ticks); + let (supervisor_image, fileserver_image) = (handed.supervisor.handoff(), handed.fileserver.handoff()); // The BSP loads it before `mm::init`, through the boot map. let (microcode_addr, microcode_len) = microcode.map_or((0, 0), |patch| (patch.as_ptr() as u64, patch.len() as u64)); @@ -667,8 +677,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v cmdline_len: cmdline.len() as u64, root_bridge_window_count, root_bridge_windows, - root_image_addr, - root_image_len, + root_image, root_partition_guid, loader_entry_counter: entry_counter, loader_handoff_counter: 0, @@ -684,6 +693,8 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v microcode_addr, microcode_len, slot_volume_guid, + supervisor_image, + fileserver_image, }; if let Some(edid) = gop.as_ref().and_then(|g| g.edid) { kernel_args.gop_edid = edid; @@ -944,8 +955,8 @@ fn main(handle: Handle, system_table: SystemTable) -> Status { println!("Log partition: signature {:02x?}", log_guid); - // Every byte the kernel is handed below — its ELF, its parameter and ROOT — - // is one the chosen slot's signed header names. ROOT is read before the + // Every byte the kernel is handed below — its ELF, its parameter, the two + // programs and ROOT — is one the chosen slot's signed header names. ROOT is read before the // kernel is loaded and not after: it is the allocation the image pages come // from, and a slot whose ROOT is refused has no use for the kernel's. let chosen = slot::choose(handle, &system_table, image_floor.value, &record) @@ -985,7 +996,7 @@ fn main(handle: Handle, system_table: SystemTable) -> Status { println!("Loading kernel elf..."); let loaded_kernel = load_kernel_elf(&kernel_bytes); - let microcode = microcode::find(system_table.boot_services(), &chosen.root); + let microcode = microcode::find(system_table.boot_services(), &chosen.root.pages); // Query UEFI GOP before exiting boot services let gop = query_gop(&system_table); @@ -1014,5 +1025,5 @@ fn main(handle: Handle, system_table: SystemTable) -> Status { watchdog::arm(&system_table, rsdp_addr, params); println!("Starting kernel..."); - start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, chosen.volume, microcode, entry_counter, wall_clock, system_table); + start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, Handed { root: chosen.root, supervisor: chosen.supervisor, fileserver: chosen.fileserver }, chosen.volume, microcode, entry_counter, wall_clock, system_table); } diff --git a/loader/src/microcode.rs b/loader/src/microcode.rs index f4d76b56d8d..1a6373381ca 100644 --- a/loader/src/microcode.rs +++ b/loader/src/microcode.rs @@ -13,14 +13,14 @@ use bcachefs::{BlockBuf, BlockIO, BlockNum, DeviceError, FsError, Mounted, ReadO use toyos_bootmap::BOOT_MAP_BYTES; use crate::efi::{AllocateType, BootServices, PAGE_SIZE}; -use crate::rootimage::RootImage; +use crate::payload::Payload; /// The line every answer here is said on. pub const SAID: &str = "Microcode:"; /// The patch for this CPU, or `None` where ROOT holds none for it or its /// architecture loads none; either way said on [`SAID`]'s line. -pub fn find(bs: &BootServices, root: &RootImage) -> Option<&'static [u8]> { +pub fn find(bs: &BootServices, root: &Payload) -> Option<&'static [u8]> { let cpu = match crate::arch::microcode_cpu()? { Ok(cpu) => cpu, Err(why) => { diff --git a/loader/src/payload.rs b/loader/src/payload.rs new file mode 100644 index 00000000000..e685135c82b --- /dev/null +++ b/loader/src/payload.rs @@ -0,0 +1,63 @@ +//! A section of the slot's signed image in pages of its own, which the kernel +//! keeps out of its allocator by the address it is handed +//! (`toyos_abi::boot::BootImage`). +//! +//! **The contract with the kernel**: the pages are `LoaderData`, whole and +//! page-aligned, the bytes past `len` in the last one are zero, and nothing +//! writes them between the read the slot's hash is checked over and the jump. +//! A check over [`Payload::bytes`] is therefore a check over exactly what the +//! kernel is handed. + +use toyos_abi::boot::BootImage; + +use crate::efi::{AllocateType, BootServices, PAGE_SIZE}; + +/// `len` bytes at the physical address `at`, identity-mapped while boot +/// services live. +pub struct Payload { + at: u64, + len: u64, +} + +impl Payload { + /// Zeroed pages for `len` bytes, `len` non-zero. + pub fn allocate(bs: &BootServices, len: u64) -> Result { + assert!(len != 0, "a payload of no bytes has no pages to hand over"); + let pages = usize::try_from(len.div_ceil(PAGE_SIZE as u64)) + .map_err(|_| alloc::format!("{len} bytes are more pages than this loader can count"))?; + let at = bs + .allocate_pages(AllocateType::AnyPages, pages) + .map_err(|e| alloc::format!("firmware would not give {len} bytes: {e:?}"))?; + // SAFETY: the `pages` pages at `at` were just allocated to this loader, + // are identity-mapped while boot services live, and nothing else holds them. + unsafe { core::ptr::write_bytes(at as *mut u8, 0, pages * PAGE_SIZE) }; + Ok(Self { at, len }) + } + + pub fn bytes(&self) -> &[u8] { + // SAFETY: `at` and `len` are the pages `allocate` took, identity-mapped + // while boot services live, and released only by `free`, which takes + // `self`. + unsafe { core::slice::from_raw_parts(self.at as *const u8, self.len as usize) } + } + + pub fn bytes_mut(&mut self) -> &mut [u8] { + // SAFETY: as `bytes`, and `&mut self` is the one borrow of them. + unsafe { core::slice::from_raw_parts_mut(self.at as *mut u8, self.len as usize) } + } + + /// Give the pages back: a payload whose slot was refused is not the kernel's. + pub fn free(self, bs: &BootServices) { + let pages = self.len.div_ceil(PAGE_SIZE as u64) as usize; + // SAFETY: the pages are this payload's own allocation, and `self` is + // consumed, so nothing reads them after this. + if let Err(e) = unsafe { bs.free_pages(self.at, pages) } { + println!("Payload: firmware would not take back the {} bytes at {:#x} ({e:?})", self.len, self.at); + } + } + + /// Where the kernel is told the payload is. + pub fn handoff(&self) -> BootImage { + BootImage { addr: self.at, len: self.len } + } +} diff --git a/loader/src/rootimage.rs b/loader/src/rootimage.rs index dac9adddfbc..e5a593494af 100644 --- a/loader/src/rootimage.rs +++ b/loader/src/rootimage.rs @@ -1,6 +1,7 @@ //! The boot disk as the loader reads it: its slot table, and a slot's ROOT -//! read whole into memory the kernel keeps, so the kernel mounts `/system` -//! from these bytes and needs no storage driver to boot. +//! read whole into memory the kernel keeps and hands the supervisor, whose +//! file server serves `/system` from these bytes, so no storage driver is +//! needed to boot. //! //! **Which disk** is the one firmware loaded this image from and no other: a //! slot is a pair of partitions on one disk, and a slot table on another disk @@ -10,21 +11,19 @@ //! length, and never a superblock's word: the header's SHA-256 is what those //! bytes are held to (`crate::slot`). //! -//! **The contract with the kernel**: [`RootImage`] is made only by -//! [`Disk::read_root`], its pages are `LoaderData`, which the kernel keeps out -//! of its allocator by the address it is handed, as it keeps the black box's -//! page, and nothing writes them between the read and the jump. A check over -//! the image between the read and [`RootImage::handoff`] is therefore a check -//! over exactly what the kernel mounts. +//! [`RootImage`] is made only by [`Disk::read_root`], and its pages are a +//! [`Payload`], whose contract with the kernel its module states. use alloc::alloc::Layout; use alloc::string::String; use core::num::NonZeroU64; +use crate::payload::Payload; + use toyos_gpt::{Guid, Located, Sectors}; use toyos_rootimage::chunk; use toyos_update::slots::{self, Table}; -use crate::efi::{AllocateType, BlockIo, BootServices, DevicePath, Handle, HardDrive, LoadedImage, Scoped, Status}; +use crate::efi::{BlockIo, BootServices, DevicePath, Handle, HardDrive, LoadedImage, Scoped, Status}; /// The unit ROOT's filesystem is written in, and the alignment every buffer /// here is allocated at. @@ -43,41 +42,13 @@ const CHUNK_BOUND: usize = 1 << 20; /// The line a read ROOT is reported on, with where it went and what it cost. pub const READ_AT: &str = "ROOT: read into memory at"; -/// ROOT in memory: `len` bytes at the physical address `at`, identity-mapped -/// while boot services live. +/// ROOT in memory, and where it was read from. pub struct RootImage { - at: u64, - len: u64, + pub pages: Payload, /// The partition it was read from, raw as in its GPT entry. - partition: [u8; 16], + pub partition: [u8; 16], /// The counter ticks the read took. - ticks: u64, -} - -impl RootImage { - /// Where the kernel is told the image is, which partition it came from, and - /// the counter ticks reading it took. - pub fn handoff(&self) -> (u64, u64, [u8; 16], u64) { - (self.at, self.len, self.partition, self.ticks) - } - - /// The image's bytes, for the hash its slot's header names. - pub fn bytes(&self) -> &[u8] { - // SAFETY: `at` and `len` are the pages `Disk::read_root` allocated and - // filled, identity-mapped while boot services live, written by nothing - // after that read, and released only by `free`, which takes `self`. - unsafe { core::slice::from_raw_parts(self.at as *const u8, self.len as usize) } - } - - /// Give the pages back: a ROOT whose slot was refused is not the kernel's. - pub fn free(self, bs: &BootServices) { - // SAFETY: the pages are this image's own allocation, and `self` is - // consumed, so nothing reads them after this. - let freed = unsafe { bs.free_pages(self.at, (self.len as usize).div_ceil(BLOCK)) }; - if let Err(e) = freed { - println!("ROOT: firmware would not take back the {} bytes at {:#x} ({e:?})", self.len, self.at); - } - } + pub ticks: u64, } /// The whole-disk block device carrying the partition firmware loaded this @@ -199,15 +170,8 @@ impl<'a> Disk<'a> { if len > capacity || !len.is_multiple_of(BLOCK as u64) { return Err(alloc::format!("{len} bytes of ROOT do not fit whole in its {capacity}-byte partition")); } - let pages = (len / BLOCK as u64) as usize; - // `LoaderData`, as the black box's page is, for the reason its module - // header gives. - let at = bs - .allocate_pages(AllocateType::AnyPages, pages) - .map_err(|e| alloc::format!("firmware would not give {len} bytes for ROOT: {e:?}"))?; - // SAFETY: the `pages` pages at `at` were just allocated to this loader, - // are identity-mapped while boot services live, and nothing else holds them. - let into = unsafe { core::slice::from_raw_parts_mut(at as *mut u8, len as usize) }; + let mut pages = Payload::allocate(bs, len).map_err(|why| alloc::format!("ROOT: {why}"))?; + let at = pages.handoff().addr; let granularity = self.granularity_lbas(); // Chunks are whole `BLOCK`s from a page-aligned buffer, so each one @@ -215,8 +179,8 @@ impl<'a> Disk<'a> { let chunk = chunk::chunk_bytes(CHUNK_BOUND, BLOCK, self.lba_bytes, granularity.unwrap_or(0)); let began = crate::arch::counter(); let mut device = Firmware { io: &self.io, media_id: self.media_id }; - let read = chunk::read(&mut device, part.first_lba(), self.lba_bytes, chunk, into); - let image = RootImage { at, len, partition: part.unique_guid().0, ticks: crate::arch::counter().wrapping_sub(began) }; + let read = chunk::read(&mut device, part.first_lba(), self.lba_bytes, chunk, pages.bytes_mut()); + let image = RootImage { pages, partition: part.unique_guid().0, ticks: crate::arch::counter().wrapping_sub(began) }; if let Err(failed) = read { let why = alloc::format!( "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read", @@ -225,7 +189,7 @@ impl<'a> Disk<'a> { failed.error, failed.read ); - image.free(bs); + image.pages.free(bs); return Err(why); } println!( diff --git a/loader/src/slot.rs b/loader/src/slot.rs index 9e026020ee2..1f396773216 100644 --- a/loader/src/slot.rs +++ b/loader/src/slot.rs @@ -6,7 +6,8 @@ //! booted only once, in this order: its signed header is on its FAT //! partition, the signature is [`KEY`]'s, its image did not die on its last //! boot, its version is at or above the floor a boot has proven, and its -//! kernel, boot parameter and ROOT each hash to what the header names. Each +//! kernel, boot parameter, supervisor, file server and ROOT each hash to what +//! the header names. Each //! refusal is said by name, and the boot that falls back tells the kernel //! which slot it refused and why, so the next boot's log carries it. //! @@ -22,13 +23,14 @@ use alloc::string::String; use alloc::vec::Vec; -use toyos_update::image::{Header, SIGNATURE_BYTES, SIGNED_BYTES}; +use toyos_update::image::{Header, Section, SIGNATURE_BYTES, SIGNED_BYTES}; use toyos_update::policy::{self, Refusal}; use toyos_update::record::{self, Booted, Record}; use toyos_update::slots::{self, Slot, Which}; use toyos_update::{sig, Digest}; use crate::efi::{BootServices, CString16, Handle, Mode, SimpleFileSystem, Status, SystemTable}; +use crate::payload::Payload; use crate::rootimage::{Disk, RootImage}; /// The owner's public key, embedded at build time: the only key this loader @@ -46,6 +48,8 @@ pub struct Chosen { pub digest: Digest, pub kernel: Vec, pub cmdline: Vec, + pub supervisor: Payload, + pub fileserver: Payload, pub root: RootImage, /// The unique GUID of its volume, which the kernel and its parameter were read from. pub volume: [u8; 16], @@ -144,20 +148,64 @@ fn verify( held(&kernel, header.kernel().sha256, "kernel")?; let cmdline = read_section(bs, &slot.boot, slots::CMDLINE_FILE, header.cmdline().len, "cmdline")?; held(&cmdline, header.cmdline().sha256, "cmdline")?; + let supervisor = program(bs, &slot.boot, slots::SUPERVISOR_FILE, header.supervisor(), "supervisor")?; + let fileserver = match program(bs, &slot.boot, slots::FILESERVER_FILE, header.fileserver(), "fileserver") { + Ok(fileserver) => fileserver, + Err(why) => { + supervisor.free(bs); + return Err(why); + } + }; - let root = disk.read_root(bs, &part, header.root().len).map_err(|why| { - println!("{HEAD} {letter}: ROOT: {why}"); - Refusal::Unreadable("root") - })?; + let root = match disk.read_root(bs, &part, header.root().len) { + Ok(root) => root, + Err(why) => { + println!("{HEAD} {letter}: ROOT: {why}"); + supervisor.free(bs); + fileserver.free(bs); + return Err(Refusal::Unreadable("root")); + } + }; let began = crate::arch::counter(); - let root_hash = toyos_update::sha256(root.bytes()); + let root_hash = toyos_update::sha256(root.pages.bytes()); println!("{HEAD} {letter}: ROOT hashed in {} counter ticks", crate::arch::counter().wrapping_sub(began)); if root_hash != header.root().sha256 { - root.free(bs); + root.pages.free(bs); + supervisor.free(bs); + fileserver.free(bs); return Err(Refusal::Hash("root")); } - println!("{HEAD} {letter}: kernel, cmdline and ROOT are the bytes the signed header names"); - Ok(Chosen { which, version: header.version, digest, kernel, cmdline, root, volume: slot.boot, refused: None }) + println!("{HEAD} {letter}: kernel, cmdline, supervisor, fileserver and ROOT are the bytes the signed header names"); + Ok(Chosen { + which, + version: header.version, + digest, + kernel, + cmdline, + supervisor, + fileserver, + root, + volume: slot.boot, + refused: None, + }) +} + +/// The program section at `path`, in pages of its own and held to `section`'s +/// hash. +fn program(bs: &BootServices, guid: &[u8; 16], path: &str, section: Section, name: &'static str) -> Result { + let bytes = read_section(bs, guid, path, section.len, name)?; + let mut pages = Payload::allocate(bs, section.len).map_err(|why| { + println!("{HEAD}: {path}: {why}"); + Refusal::Unreadable(name) + })?; + pages.bytes_mut().copy_from_slice(&bytes); + drop(bytes); + // Over the pages the kernel is handed, not over the buffer they came from. + if let Err(why) = held(pages.bytes(), section.sha256, name) { + pages.free(bs); + return Err(why); + } + Ok(pages) } /// `slot`'s signed header, held to [`KEY`], and its SHA-256. diff --git a/loader/src/watchdog.rs b/loader/src/watchdog.rs index 2214bd6ed31..6e07a688b6d 100644 --- a/loader/src/watchdog.rs +++ b/loader/src/watchdog.rs @@ -48,7 +48,7 @@ impl Phys for Identity { /// Arm the watchdog when `cmdline` names it, and say on the console what was /// armed or why nothing was. pub fn arm(system_table: &SystemTable, rsdp_addr: u64, cmdline: &str) { - if !toyos_abi::boot::actuators(cmdline).any(|token| token == toyos_tco::PARAM) { + if !toyos_abi::boot::tokens(cmdline).any(|token| token == toyos_tco::PARAM) { return; } if !crate::arch::pio::EXISTS { diff --git a/sdk/abi/src/boot.rs b/sdk/abi/src/boot.rs index 4190b97681e..153e852f7b2 100644 --- a/sdk/abi/src/boot.rs +++ b/sdk/abi/src/boot.rs @@ -62,11 +62,12 @@ pub struct KernelArgs { /// layout. pub layout: u32, /// The boot parameter, as ASCII with no terminator: comma-separated tokens - /// read out of `\toyos\cmdline` on the volume the bootloader loaded itself - /// from. [`root_uuid`] and [`actuators`] are the two readings of it. + /// read out of `\toyos\cmdline` on the slot's volume, with the words the + /// loader appends ([`SLOT_PARAM`] among them), read by [`tokens`]. /// - /// Every shipping image carries exactly `root=`, and a kernel that - /// carries no actuators refuses any other token rather than ignoring it + /// Every token is a parameter the kernel claims (`kernel/src/params.rs`) + /// or an actuator its table declares, and a kernel that carries no + /// actuators refuses any other rather than ignoring it /// (`kernel/src/actuator.rs`). It is in this struct rather than anywhere the /// kernel could go and fetch it because the earliest actuator panics before /// `mm::init` and another acts at AP bring-up: a parameter that is not here @@ -88,16 +89,10 @@ pub struct KernelArgs { /// land inside, and there is no deriving it from the memory map — the map /// says what firmware *used*, not what the bridge would decode. pub root_bridge_windows: [RootBridgeWindow; MAX_ROOT_BRIDGE_WINDOWS], - /// Where the loader put ROOT: its filesystem, read through the firmware's - /// block I/O into `LoaderData` pages, which the kernel mounts from memory and - /// keeps out of its allocator by this address, as it keeps the black box's. - /// - /// These are the bytes a signature check over ROOT has to cover: the loader - /// writes them once, and nothing writes them between that read and the - /// kernel's mount. Zero length is a loader that handed no image, and the - /// kernel refuses to boot on it by name; it never reads ROOT anywhere else. - pub root_image_addr: u64, - pub root_image_len: u64, + /// ROOT's bytes, opaque to the kernel: the slot's ROOT partition read + /// through the firmware's block I/O. [`Self::supervisor_image`] says what + /// every [`BootImage`] is. + pub root_image: BootImage, /// The unique GUID of the partition the image was read from, raw as in the /// GPT entry like [`Self::boot_partition_guid`]; zero with no image. The /// kernel holds that partition so no claim writes the slot it is running. @@ -145,6 +140,27 @@ pub struct KernelArgs { /// read the kernel and its parameter from, raw as in the GPT entry like /// [`Self::root_partition_guid`]; zero with no image. pub slot_volume_guid: [u8; 16], + /// The supervisor's executable and the file server's, which the kernel + /// hands the supervisor with [`Self::root_image`]: the programs that serve + /// ROOT, so the kernel opens no file to start the machine. + /// + /// **Every `BootImage` is a section of the slot's signed header**, read + /// into page-aligned `LoaderData` pages and held to its hash before the + /// loader exits boot services; nothing writes it after. The kernel keeps + /// each out of its allocator for the machine's life, and refuses by name a + /// boot whose image is empty, unaligned, outside one `LoaderData` + /// descriptor or overlapping another region the loader handed. + pub supervisor_image: BootImage, + pub fileserver_image: BootImage, +} + +/// One payload the loader hands the kernel: `len` bytes at the physical +/// address `addr`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct BootImage { + pub addr: u64, + pub len: u64, } /// The bytes of [`KernelArgs::gop_edid`]: one EDID base block. @@ -163,10 +179,6 @@ pub const LAYOUT: u32 = 0x5459_0000 | core::mem::size_of::() as u32; /// negative control on the kernel's refusal, and read by both of them. pub const WRITE_NO_LAYOUT_PARAM: &str = "loader-writes-no-layout"; -/// The boot parameter on which the loader hands the kernel no ROOT image: the -/// negative control on the kernel's refusal, and read by both of them. -pub const WITHHOLD_ROOT_PARAM: &str = "loader-withholds-root"; - /// The word the loader appends naming the slot it booted, `A` or `B`: the /// signed boot parameter is the slot's own, and which slot carried it is a /// fact only the loader has. @@ -211,23 +223,9 @@ impl RootBridgeWindow { } } -/// The token naming the filesystem the kernel mounts as root. -const ROOT_PARAM: &str = "root="; - -/// What the boot parameter names ROOT, or `None` on a parameter that names none. -/// -/// `bcachefs::FsUuid`'s text, compared against each candidate partition's -/// *superblock* and never against a partition GUID: a role names a filesystem, -/// and a filesystem may have members on more than one disk. -pub fn root_uuid(cmdline: &str) -> Option<&str> { - cmdline.split(',').find_map(|token| token.strip_prefix(ROOT_PARAM)) -} - -/// Every token of the boot parameter that is not [`root_uuid`]'s. A token this -/// yields is one the actuator table has to declare, so a `root=` left in would -/// panic a kernel that boots. -pub fn actuators(cmdline: &str) -> impl Iterator { - cmdline.split(',').filter(|t| !t.is_empty() && !t.starts_with(ROOT_PARAM)) +/// Every token of the boot parameter: each one a parameter of the kernel's (`params`) or an actuator its table declares. +pub fn tokens(cmdline: &str) -> impl Iterator { + cmdline.split(',').filter(|t| !t.is_empty()) } impl KernelArgs { @@ -262,8 +260,7 @@ const _: () = { assert!(offset_of!(KernelArgs, cmdline_len) == 176); assert!(offset_of!(KernelArgs, root_bridge_window_count) == 184); assert!(offset_of!(KernelArgs, root_bridge_windows) == 192); - assert!(offset_of!(KernelArgs, root_image_addr) == 1216); - assert!(offset_of!(KernelArgs, root_image_len) == 1224); + assert!(offset_of!(KernelArgs, root_image) == 1216); assert!(offset_of!(KernelArgs, root_partition_guid) == 1232); assert!(offset_of!(KernelArgs, loader_entry_counter) == 1248); assert!(offset_of!(KernelArgs, loader_handoff_counter) == 1256); @@ -278,7 +275,10 @@ const _: () = { assert!(offset_of!(KernelArgs, microcode_addr) == 1472); assert!(offset_of!(KernelArgs, microcode_len) == 1480); assert!(offset_of!(KernelArgs, slot_volume_guid) == 1488); - assert!(size_of::() == 1504); + assert!(offset_of!(KernelArgs, supervisor_image) == 1504); + assert!(offset_of!(KernelArgs, fileserver_image) == 1520); + assert!(size_of::() == 1536); + assert!(size_of::() == 16); assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440); assert!(align_of::() == 8); assert!(size_of::() == 16); @@ -296,28 +296,6 @@ pub struct MemoryMapEntry { mod tests { use super::*; - /// Every token is one reading's or the other's, and neither sees the - /// other's: a `root=` reaching the actuator table panics a kernel. - #[test] - fn the_two_readings_of_a_boot_parameter_partition_it() { - const ROOT: &str = "0123456789abcdef0123456789abcdef"; - const SHIPPING: &str = "root=0123456789abcdef0123456789abcdef"; - const ARMED: &str = - "root=0123456789abcdef0123456789abcdef,usb-flush-fails,fat-boot-reads-fail"; - - assert_eq!(root_uuid(SHIPPING), Some(ROOT)); - assert_eq!(actuators(SHIPPING).count(), 0); - - assert_eq!(root_uuid(ARMED), Some(ROOT)); - assert!(actuators(ARMED).eq(["usb-flush-fails", "fat-boot-reads-fail"])); - - // No root filesystem, and no parameter at all: neither reading invents. - assert_eq!(root_uuid("usb-flush-fails"), None); - assert!(actuators("usb-flush-fails").eq(["usb-flush-fails"])); - assert_eq!(root_uuid(""), None); - assert_eq!(actuators("").count(), 0); - } - #[test] fn a_window_holds_an_extent_and_not_merely_its_first_address() { let window = RootBridgeWindow { base: 0xa080_0000, length: 0x1f80_0000 }; @@ -367,8 +345,7 @@ mod tests { root_bridge_window_count: 0, root_bridge_windows: [RootBridgeWindow { base: 0, length: 0 }; MAX_ROOT_BRIDGE_WINDOWS], - root_image_addr: 0, - root_image_len: 0, + root_image: BootImage { addr: 0, len: 0 }, root_partition_guid: [0; 16], loader_entry_counter: 0, loader_handoff_counter: 0, @@ -383,6 +360,8 @@ mod tests { microcode_addr: 0, microcode_len: 0, slot_volume_guid: [0; 16], + supervisor_image: BootImage { addr: 0, len: 0 }, + fileserver_image: BootImage { addr: 0, len: 0 }, }; #[test] diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 627a0f93123..ccaa2bb4081 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -341,7 +341,7 @@ crate::user_safe! { pub labels_ptr: u64, pub labels_len: u64, /// The child's working directory, absolute, or the spawn is refused - /// `InvalidArgument`. Under a name the kernel serves (`/system`, `/tmp`) it + /// `InvalidArgument`. Under the name the kernel serves (`/tmp`) it /// must be a directory there or the spawn is `NotFound`; under any other /// name it is a file server's directory, which the caller's client asked /// that server about and the kernel cannot. **Always the caller's @@ -418,6 +418,11 @@ const _: () = assert!(core::mem::size_of::() == 16); pub const SYSCAP_LABEL: &str = "syscap"; /// The label for a program's namespace — what its manifest `receives` becomes. pub const SVC_LABEL: &str = "svc"; +/// The labels the kernel puts on the immutable memory objects it hands the +/// supervisor beside the system capability: the file server's executable and +/// ROOT's bytes, both as the loader verified them (`boot::KernelArgs`). +pub const FILESERVER_IMAGE_LABEL: &str = "boot:fileserver"; +pub const ROOT_IMAGE_LABEL: &str = "boot:root"; /// The label every process starts holding a handle to itself under, carrying /// `WRITE`, `DUP` and `TRANSFER`: what it hands on for a child to be placed /// under it ([`SpawnArgs::place`]). The kernel puts it in every table. @@ -604,6 +609,10 @@ pub enum FileType { DeviceMemory = 13, /// A [null](null) handle. Null = 14, + /// A region nobody can write, which every mapping of is read-only: a + /// payload the loader verified (`boot::BootImage`); `size` is the + /// payload's length, within the region's pages. + ImmutableMemory = 15, } impl FileType { @@ -621,6 +630,7 @@ impl FileType { 12 => Some(Self::Symlink), 13 => Some(Self::DeviceMemory), 14 => Some(Self::Null), + 15 => Some(Self::ImmutableMemory), _ => None, } } diff --git a/sdk/osrelease/src/lib.rs b/sdk/osrelease/src/lib.rs index c738b6a2610..8cb7f6545af 100644 --- a/sdk/osrelease/src/lib.rs +++ b/sdk/osrelease/src/lib.rs @@ -1,5 +1,5 @@ //! `/system/etc/os-release`: which build an image is, written by the build -//! system into ROOT and read by `/system/bin/supervisor`, libc's `uname` and +//! system into ROOT and read by the supervisor, libc's `uname` and //! the sysinfo fork. //! //! The freedesktop os-release format (`os-release(5)`), ToyOS's own fields @@ -50,9 +50,14 @@ pub const NAME: &str = "ToyOS"; /// `ID`, the operating system's identifier. pub const ID: &str = "toyos"; -/// What `/system/bin/supervisor` says a release under, followed by its commit. +/// What the supervisor says a release under, followed by its commit. pub const SAID: &str = "supervisor: build "; +/// What the file server serving `/system` says ROOT under, followed by its +/// filesystem's UUID: a name derived from every file ROOT carries, so the +/// build again, said by the server that serves it. +pub const ROOT_SAID: &str = "fileserver: ROOT is filesystem "; + /// The architecture an image is built for. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Arch { diff --git a/sdk/std/os/process.rs b/sdk/std/os/process.rs index 285421723e4..8d772bce6ea 100644 --- a/sdk/std/os/process.rs +++ b/sdk/std/os/process.rs @@ -46,7 +46,7 @@ pub trait CommandExt { /// find the program, judge the working directory, and read a program on a /// file server into memory. The spawn then calls no file server for them — /// for a caller whose spawning thread may not wait on one, as - /// `/system/bin/supervisor`'s loop may not wait on the file servers it starts + /// the supervisor's loop may not wait on the file servers it starts /// again. A later [`current_dir`](crate::process::Command::current_dir) /// undoes it but for the program read, which a prepare after it keeps and /// a later [`image_from`](CommandExt::image_from) does not. @@ -70,6 +70,13 @@ pub trait CommandExt { #[stable(feature = "toyos_ext", since = "1.0.0")] fn image_from(&mut self, path: &crate::path::Path) -> &mut Self; + /// Run the boot image `image` names, an immutable region this process + /// holds and keeps, rather than any file: the command's program is the + /// child's `argv[0]` and nothing opens it. What the supervisor starts the + /// file server with before any file can be read. + #[stable(feature = "toyos_ext", since = "1.0.0")] + fn boot_image(&mut self, image: u32) -> &mut Self; + /// Place the child under the process `place` names rather than under this /// one, so that its end, not this process's, takes the child down. /// @@ -124,6 +131,11 @@ impl CommandExt for crate::process::Command { self } + fn boot_image(&mut self, image: u32) -> &mut Self { + self.as_inner_mut().boot_image(image); + self + } + fn under(&mut self, place: u32) -> &mut Self { self.as_inner_mut().under(place); self diff --git a/sdk/std/sys/fs.rs b/sdk/std/sys/fs.rs index ffecb3da6c1..a42f8bf1c5f 100644 --- a/sdk/std/sys/fs.rs +++ b/sdk/std/sys/fs.rs @@ -1,5 +1,5 @@ -//! Files on ToyOS: a path is served by the kernel (`/system`, `/tmp`) or by a -//! file server behind one of the process's directory capabilities, and +//! Files on ToyOS: a path is served by the kernel (`/tmp`) or by a file +//! server behind one of the process's directory capabilities, and //! `toyos::files` routes it, follows its links and keeps what it opened. A //! `File` is one descriptor of the process's table either way. diff --git a/sdk/std/sys/process.rs b/sdk/std/sys/process.rs index a60be02f030..dedf7f4f9b4 100644 --- a/sdk/std/sys/process.rs +++ b/sdk/std/sys/process.rs @@ -34,6 +34,9 @@ pub struct Command { /// it read: kept across a later working directory, so a spawn starts the /// bytes the caller saw ([`Command::prepared_image`]). image: Option<(OsString, toyos::process::Image)>, + /// A boot image this process holds, which the child runs instead of any + /// file ([`Command::boot_image`]). + boot_image: Option, } /// The file reads a spawn needs, made ahead of it ([`Command::prepare`]). @@ -83,6 +86,7 @@ impl Command { image_path: None, prepared: None, image: None, + boot_image: None, } } @@ -96,6 +100,17 @@ impl Command { self.image_path = Some(path.to_owned()); self.prepared = None; self.image = None; + self.boot_image = None; + } + + /// Run the immutable region `handle` names, which this process keeps, + /// rather than any file: `argv[0]` is the program the command names, and + /// nothing opens it. + pub fn boot_image(&mut self, handle: u32) { + self.boot_image = Some(handle); + self.image_path = None; + self.prepared = None; + self.image = None; } pub fn env_mut(&mut self) -> &mut CommandEnv { @@ -117,7 +132,7 @@ impl Command { let kept = self.image.as_ref().is_some_and(|(read, _)| *read == source); if !kept { self.image = None; - if crate::sys::fs::is_served(Path::new(&source)) { + if self.boot_image.is_none() && crate::sys::fs::is_served(Path::new(&source)) { let image = crate::sys::fs::read_image(Path::new(&source))?; self.image = Some((source, image)); } @@ -208,7 +223,7 @@ impl Command { fn resolve_program(&self) -> io::Result { let prog = self.program.to_str().unwrap_or(""); - if prog.contains('/') { + if prog.contains('/') || self.boot_image.is_some() { return Ok(self.program.clone()); } // Search PATH for the executable @@ -323,10 +338,19 @@ impl Command { // this process's own that the kernel pages the child from: the kernel // opens only what it serves itself. let source = Path::new(image_path.unwrap_or(program)); - let image = match prepared_image { - Some(image) => Some(image), - None if crate::sys::fs::is_served(source) => Some(crate::sys::fs::read_image(source)?), - None => None, + let image = match (prepared_image, self.boot_image) { + (Some(image), _) => Some(image), + (None, Some(handle)) => { + let copy = toyos_abi::syscall::dup(toyos_abi::RawHandle(handle)).map_err(crate::sys::to_io_error)?; + // SAFETY: `dup` answered a fresh handle nothing else answers for. + let copy = unsafe { toyos::OwnedHandle::from_raw(copy) }; + let region = toyos::shm::Immutable::adopt(copy).map_err(|why| { + io::Error::new(io::ErrorKind::InvalidInput, format!("the boot image is no immutable region: {why}")) + })?; + Some(toyos::process::Image::boot(region)) + } + (None, None) if crate::sys::fs::is_served(source) => Some(crate::sys::fs::read_image(source)?), + (None, None) => None, }; let spawned = request.direct(&direct_env, image.as_ref()); // The child keeps the object alive; this process's handle, and with it diff --git a/sdk/toyos/src/files.rs b/sdk/toyos/src/files.rs index cc6d0e5dd43..926c9791760 100644 --- a/sdk/toyos/src/files.rs +++ b/sdk/toyos/src/files.rs @@ -1,5 +1,5 @@ //! Files by path, and the descriptors that name open ones, wherever they are -//! served: the kernel serves `/system` and `/tmp`, and every other directory a +//! served: the kernel serves `/tmp`, and every other directory a //! process may reach is a capability in its namespace, `fs:`, //! served by a file server ([`crate::fs`]). A program or a library is read //! here into an image the kernel pages it from ([`read_image`]). diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 7e65d15ff70..b3621c4a725 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -211,7 +211,7 @@ impl From for OwnedHandle { fn from(pipe: Pipe) -> Self { pipe.0 } } -/// A console handle: the one `/system/bin/supervisor` endows `/system/bin/logkeeper` with, +/// A console handle: the one the supervisor endows `/system/bin/logkeeper` with, /// the only one in the machine that may write. /// /// **A write takes whole lines and says how many bytes that was.** The diff --git a/sdk/toyos/src/log/mod.rs b/sdk/toyos/src/log/mod.rs index c8f5b403c2a..b60ccaab22c 100644 --- a/sdk/toyos/src/log/mod.rs +++ b/sdk/toyos/src/log/mod.rs @@ -5,7 +5,7 @@ //! [`ring`]): its stdout and stderr, and what it says with [`say!`](crate::say), //! [`warn!`](crate::warn) and [`error!`](crate::error), each stamped with its //! time, severity and thread as it is written ([`stdio`]). Whose lines they are is decided where the ring was made: -//! `/system/bin/supervisor` names each ring to `/system/bin/logkeeper`, and nothing a +//! the supervisor names each ring to `/system/bin/logkeeper`, and nothing a //! program writes can change the name. //! //! **The kernel's records** are read with [`LogTail`]. The kernel keeps no diff --git a/sdk/toyos/src/log/region.rs b/sdk/toyos/src/log/region.rs index f9363459f59..b7ec111d8da 100644 --- a/sdk/toyos/src/log/region.rs +++ b/sdk/toyos/src/log/region.rs @@ -2,7 +2,7 @@ //! header, [`LANES`] lanes of [`LANE_SLOTS`] slots, and [`SHARED_SLOTS`] slots //! of shared ring — each slot a protocol word and a [`Body`]. //! -//! `/system/bin/supervisor` creates one per program it starts, lays it out before +//! The supervisor creates one per program it starts, lays it out before //! any other process can map it ([`Ring::lay_out`]), gives the program //! duplicates as its stdout and stderr, and hands `/system/bin/logkeeper` the //! region with the program's name. The program writes, `logkeeper` reads, and diff --git a/sdk/toyos/src/log/stdio.rs b/sdk/toyos/src/log/stdio.rs index 25b3db81868..f8e57a57525 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -2,7 +2,7 @@ //! when the stream is one, bytes into its descriptor when it is anything else. //! //! **Where a stream goes is what its descriptor names** (`crate::fd`). -//! `/system/bin/supervisor` gives a program its log ring as descriptors 1 and +//! The supervisor gives a program its log ring as descriptors 1 and //! 2, so a daemon's output is records; a program a terminal started holds that //! terminal's pipes there, so its output is bytes on the terminal. The first //! write to a stream asks the descriptor once ([`FileType::SharedMemory`] is a diff --git a/sdk/toyos/src/power.rs b/sdk/toyos/src/power.rs index 56f07a154f0..5ecbb525f4d 100644 --- a/sdk/toyos/src/power.rs +++ b/sdk/toyos/src/power.rs @@ -1,4 +1,4 @@ -//! Asking `/system/bin/supervisor` to stop the machine. +//! Asking the supervisor to stop the machine. //! //! **The stop is the supervisor's to sequence, because the log is a process.** The //! kernel stops every userland thread at once and waits for none of them, so diff --git a/sdk/toyos/src/process.rs b/sdk/toyos/src/process.rs index bd75ed15f78..84085baa5a3 100644 --- a/sdk/toyos/src/process.rs +++ b/sdk/toyos/src/process.rs @@ -11,7 +11,7 @@ use toyos_abi::handle::Rights; use toyos_abi::syscall::{self, ProcessStats, SyscallError}; use crate::endow::FromHandle; -use crate::shm::SharedMemory; +use crate::shm::{Immutable, SharedMemory}; use crate::{AsHandle, OwnedHandle, RawHandle}; /// The `` number POSIX names an end by, as libc's `WTERMSIG` @@ -101,14 +101,21 @@ impl FromHandle for Process { } } -/// A program's bytes in a memory object of this process's own, which the -/// kernel pages a child from: how a program the kernel does not serve is -/// spawned (`SpawnArgs::image`). +/// A program's bytes in a memory object this process holds, which the kernel +/// pages a child from: how a program the kernel does not serve is spawned +/// (`SpawnArgs::image`). pub struct Image { - object: SharedMemory, + object: Object, len: u64, } +enum Object { + /// Read into memory of this process's own. + Read(SharedMemory), + /// A boot image the kernel handed over. + Boot(Immutable), +} + /// Why [`Image::read`] made no image. #[derive(Debug)] pub enum ImageRefused { @@ -141,18 +148,32 @@ impl Image { n => done += n, } } - Ok(Self { object, len }) + Ok(Self { object: Object::Read(object), len }) + } + + /// The program a boot image is, whole. + pub fn boot(image: Immutable) -> Self { + let len = image.as_slice().len() as u64; + Self { object: Object::Boot(image), len } } /// The program's bytes, as they were read. pub fn bytes(&self) -> &[u8] { - &self.object.as_slice()[..self.len as usize] + let bytes = match &self.object { + Object::Read(object) => object.as_slice(), + Object::Boot(image) => image.as_slice(), + }; + &bytes[..self.len as usize] } /// What `SpawnArgs::image` and `SpawnArgs::image_len` carry. The object is /// this process's until the spawn returns; the child keeps its own. pub fn spawn_words(&self) -> (u64, u64) { - (u64::from(self.object.as_handle().0), self.len) + let handle = match &self.object { + Object::Read(object) => object.as_handle(), + Object::Boot(image) => image.as_handle(), + }; + (u64::from(handle.0), self.len) } } diff --git a/sdk/toyos/src/shm.rs b/sdk/toyos/src/shm.rs index 8a69f130834..c7c75f2dd8b 100644 --- a/sdk/toyos/src/shm.rs +++ b/sdk/toyos/src/shm.rs @@ -16,6 +16,10 @@ //! length the protocol says, is refused before it is mapped, since mapping a //! handle that names no region ends this process. [`SharedMemory::adopt`] is //! for a region the kernel itself handed this process. +//! +//! **A region nobody can write is an [`Immutable`]**, taken only once the +//! kernel answers it is one: every mapping of it is read-only, so it has no +//! mutable view to offer. use core::fmt; use core::sync::atomic::AtomicU8; @@ -152,6 +156,63 @@ impl AsHandle for SharedMemory { } } +/// A region nobody can write, mapped read-only: a payload the loader verified, +/// as the kernel hands it out (`toyos_abi::boot::BootImage`). +pub struct Immutable { + handle: OwnedHandle, + ptr: *const u8, + len: usize, +} + +// SAFETY: the mapping is read-only and nobody writes the region. +unsafe impl Send for Immutable {} +// SAFETY: as for `Send`: there is nothing to race with. +unsafe impl Sync for Immutable {} + +impl Immutable { + /// Map `handle`, refused and never mapped unless the kernel answers it is + /// a region nobody can write. Its length is the kernel's answer. The handle + /// is consumed either way. + pub fn adopt(handle: OwnedHandle) -> Result { + let stat = syscall::fstat(handle.raw()).map_err(NotImmutable::Kernel)?; + if stat.file_type != FileType::ImmutableMemory { + return Err(NotImmutable::Writable); + } + // A region the kernel maps is within the address space, so its length is a `usize`. + Ok(Self { ptr: map(&handle).map_err(NotImmutable::Kernel)?, handle, len: stat.size as usize }) + } + + pub fn as_slice(&self) -> &[u8] { + // SAFETY: the mapping is `len` bytes, lives as long as `self`, and is + // written by nobody, so a shared reference over it is sound. + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } +} + +/// Why a handle was not taken as an [`Immutable`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NotImmutable { + /// The kernel would not describe or map it. + Kernel(SyscallError), + /// It names something other than a region nobody can write. + Writable, +} + +impl fmt::Display for NotImmutable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kernel(e) => write!(f, "the kernel would not describe or map it ({e:?})"), + Self::Writable => write!(f, "it is not a region nobody can write"), + } + } +} + +impl AsHandle for Immutable { + fn as_handle(&self) -> RawHandle { + self.handle.raw() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/system/CLAUDE.md b/system/CLAUDE.md index 8fa936c3a79..1f43e64ad23 100644 --- a/system/CLAUDE.md +++ b/system/CLAUDE.md @@ -1,6 +1,6 @@ # System programs -The module header at the site owns its subject — surfaces, translators and the channel in `sdk/toyos/`'s surface modules, soundserver whole under `system/soundserver/`, what a process holds and how it got it at `kernel/src/object/` and `/system/bin/supervisor`. The compositor's decisions are `system/compositor/desktop/`, pure and host-tested; `system/compositor/` is devices, handles, shared memory and the panel. +The module header at the site owns its subject — surfaces, translators and the channel in `sdk/toyos/`'s surface modules, soundserver whole under `system/soundserver/`, what a process holds and how it got it at `kernel/src/object/` and `system/supervisor/`. The compositor's decisions are `system/compositor/desktop/`, pure and host-tested; `system/compositor/` is devices, handles, shared memory and the panel. **A server never blocks on a client** — the doctrine no single site owns. Accept and the first frame are two events; a frame is buffered until whole before anything acts on it; a write is one `try_send` whose refusal drops the peer by name; a blocking read or write of a pipe the client owns is the same bug. The supervisor, the compositor, netstack, soundserver and every surface host use `ipc::FrameRx`. diff --git a/system/acpiserver/src/host.rs b/system/acpiserver/src/host.rs index f7b9dcca432..9a610ca6d26 100644 --- a/system/acpiserver/src/host.rs +++ b/system/acpiserver/src/host.rs @@ -157,7 +157,7 @@ pub trait Kernel { fn latched(&self) -> Option; /// Acknowledge the SCI, whose status this server has cleared. fn ack(&self); - /// Ask `/system/bin/supervisor` to power the machine off + /// Ask the supervisor to power the machine off /// (`toyos::power::stop`), which answers only where it did not. fn power_off(&self) -> toyos::power::Refused; } diff --git a/system/console/src/main.rs b/system/console/src/main.rs index 0a897822d36..6700fc8a98a 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -62,7 +62,7 @@ const SOURCE_FRAMES: usize = 64; /// apply. const SEED_MAX_BYTES: usize = 64 * 1024; -/// The name `/system/bin/supervisor` starts this program under, which its own lines +/// The name the supervisor starts this program under, which its own lines /// come back from `logkeeper` tagged with. const OWN_TAG: &str = "console"; diff --git a/system/filepicker/src/consent.rs b/system/filepicker/src/consent.rs index 2c0febbccad..60ab92f0069 100644 --- a/system/filepicker/src/consent.rs +++ b/system/filepicker/src/consent.rs @@ -1,4 +1,4 @@ -//! The question `/system/bin/supervisor` asks before an installed package's +//! The question the supervisor asks before an installed package's //! launch, drawn in the compositor's prompt layer ([`toyos_manifest::consent`]). //! //! **A chooser bounded by the folder rules**: it starts at the session user's diff --git a/system/fileserver/Cargo.toml b/system/fileserver/Cargo.toml index e5d5ad66fc2..b3b28830be5 100644 --- a/system/fileserver/Cargo.toml +++ b/system/fileserver/Cargo.toml @@ -2,7 +2,7 @@ name = "fileserver" edition = "2024" license = "MIT OR Apache-2.0" -description = "ToyOS's file server: one process per storage role — DATA, the log or the boot volume — serves that role's paths." +description = "ToyOS's file server: one process per storage role — ROOT, DATA, the log or the boot volume — serves that role's paths." # The library is the server's decisions — the block cache, the volumes and the # resolver — so the host can test them; the service is `src/main.rs`. @@ -18,6 +18,7 @@ path = "src/main.rs" toyos-abi = { path = "../../sdk/abi" } toyos = { path = "../../sdk/toyos" } toyos-manifest = { path = "../../lib/manifest" } +toyos-osrelease = { path = "../../sdk/osrelease" } toyos-blockring = { path = "../../lib/blockring" } toyos-gpt = { path = "../../lib/gpt" } toyos-fat32 = { path = "../../lib/fat32" } diff --git a/system/fileserver/src/data.rs b/system/fileserver/src/data.rs index e587f828422..bc9f024c859 100644 --- a/system/fileserver/src/data.rs +++ b/system/fileserver/src/data.rs @@ -107,7 +107,7 @@ pub fn find(served: Result<&[[u8; 16]], String>) -> Located { } } -fn refusal(e: &FsError) -> Error { +pub(crate) fn refusal(e: &FsError) -> Error { match e { FsError::NotFound => Error::NotFound, FsError::NoSpace { .. } => Error::NoSpace, @@ -131,7 +131,7 @@ fn refusal(e: &FsError) -> Error { } /// Logs the format's own account, answers the word a client can act on. -fn mapped(what: &str, path: &str, r: Result) -> Result { +pub(crate) fn mapped(what: &str, path: &str, r: Result) -> Result { r.map_err(|e| { println!("fileserver: {what} of '{path}' failed: {e:?}"); refusal(&e) @@ -148,7 +148,7 @@ fn disk_word(e: DiskError) -> Error { } /// The block holding page `page`, if the extents reach it. -fn block_for(extents: &[Extent], page: u64) -> Option { +pub(crate) fn block_for(extents: &[Extent], page: u64) -> Option { let mut at = 0u64; for e in extents { let end = at.checked_add(e.block_count as u64)?; diff --git a/system/fileserver/src/lib.rs b/system/fileserver/src/lib.rs index 6876c9fa143..40b266a0d28 100644 --- a/system/fileserver/src/lib.rs +++ b/system/fileserver/src/lib.rs @@ -1,8 +1,9 @@ //! A file server's decisions: the blocks memory stands in for a partition //! with ([`ram`]) — a partition's own are `diskserver::disk`'s — the one //! cache every byte of its volume passes through ([`cache`]), the volumes it -//! can serve ([`data`] for the bcachefs DATA role, [`fat`] for FAT32's LOG and -//! BOOT, [`absent`] for a role with no volume this boot), and the resolver that +//! can serve ([`data`] for the bcachefs DATA role, [`root`] for the SYSTEM +//! role's ROOT in memory, [`fat`] for FAT32's LOG and BOOT, [`absent`] for a +//! role with no volume this boot), and the resolver that //! keeps every path inside the directory a connection was given ([`resolve`]), //! which requests a read-only one is refused ([`rights`]), what a client //! may take of the server and name in a request ([`bounds`]), and the open @@ -25,5 +26,6 @@ pub mod fat; pub mod ram; pub mod resolve; pub mod rights; +pub mod root; pub mod volume; pub mod writeback; diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index b3d4a20fad8..4c9a2bc9324 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -1,14 +1,19 @@ -//! fileserver: one role's file server — DATA, LOG or BOOT — serving the directories -//! of that role as capabilities. +//! fileserver: one role's file server — SYSTEM, DATA, LOG or BOOT — serving the +//! directories of that role as capabilities. //! //! **What it holds**: the acceptor of its role's one port, endowed by the -//! supervisor under `serve:fs:`; for its volume, `block` in its -//! namespace, a connector to the block service minted for its role's -//! partition alone — every DATA partition, or the one the loader named, -//! read-only for the boot volume; and nothing else of the machine. Two DATA -//! partitions are refused by name, never guessed between -//! (`fileserver::data::find`). Argv is the role, and for LOG and BOOT the -//! unique GUID of the partition the loader named for it. +//! supervisor under `serve:fs:`; for its volume, SYSTEM's ROOT image +//! (`ROOT_IMAGE_LABEL`), which needs no disk, or `block` in its namespace, a +//! connector to the block service minted for its role's partition alone — +//! every DATA partition, or the one the loader named, read-only for the boot +//! volume; and nothing else of the machine. Two DATA partitions are refused by +//! name, never guessed between (`fileserver::data::find`). Argv is the role, +//! and for LOG and BOOT the unique GUID of the partition the loader named for +//! it. +//! +//! **SYSTEM is the first program the supervisor starts**, from its boot image +//! and before any file can be read, so it needs no file and no library to +//! serve `/system`. //! //! **A connection is what its grant says** (`toyos::fs::Grant`): the badge //! the supervisor minted its connector with, which the kernel stamped on it and @@ -54,6 +59,7 @@ use fileserver::ram::Ram; use fileserver::fat::FatVolume; use fileserver::resolve::{self, Found, Refusal as Escape, Resolved}; use fileserver::rights; +use fileserver::root::RootVolume; use fileserver::volume::{Kind, Meta, Node, OpenHow, Out, Volume}; use fileserver::writeback::WriteBack; use toyos::endow::{self, Endowments}; @@ -105,6 +111,7 @@ const TOKEN_STREAM: u64 = 2 << 32; #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Role { + System, Data, Log, Boot, @@ -113,6 +120,7 @@ enum Role { impl Role { fn parse(text: &str) -> Option { match text { + "system" => Some(Self::System), "data" => Some(Self::Data), "log" => Some(Self::Log), "boot" => Some(Self::Boot), @@ -122,6 +130,7 @@ impl Role { fn name(self) -> &'static str { match self { + Self::System => "system", Self::Data => "data", Self::Log => "log", Self::Boot => "boot", @@ -203,7 +212,7 @@ impl Exports { fn main() { let args: Vec = std::env::args().collect(); let role = args.get(1).and_then(|r| Role::parse(r)).unwrap_or_else(|| { - panic!("fileserver: started with {args:?}; the first argument is a role: data, log or boot") + panic!("fileserver: started with {args:?}; the first argument is a role: system, data, log or boot") }); let mut guid = None; for arg in args.iter().skip(2) { @@ -269,6 +278,7 @@ fn data_partitions() -> Result, String> { fn open_volume(role: Role, guid: Option<&str>, roots: &[&str]) -> Box { match role { + Role::System => Box::new(root()), Role::Data => match fileserver::data::find(data_partitions().as_deref().map_err(String::clone)) { Located::Nowhere => ram(roots, "this machine has no DATA partition"), Located::Served(guid) => match served(guid) { @@ -296,6 +306,20 @@ fn open_volume(role: Role, guid: Option<&str>, roots: &[&str]) -> Box RootVolume { + let label = toyos_abi::syscall::ROOT_IMAGE_LABEL; + let handle = Endowments::get() + .take::(label) + .unwrap_or_else(|| panic!("fileserver: the system role was started without `{label}`")); + let image = toyos::shm::Immutable::adopt(handle) + .unwrap_or_else(|why| panic!("fileserver: `{label}` is no image nobody writes: {why}")); + let bytes: &'static [u8] = Box::leak(Box::new(image)).as_slice(); + RootVolume::mount(bytes).unwrap_or_else(|why| panic!("fileserver: {why}")) +} + fn absent(roots: &[&str], why: String) -> Box { println!("fileserver: {why}; DATA is absent this boot"); Box::new(Absent::new(roots, why)) diff --git a/system/fileserver/src/root.rs b/system/fileserver/src/root.rs new file mode 100644 index 00000000000..282d9b293fa --- /dev/null +++ b/system/fileserver/src/root.rs @@ -0,0 +1,392 @@ +//! The SYSTEM role's volume: ROOT, the bcachefs volume of the slot's signed +//! image, served read-only out of the memory the kernel handed the supervisor +//! (`toyos_abi::syscall::ROOT_IMAGE_LABEL`). No disk and no cache: the bytes +//! are already in memory, and nobody writes them. +//! +//! **ROOT crossed the loader's hash and nothing else.** Every block the format +//! names is bounds-checked against the bytes ([`Bytes`]), and every refusal of +//! the format is a client's `Io`, never a panic of this server. +//! +//! Every name is indexed at mount, as DATA's are ([`crate::data`]): ROOT is +//! written once by the build and never changes, so a directory is a prefix of +//! names and nothing more. + +use std::collections::BTreeMap; + +use bcachefs::{BlockBuf, BlockIO, BlockNum, DeviceError, Extent, Mounted, ReadOnly, TransferError}; +use toyos::fs::Error; + +use crate::data::{block_for, mapped}; +use crate::volume::{join, Identities, Kind, Meta, Node, OpenHow, Out, Volume}; + +/// The longest symlink target read back: the wire's path bound. +const MAX_LINK: u64 = toyos::fs::MAX_PATH as u64; + +/// ROOT's unit, which is the format's block. +const BLOCK: usize = 4096; + +/// ROOT's bytes, as the format reads them: whole blocks of them, and none past. +pub struct Bytes(&'static [u8]); + +impl Bytes { + /// Block `block`, or `None` past the bytes. + fn block(&self, block: u64) -> Option<&[u8]> { + let at = usize::try_from(block).ok()?.checked_mul(BLOCK)?; + self.0.get(at..at.checked_add(BLOCK)?) + } +} + +/// A block outside the bytes, or a write to them. +struct Refused; + +impl TransferError for Refused { + fn refused_before_attempt(&self) -> bool { + false + } +} + +impl BlockIO for Bytes { + fn read_block(&self, block: BlockNum, buf: &mut BlockBuf) -> Result<(), DeviceError> { + buf.0.copy_from_slice(self.block(block.raw()).ok_or(DeviceError::classify(&Refused))?); + Ok(()) + } + + /// A `ReadOnly` mount has no path here. + fn write_block(&self, _: BlockNum, _: &BlockBuf) -> Result<(), DeviceError> { + Err(DeviceError::classify(&Refused)) + } + + fn block_count(&self) -> u64 { + (self.0.len() / BLOCK) as u64 + } +} + +/// One file open, while any client holds it. +struct Open { + extents: Vec, + size: u64, + mtime: u64, + holders: u32, +} + +pub struct RootVolume { + fs: Mounted, + names: BTreeMap, + open: BTreeMap, + by_path: BTreeMap, + next: Node, + ids: Identities, +} + +impl RootVolume { + /// The volume `bytes` hold, every name indexed; why not, by the format's word. + pub fn mount(bytes: &'static [u8]) -> Result { + let fs = Mounted::<_, ReadOnly>::open(Bytes(bytes)).map_err(|e| format!("ROOT does not mount: {e:?}"))?; + let listed = fs.list(usize::MAX, &|_| true).map_err(|e| format!("ROOT would not list: {e:?}"))?; + let mut names = BTreeMap::new(); + for (name, _) in listed { + let kind = match name.strip_suffix('/') { + Some(dir) => { + names.insert(dir.to_string(), Kind::Dir); + continue; + } + None if fs.is_symlink(&name).map_err(|e| format!("ROOT: {name}: {e:?}"))? => Kind::Symlink, + None => Kind::File, + }; + names.insert(name, kind); + } + Ok(Self { fs, names, open: BTreeMap::new(), by_path: BTreeMap::new(), next: 1, ids: Identities::new() }) + } + + /// The filesystem's name, which the build derives from every file ROOT carries. + pub fn uuid(&self) -> String { + self.fs.uuid().to_string() + } + + fn has_children(&self, dir: &str) -> bool { + let prefix = format!("{dir}/"); + self.names.range(prefix.clone()..).next().is_some_and(|(name, _)| name.starts_with(&prefix)) + } + + fn is_dir(&self, path: &str) -> bool { + path.is_empty() + || self.names.get(path) == Some(&Kind::Dir) + || (!self.names.contains_key(path) && self.has_children(path)) + } + + fn entry(&self, node: Node) -> Result<&Open, Error> { + self.open.get(&node).ok_or(Error::Stale) + } +} + +impl Volume for RootVolume { + fn writable(&self) -> bool { + false + } + + fn lstat(&mut self, path: &str) -> Result { + match self.names.get(path).copied() { + Some(Kind::Dir) => Ok(Meta { kind: Kind::Dir, size: 0, mtime: 0 }), + Some(kind) => { + let (_, size) = mapped("a lookup", path, self.fs.file_extents(path))?.ok_or(Error::NotFound)?; + let mtime = mapped("a lookup", path, self.fs.file_mtime(path))?.ok_or(Error::NotFound)?; + Ok(Meta { kind, size, mtime }) + } + None if self.is_dir(path) => Ok(Meta { kind: Kind::Dir, size: 0, mtime: 0 }), + None => Err(Error::NotFound), + } + } + + fn read_link(&mut self, path: &str) -> Result { + if self.names.get(path) != Some(&Kind::Symlink) { + return Err(if self.names.contains_key(path) || self.is_dir(path) { Error::NotALink } else { Error::NotFound }); + } + mapped("readlink", path, self.fs.read_link(path, MAX_LINK))?.ok_or(Error::Io) + } + + fn list(&mut self, dir: &str) -> Result, Error> { + if !self.is_dir(dir) { + return Err(if self.names.contains_key(dir) { Error::NotADirectory } else { Error::NotFound }); + } + let prefix = if dir.is_empty() { String::new() } else { format!("{dir}/") }; + let mut children: BTreeMap = BTreeMap::new(); + for (name, kind) in self.names.range(prefix.clone()..) { + let Some(rest) = name.strip_prefix(&prefix) else { break }; + match rest.split_once('/') { + Some((child, _)) => { + children.insert(child.to_string(), Kind::Dir); + } + None => { + children.entry(rest.to_string()).or_insert(*kind); + } + } + } + let mut out = Vec::with_capacity(children.len()); + for (name, kind) in children { + let meta = match kind { + Kind::Dir => Meta { kind, size: 0, mtime: 0 }, + _ => self.lstat(&join(dir, &name))?, + }; + out.push((name, meta)); + } + Ok(out) + } + + fn open(&mut self, path: &str, how: OpenHow) -> Result { + if self.is_dir(path) { + return Err(Error::IsADirectory); + } + match self.names.get(path) { + Some(_) if how.create_new => return Err(Error::AlreadyExists), + None if how.create || how.create_new => return Err(Error::ReadOnly), + None => return Err(Error::NotFound), + // A link the resolver did not follow is one with nothing behind it. + Some(Kind::Symlink | Kind::Dir) => return Err(Error::NotFound), + Some(Kind::File) if how.truncate => return Err(Error::ReadOnly), + Some(Kind::File) => {} + } + let node = match self.by_path.get(path).copied() { + Some(node) => node, + None => { + let (extents, size) = mapped("open", path, self.fs.file_extents(path))?.ok_or(Error::NotFound)?; + let mtime = mapped("open", path, self.fs.file_mtime(path))?.ok_or(Error::NotFound)?; + let node = self.next; + self.next += 1; + self.open.insert(node, Open { extents, size, mtime, holders: 0 }); + self.by_path.insert(path.to_string(), node); + node + } + }; + self.open.get_mut(&node).expect("just found or made").holders += 1; + Ok(node) + } + + fn close(&mut self, node: Node) -> Result<(), Error> { + let Some(open) = self.open.get_mut(&node) else { return Ok(()) }; + open.holders -= 1; + if open.holders == 0 { + self.open.remove(&node); + self.by_path.retain(|_, n| *n != node); + } + Ok(()) + } + + fn node_meta(&mut self, node: Node) -> Result { + let open = self.entry(node)?; + Ok(Meta { kind: Kind::File, size: open.size, mtime: open.mtime }) + } + + fn identity(&mut self, path: &str) -> Result { + if !self.names.contains_key(path) && !self.is_dir(path) { + return Err(Error::NotFound); + } + Ok(self.ids.of(path.to_string())) + } + + fn read(&mut self, node: Node, offset: u64, out: &mut dyn Out) -> Result { + let open = self.entry(node)?; + if offset >= open.size { + return Ok(0); + } + let n = out.len().min((open.size - offset) as usize); + let mut done = 0; + while done < n { + let at = offset + done as u64; + let within = (at % BLOCK as u64) as usize; + let take = (BLOCK - within).min(n - done); + match block_for(&open.extents, at / BLOCK as u64) { + Some(block) => { + let Some(bytes) = self.fs.io().block(block) else { + println!("fileserver: a file on ROOT names block {block}, past ROOT's end"); + return Err(Error::Io); + }; + out.put(done, &bytes[within..within + take]); + } + // Past the extents: a hole, whose bytes are zeros. + None => out.zero(done, take), + } + done += take; + } + Ok(n) + } + + fn write(&mut self, _: Node, _: u64, _: &[u8]) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn truncate(&mut self, _: Node, _: u64) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn mkdir(&mut self, _: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn rmdir(&mut self, _: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn unlink(&mut self, _: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn rename(&mut self, _: &str, _: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn symlink(&mut self, _: &str, _: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + fn sync(&mut self) -> Result, Error> { + Ok(Vec::new()) + } + + /// None: ROOT is read-only. + fn room(&mut self) -> Result { + Ok(toyos::fs::Room { free: 0, unit: BLOCK as u64 }) + } + + fn describe(&self) -> String { + format!("{}{}, {} blocks read-only from memory", toyos_osrelease::ROOT_SAID, self.uuid(), self.fs.io().block_count()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::volume::Buf; + + /// A ROOT the format writes, as the build writes one: two files, one of + /// them several blocks, a symlink and a nested directory. + fn root() -> RootVolume { + let mut fs = bcachefs::Formatted::format(bcachefs::VecBlockIO::new(256)).expect("format"); + fs.create("etc/system.manifest", b"program shell /system/bin/shell\n", 7).unwrap(); + fs.create("bin/toybox", &big(), 9).unwrap(); + fs.create_symlink("bin/ls", "/system/bin/toybox", 0).unwrap(); + fs.create("share/fonts/a.font", b"font", 0).unwrap(); + let bytes = fs.into_io().expect("write").into_vec(); + RootVolume::mount(Box::leak(bytes.into_boxed_slice())).expect("mount") + } + + fn big() -> Vec { + (0..3 * BLOCK as u32 + 17).map(|i| (i * 7) as u8).collect() + } + + const READ: OpenHow = OpenHow { create: false, create_new: false, truncate: false }; + + fn read_all(v: &mut RootVolume, path: &str) -> Vec { + let node = v.open(path, READ).expect(path); + let size = v.node_meta(node).unwrap().size as usize; + let mut out = vec![0; size + 5]; + let n = v.read(node, 0, &mut Buf(&mut out)).unwrap(); + v.close(node).unwrap(); + out.truncate(n); + out + } + + #[test] + fn root_reads_back_what_the_build_put_in_it() { + let mut v = root(); + assert_eq!(read_all(&mut v, "etc/system.manifest"), b"program shell /system/bin/shell\n"); + assert_eq!(read_all(&mut v, "bin/toybox"), big()); + let node = v.open("bin/toybox", READ).unwrap(); + let mut part = [0u8; 10]; + assert_eq!(v.read(node, BLOCK as u64 - 3, &mut Buf(&mut part)).unwrap(), 10); + assert_eq!(part[..], big()[BLOCK - 3..BLOCK + 7]); + assert_eq!(v.lstat("bin/toybox").unwrap().mtime, 9); + assert_eq!(v.read_link("bin/ls").unwrap(), "/system/bin/toybox"); + assert_eq!(v.read_link("bin/toybox"), Err(Error::NotALink)); + let listed: Vec<(String, Kind)> = v.list("").unwrap().into_iter().map(|(n, m)| (n, m.kind)).collect(); + assert_eq!(listed, [("bin".into(), Kind::Dir), ("etc".into(), Kind::Dir), ("share".into(), Kind::Dir)]); + let bin: Vec = v.list("bin").unwrap().into_iter().map(|(n, _)| n).collect(); + assert_eq!(bin, ["ls", "toybox"]); + assert_eq!(v.lstat("share/fonts").unwrap().kind, Kind::Dir); + assert_eq!(v.lstat("nothing"), Err(Error::NotFound)); + assert!(v.describe().starts_with(toyos_osrelease::ROOT_SAID)); + } + + /// Every request that would change ROOT is refused read-only, and an open + /// that would make or empty a file is refused before anything is opened. + #[test] + fn every_change_is_refused() { + let mut v = root(); + assert!(!v.writable()); + let create = OpenHow { create: true, ..READ }; + assert_eq!(v.open("etc/new", create), Err(Error::ReadOnly)); + assert_eq!(v.open("bin/toybox", OpenHow { truncate: true, ..READ }), Err(Error::ReadOnly)); + assert_eq!(v.open("bin/toybox", OpenHow { create_new: true, ..READ }), Err(Error::AlreadyExists)); + let node = v.open("bin/toybox", READ).unwrap(); + assert_eq!(v.write(node, 0, b"x"), Err(Error::ReadOnly)); + assert_eq!(v.truncate(node, 0), Err(Error::ReadOnly)); + assert_eq!(v.mkdir("d"), Err(Error::ReadOnly)); + assert_eq!(v.rmdir("bin"), Err(Error::ReadOnly)); + assert_eq!(v.unlink("bin/ls"), Err(Error::ReadOnly)); + assert_eq!(v.rename("bin/ls", "bin/dir"), Err(Error::ReadOnly)); + assert_eq!(v.symlink("x", "y"), Err(Error::ReadOnly)); + assert_eq!(read_all(&mut v, "bin/toybox"), big(), "a refused write changed nothing"); + } + + /// Bytes that are no volume, and a block past them, are refusals and never + /// a panic of this server. + #[test] + fn bytes_that_are_no_volume_or_too_few_are_refused() { + assert!(RootVolume::mount(Box::leak(vec![0u8; 8 * BLOCK].into_boxed_slice())).is_err()); + let bytes = Bytes(Box::leak(vec![1u8; 2 * BLOCK].into_boxed_slice())); + let mut buf = BlockBuf::zeroed(); + assert!(bytes.read_block(BlockNum::new(1), &mut buf).is_ok()); + assert!(bytes.read_block(BlockNum::new(2), &mut buf).is_err()); + assert!(bytes.read_block(BlockNum::new(u64::MAX), &mut buf).is_err()); + assert!(bytes.write_block(BlockNum::new(0), &buf).is_err()); + } + + /// A node closed by its last holder is gone; a second open is a fresh one. + #[test] + fn a_closed_node_is_stale() { + let mut v = root(); + let node = v.open("bin/toybox", READ).unwrap(); + v.close(node).unwrap(); + assert_eq!(v.node_meta(node), Err(Error::Stale)); + assert!(v.open("bin/toybox", READ).is_ok()); + } +} diff --git a/system/grants/src/main.rs b/system/grants/src/main.rs index 1c5bd679391..90e4ddb0594 100644 --- a/system/grants/src/main.rs +++ b/system/grants/src/main.rs @@ -1,5 +1,5 @@ //! `/system/bin/grants`: which folder of the session's home an installed -//! package is granted, asked of `/system/bin/supervisor`, which alone keeps +//! package is granted, asked of the supervisor, which alone keeps //! them (`toyos_manifest::grants`). //! //! - `grants list`: every grant, one line each: package, access, folder. diff --git a/system/logkeeper/src/main.rs b/system/logkeeper/src/main.rs index 9d0245fbaa6..ed0b6c453f6 100644 --- a/system/logkeeper/src/main.rs +++ b/system/logkeeper/src/main.rs @@ -12,7 +12,7 @@ //! (`toyos_logstream`'s header is the form): //! //! - **the kernel's records**, read off its ring with `SYS_LOG_READ`; -//! - **every program's records**, read off the log ring `/system/bin/supervisor` made +//! - **every program's records**, read off the log ring the supervisor made //! for it and sent here with the manifest's name for the program and its pid, //! on a connection only the supervisor holds ([`toyos_logstream::ORIGINS`]). A record is //! that program's because it came out of that ring ([`origin`]). This diff --git a/system/netstack/i219/src/lib.rs b/system/netstack/i219/src/lib.rs index 113b366195b..1b3ec10487f 100644 --- a/system/netstack/i219/src/lib.rs +++ b/system/netstack/i219/src/lib.rs @@ -758,7 +758,7 @@ fn phy_configured(regs: &R, clock: &C, since: u64) -> Op /// Which part the claim is on. /// -/// **The parent's answer and never a probe**: `/system/bin/supervisor` moved a claim +/// **The parent's answer and never a probe**: the supervisor moved a claim /// on a declared vendor and device into this process, and a driver that read /// the register file to work out which part it was on would be guessing at the /// registers it does not yet trust. diff --git a/system/netstack/src/main.rs b/system/netstack/src/main.rs index f73f265d04c..6e0233996cf 100644 --- a/system/netstack/src/main.rs +++ b/system/netstack/src/main.rs @@ -60,7 +60,7 @@ use client::{Client, ClientRx, HANDSHAKE_TIMEOUT, MAX_PENDING_CONNS, PendingConn /// identifies the card rather than by the slot firmware put it in, and each /// with the driver that opens it. The manifest row spells the same matcher and /// the claim arrives under its text, so which of these exists is -/// `/system/bin/supervisor`'s answer and not this program's — at most one is +/// the supervisor's answer and not this program's — at most one is /// ever endowed, and a machine with none is a machine netstack leaves. /// /// `1af4:1041` is virtio's transitional device id `1000 + 1` for a network diff --git a/system/supervisor/lifecycle/src/lib.rs b/system/supervisor/lifecycle/src/lib.rs index ffe204e5b39..0af9d7e203a 100644 --- a/system/supervisor/lifecycle/src/lib.rs +++ b/system/supervisor/lifecycle/src/lib.rs @@ -1,4 +1,4 @@ -//! The supervisor's lifecycle decisions, pure: `/system/bin/supervisor` holds +//! The supervisor's lifecycle decisions, pure: the supervisor holds //! the handles, makes the spawns and runs the loop, and asks here what to do. //! //! [`restart`] is when a process that ended is started again, [`claims`] which diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 3693fe1d3ed..49fa1ecba7f 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -101,7 +101,7 @@ use toyos_manifest::grants::{self, Answer, Decision, Store}; use toyos_manifest::launch::{self as authority, Authority, Session, Sessions, Target}; use toyos_manifest::package::{self, Package}; use toyos_manifest::device::{Entry, PciMatch}; -use toyos_manifest::{Manifest, Program, View}; +use toyos_manifest::{Manifest, Program, Source, View}; use toyos_supervisor::claims::{self, Missing, Unresolved}; use toyos_supervisor::drives::{self, Act, Drives}; use toyos_supervisor::restart::{self, AfterEnd, Budget}; @@ -125,8 +125,8 @@ use toyos_logstream::{ }; use toyos_abi::inventory::{Arrival, Pci, PciAddr, Record}; use toyos_abi::syscall::{ - DeviceRequest, FileType, SyscallError, DEV_PREFIX, JOB_LABEL, MAX_BADGE, PROVIDE_PREFIX, SERVE_PREFIX, - SVC_LABEL, SYSCAP_LABEL, + DeviceRequest, FileType, SyscallError, DEV_PREFIX, FILESERVER_IMAGE_LABEL, JOB_LABEL, MAX_BADGE, PROVIDE_PREFIX, + ROOT_IMAGE_LABEL, SERVE_PREFIX, SVC_LABEL, SYSCAP_LABEL, }; /// netstack's port, whose every connector the supervisor mints for one row and @@ -403,47 +403,22 @@ fn make_dir(path: &str) -> std::io::Result<()> { fn main() { // Before anything is started, and before the supervisor says anything: from here // the supervisor's own lines are records in its ring. - let log = Log::open(); - say_release(); + let mut log = Log::open(); let syscap: SysCap = Endowments::get() .take(SYSCAP_LABEL) .expect("supervisor: the kernel spawns this program holding the system capability"); + let boot: &'static Boot = Box::leak(Box::new(Boot::take())); - let bytes = read_root(toyos_manifest::GUEST_PATH) - .unwrap_or_else(|e| panic!("supervisor: cannot read {}: {e:?}", toyos_manifest::GUEST_PATH)); - let text = String::from_utf8(bytes) - .unwrap_or_else(|_| panic!("supervisor: {} is not text", toyos_manifest::GUEST_PATH)); - // For the machine's life, and `'static` so a launch's path can be resolved - // against it on the supervisor's file worker. - let system: &'static Manifest = Box::leak(Box::new(toyos_manifest::parse(&text))); - - // Before anything is spawned, and for every `serves` name in the manifest - // rather than only the ones `[boot] start` names: the filepicker is - // launched by the compositor, and an editor holding its connector must be - // able to ask for a file before the picker has run an instruction. - let mut acceptors = BTreeMap::new(); - let mut connectors: BTreeMap<&str, Connector> = BTreeMap::new(); - let names = system - .served_names() - .into_iter() - .chain(system.supervisor_serves.iter().map(String::as_str)); - for name in names { - let (acceptor, connector) = - port::create().unwrap_or_else(|e| panic!("supervisor: no port for `{name}`: {e:?}")); - acceptors.insert(name, acceptor); - connectors.insert(name, connector); - } - // Its connector goes at once: every `netstack` a program holds is one - // minted with that program's row and session (`Grants::netstack`). - drop(connectors.remove(NETSTACK)); - - // One port per file-server role, made here for the same reason: a + // **Everything the supervisor's own files and the SYSTEM server need is + // made before a file is read**, since the manifest is a file of `/system` + // and SYSTEM is what serves it. One port per file-server role, then: a // program's first open works whether or not its server runs yet. Its // connector goes at once: every connector a program holds to it is a - // grant minted for that program's share (`Grants`). + // grant minted for that program's share (`Grants`). A role no row serves + // has its port closed once the manifest says so. let mut role_acceptors: BTreeMap<&str, Acceptor> = BTreeMap::new(); - for role in system.programs.iter().flat_map(|p| p.roles.iter()) { + for (role, _) in toyos_manifest::ROLES { let (acceptor, _) = port::create().unwrap_or_else(|e| panic!("supervisor: no port for the `{role}` role: {e:?}")); role_acceptors.insert(role, acceptor); } @@ -455,7 +430,7 @@ fn main() { let whole = toyos_manifest::whole_tree(); let own: Vec<(String, Connector)> = whole .iter() - .filter_map(|view| Some((role_acceptors.get(view.role)?, view))) + .map(|view| (&role_acceptors[view.role], view)) .map(|(acceptor, view)| mint(acceptor, authority::SUPERVISOR_SHARE, view)) .collect(); let build = || { @@ -471,13 +446,77 @@ fn main() { } Box::leak(Box::new(build())) }; - // Its connector goes at once: every launcher a program holds is one minted // with that program's row. let (launcher, _) = port::create().expect("supervisor: no port for `launcher`"); - let grants_port = acceptors - .remove(grants::PORT) - .expect("supervisor: the manifest declares the supervisor serves `grants`"); + let (grants_port, grants_connector) = port::create().expect("supervisor: no port for `grants`"); + let minted = Minted { launcher, grants: grants_port }; + let mut sessions = Sessions::default(); + + // SYSTEM, from the file server's boot image: what serves `/system`. No + // manifest row holds it, since it serves the manifest. + let system_row: &'static Program = Box::leak(Box::new(system_row())); + let acceptor = role_acceptors.remove(toyos_manifest::SYSTEM).expect("SYSTEM is a role"); + let mut system_service = Service::new( + system_row, + Some(toyos_manifest::SYSTEM), + vec![(format!("{CAPABILITY_PREFIX}{}", toyos_manifest::SYSTEM), acceptor)], + sessions.machine(), + ); + let no_grants = Grants { roles: Vec::new(), block: None, netstack: None }; + let unread = Manifest::default(); + let started = system_service.spawn( + &system_row.source, + &[], + Vec::new(), + &unread, + &syscap, + &BTreeMap::new(), + &no_grants, + &minted, + boot, + &mut log, + ); + if let Err(e) = started { + bootstrap_fails(format!("the file server serving /system did not start: {e}")); + } + + say_release(&system_service); + let text = read_system(toyos_manifest::GUEST_PATH, &system_service); + // For the machine's life, and `'static` so a launch's path can be resolved + // against it on the supervisor's file worker. + let system: &'static Manifest = Box::leak(Box::new(toyos_manifest::parse(&text))); + // A role no row serves: closed before anything asks it, so a connect to it + // answers `ServerGone` rather than waiting in a queue nothing will ever + // accept from. + role_acceptors.retain(|role, _| system.programs.iter().any(|p| p.roles.iter().any(|r| r == role))); + + // Before anything is spawned, and for every `serves` name in the manifest + // rather than only the ones `[boot] start` names: the filepicker is + // launched by the compositor, and an editor holding its connector must be + // able to ask for a file before the picker has run an instruction. + // `grants` was made above, before the manifest said the supervisor serves it. + assert!( + system.supervisor_serves.iter().any(|name| name == grants::PORT), + "supervisor: the manifest declares the supervisor serves `grants`" + ); + let mut acceptors = BTreeMap::new(); + let mut connectors: BTreeMap<&str, Connector> = BTreeMap::from([(grants::PORT, grants_connector)]); + let names = system + .served_names() + .into_iter() + .chain(system.supervisor_serves.iter().map(String::as_str)) + .filter(|name| *name != grants::PORT); + for name in names { + let (acceptor, connector) = + port::create().unwrap_or_else(|e| panic!("supervisor: no port for `{name}`: {e:?}")); + acceptors.insert(name, acceptor); + connectors.insert(name, connector); + } + // Its connector goes at once: every `netstack` a program holds is one + // minted with that program's row and session (`Grants::netstack`). + drop(connectors.remove(NETSTACK)); + // The supervisor alone holds a connector to it, so a connection on it is // the supervisor's question and nobody else's. let consent = connectors.remove(consent::PORT).map(|connector| { @@ -486,18 +525,21 @@ fn main() { .finish() .expect("supervisor: no namespace for the consent port") }); + let mut grants = no_grants; + grants.roles.push((toyos_manifest::SYSTEM, Rc::clone(&system_service.ports))); let mut supervisor = Supervisor { system, - minted: Minted { launcher, grants: grants_port }, + minted, store: Store::default(), consent, syscap: &syscap, + boot, acceptors, connectors, - grants: Grants { roles: Vec::new(), block: None, netstack: None }, - sessions: Sessions::default(), + grants, + sessions, files, - services: Vec::new(), + services: vec![system_service], drivers: drives::matchers(system), drives: Drives::default(), instances: Vec::new(), @@ -613,6 +655,8 @@ struct Supervisor<'a> { /// the screen on. consent: Option, syscap: &'a SysCap, + /// The images the kernel handed the supervisor. + boot: &'static Boot, /// The `serves` acceptors nobody has been started with yet, which a launch /// takes by move. acceptors: BTreeMap<&'a str, Acceptor>, @@ -663,9 +707,9 @@ struct Service<'a> { program: &'a Program, /// The file-server role this process serves, for a row with `roles`. role: Option<&'a str>, - /// The binary the running process was started from: the row's path, or a - /// swap's installed one. - path: String, + /// What the running process was started from: the row's source, or a + /// swap's installed binary. + source: Source, /// `None` once neither binary would start. child: Option, /// The row's devices the running process was endowed: what a process @@ -687,7 +731,7 @@ impl<'a> Service<'a> { Self { program, role, - path: program.path.clone(), + source: program.source.clone(), child: None, devices: Vec::new(), ports: Rc::new(RefCell::new(acceptors)), @@ -697,13 +741,13 @@ impl<'a> Service<'a> { } } - /// Start `path` holding this service's manifest row, and answer its pid. + /// Start `source` holding this service's manifest row, and answer its pid. /// /// `owed` is what the process this start follows held: a start refused any /// of it is no start, so a service never runs on without a device it had. fn spawn( &mut self, - path: &str, + source: &Source, owed: &[String], block: Vec, system: &Manifest, @@ -711,6 +755,7 @@ impl<'a> Service<'a> { connectors: &BTreeMap<&str, Connector>, grants: &Grants<'_>, minted: &Minted, + boot: &Boot, log: &mut Log, ) -> Result { // **Checked again at every start, not only on arrival**: the installed @@ -718,20 +763,28 @@ impl<'a> Service<'a> { // wrote it is not a claim about what is there now. This narrows the // window to the spawn itself and does not close it // (`issues/a-swapped-binary-lives-where-any-process-can-rewrite-it.md`). - if let Some(digest) = toyos_swap::installed_digest(path) { + if let Some((path, digest)) = source.path().and_then(|path| Some((path, toyos_swap::installed_digest(path)?))) { let bytes = read_binary(path).map_err(|why| std::io::Error::other(why.to_string()))?; toyos_swap::verify(&bytes, &digest).map_err(|why| { std::io::Error::other(format!("{path} no longer holds what was installed: {why}")) })?; } - let mut storage = storage_endowment(self.role, syscap, grants)?; + let mut storage = storage_endowment(self.role, syscap, grants, boot)?; if is_block(self.program) { storage.args.extend(block); } let ports = self.ports.borrow(); let served = Served::Keep { acceptors: &ports, owed }; + let command = match source { + Source::Path(path) => Command::new(path), + Source::BootFileServer => { + let mut command = Command::new(&self.program.name); + command.boot_image(boot.fileserver.as_handle().0); + command + } + }; let (child, devices) = start( - Command::new(path), + command, self.program, system, syscap, @@ -746,7 +799,7 @@ impl<'a> Service<'a> { drop(ports); let pid = child.id(); self.child = Some(child); - self.path = path.to_string(); + self.source = source.clone(); self.devices = devices; Ok(pid) } @@ -804,7 +857,7 @@ enum Phase { Answered { conn: Connection, rx: LaunchRx, since: Instant }, /// The new binary runs; it is the service if it is still running at /// `until`, and `previous` is started again if it is not. - Probation { until: Instant, previous: String, owed: Vec }, + Probation { until: Instant, previous: Source, owed: Vec }, } impl Flight { @@ -895,8 +948,12 @@ impl<'a> Supervisor<'a> { self.make_home(program); let block = self.block_args(); let service = &mut self.services[at]; + // SYSTEM was started before the manifest was read. + if service.child.is_some() { + continue; + } let started = service.spawn( - &program.path, + &program.source, &[], block, system, @@ -904,6 +961,7 @@ impl<'a> Supervisor<'a> { &self.connectors, &self.grants, &self.minted, + self.boot, &mut self.log, ); match started { @@ -1000,7 +1058,8 @@ impl<'a> Supervisor<'a> { let launcher = (&self.minted, self.sessions.machine()); let served = Served::Keep { acceptors: &[], owed: &[] }; let output = Output::Boot(&mut self.log); - start(Command::new(&row.path), row, self.system, self.syscap, served, &self.connectors, &self.grants, &[], extra, launcher, output) + let path = row.source.path().ok_or_else(|| format!("{} runs {}, and a `drives` row runs a file", row.name, row.source))?; + start(Command::new(path), row, self.system, self.syscap, served, &self.connectors, &self.grants, &[], extra, launcher, output) .map(|(child, _)| (child, stop)) .map_err(|e| e.to_string()) } @@ -1063,7 +1122,8 @@ impl<'a> Supervisor<'a> { grants::Request::Add { package, folder } => { let (system, name) = (self.system, package.clone()); let binary = self.files("a grant's files", move || -> Result { - binary_digest(&installed(system, &name)?.path) + let row = installed(system, &name)?; + binary_digest(row.source.path().ok_or_else(|| format!("{name} runs {}", row.source))?) })??; let said = format!("{package} is granted {} {}", folder.path, folder.access); self.keep(&package, binary, Answer::Granted(folder))?; @@ -1446,7 +1506,9 @@ impl<'a> Supervisor<'a> { // request leaves nothing staged behind it. let bytes = read_binary(&request.staged); let _ = std::fs::remove_file(&request.staged); - let Some(index) = self.services.iter().position(|s| s.program.name == name) else { + // Not SYSTEM, which no manifest row declares: it runs the signed boot + // image whatever is installed. + let Some(index) = self.services.iter().position(|s| s.program.name == name && s.role != Some(toyos_manifest::SYSTEM)) else { return refuse(name, Refusal::NotAService(name.to_string())); }; if let Some(busy) = flight { @@ -1470,7 +1532,7 @@ impl<'a> Supervisor<'a> { return refuse(name, why); } let installed = toyos_swap::installed_path(name, &request.digest); - if installed == service.path { + if service.source.path() == Some(installed.as_str()) { return refuse(name, Refusal::AlreadyRuns(installed)); } if let Err(why) = install(&installed, &request.digest, &bytes) { @@ -1483,7 +1545,7 @@ impl<'a> Supervisor<'a> { "{installed} ({} bytes, sha256 {}) replaces {} (pid {})", bytes.len(), toyos_swap::hex(&request.digest), - service.path, + service.source, service.pid().map_or(0, |p| p) ), ); @@ -1523,7 +1585,7 @@ impl<'a> Supervisor<'a> { /// Stop the service and start the installed binary in its place. fn cut_over(&mut self, index: usize, path: String) -> Option { let name = self.services[index].program.name.clone(); - let previous = self.services[index].path.clone(); + let previous = self.services[index].source.clone(); // What the process being stopped holds, which its replacement and the // binary a failed swap starts again are each owed. let owed = self.services[index].devices.clone(); @@ -1546,7 +1608,7 @@ impl<'a> Supervisor<'a> { let _ = old.wait(); } let started = service.spawn( - &path, + &Source::Path(path.clone()), &owed, block, self.system, @@ -1554,6 +1616,7 @@ impl<'a> Supervisor<'a> { &self.connectors, &self.grants, &self.minted, + self.boot, &mut self.log, ); match started { @@ -1580,7 +1643,7 @@ impl<'a> Supervisor<'a> { /// Probation is over: the new binary is the service, or the one it /// replaced is started again. - fn end_probation(&mut self, index: usize, path: &str, previous: &str, owed: &[String]) { + fn end_probation(&mut self, index: usize, path: &str, previous: &Source, owed: &[String]) { let service = &mut self.services[index]; let name = service.program.name.clone(); // The loop watches its end from here, and an end before this answer @@ -1596,7 +1659,7 @@ impl<'a> Supervisor<'a> { Word::InService, &format!("{path} as pid {}", service.pid().map_or(0, |p| p)), ); - if previous != path { + if let Some(previous) = previous.path().filter(|&previous| previous != path) { forget(previous); } } @@ -1770,12 +1833,12 @@ impl<'a> Supervisor<'a> { ); } AfterEnd::Restart => { - let (path, owed, program) = (service.path.clone(), service.devices.clone(), service.program); + let (source, owed, program) = (service.source.clone(), service.devices.clone(), service.program); self.make_home(program); let block = self.block_args(); let service = &mut self.services[index]; let (system, syscap, connectors, grants) = (self.system, self.syscap, &self.connectors, &self.grants); - match service.spawn(&path, &owed, block, system, syscap, connectors, grants, &self.minted, &mut self.log) { + match service.spawn(&source, &owed, block, system, syscap, connectors, grants, &self.minted, self.boot, &mut self.log) { Ok(new) => say!("supervisor: {label} (pid {pid}) ended; started again as pid {new}"), Err(e) => { service.ports.borrow_mut().clear(); @@ -1804,13 +1867,13 @@ impl<'a> Supervisor<'a> { /// Start the binary a failed swap replaced, or close the service's ports /// when that will not start either. - fn restore(&mut self, index: usize, previous: &str, owed: &[String]) { + fn restore(&mut self, index: usize, previous: &Source, owed: &[String]) { let program = self.services[index].program; self.make_home(program); let block = self.block_args(); let service = &mut self.services[index]; let name = service.program.name.clone(); - match service.spawn(previous, owed, block, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log) { + match service.spawn(previous, owed, block, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, self.boot, &mut self.log) { Ok(pid) => { service.swapping = false; swapped(&name, Word::Restored, &format!("{previous} as pid {pid}")); @@ -2043,12 +2106,15 @@ impl Supervisor<'_> { resolve(system, &path, |target| -> Result<_, authority::Refusal> { let starts = authority::may_start(row, session, target)?; let (Target::Row(program) | Target::Package(program)) = target; - let prepared = command.image_from(Path::new(&program.path)).prepare().map(drop); + let prepared = match program.source.path() { + Some(path) => command.image_from(Path::new(path)).prepare().map(drop), + None => Err(std::io::Error::other(format!("{} runs {}, which no launch starts", program.name, program.source))), + }; let binary = matches!(target, Target::Package(_)).then(|| { command .prepared_image() .map(|bytes| toyos_swap::hex(&toyos_swap::digest(bytes))) - .ok_or_else(|| format!("{} was not read before its start, so no grant can name it", program.path)) + .ok_or_else(|| format!("{} was not read before its start, so no grant can name it", program.source)) }); Ok((starts, binary, prepared.map(|()| command))) }) @@ -2389,7 +2455,7 @@ impl Drop for Moved { /// named that row's binary — directly, or through a link that lands on it. fn declared<'a>(system: &'a Manifest, path: &str) -> Option<&'a Program> { let key = |p: &str| p.rsplit('/').next().unwrap_or(p).to_string(); - let row = |p: &str| system.program(&key(p)).filter(|program| program.path == p); + let row = |p: &str| system.program(&key(p)).filter(|program| program.source.path() == Some(p)); if let Some(program) = row(path) { return Some(program); } @@ -2435,8 +2501,8 @@ fn resolve<'a, V>(system: &'a Manifest, path: &str, judge: impl FnOnce(Target<'_ Ok(row) => row, Err(why) => return Resolved::Refused(why), }; - if row.path != path { - return Resolved::Refused(format!("{} launches {:?} and this asks for {path:?}", Package::path(name), row.path)); + if row.source.path() != Some(path) { + return Resolved::Refused(format!("{} launches {} and this asks for {path:?}", Package::path(name), row.source)); } let verdict = judge(Target::Package(&row)); Resolved::Package(row, verdict) @@ -2605,7 +2671,7 @@ fn start<'a>( launcher: (&Minted, Session), output: Output<'_>, ) -> Result<(Child, Vec), StartError> { - let Extra { args, claims, ports, stop } = extra; + let Extra { args, claims, root, ports, stop } = extra; // A storage row's own arguments first: a file server's role leads its argv. command.args(&args); command.args(&program.args); @@ -2648,6 +2714,10 @@ fn start<'a>( command.endow(toyos_quiesce::DRIVER_STOP, raw.0); held.0.push(raw); } + if let Some(root) = root { + command.endow(ROOT_IMAGE_LABEL, root.0); + held.0.push(root); + } let mut view = grants.view(program, launcher.1); view.extend(ports); view.extend(grants.netstack(program, launcher.1)); @@ -3152,6 +3222,8 @@ fn swapped(service: &str, word: Word, detail: &str) { struct Extra { args: Vec, claims: Vec<(String, toyos::Device)>, + /// SYSTEM's ROOT image: a duplicate the start owns until the spawn moves it. + root: Option, ports: Vec<(String, Connector)>, /// A disk driver's end of its [`toyos_quiesce::DRIVER_STOP`] pipe. stop: Option, @@ -3191,15 +3263,15 @@ fn guid_text(guid: [u8; 16]) -> String { toyos_gpt::Guid(guid).to_string() } -/// A file server's arguments and block connector for one start. +/// A file server's arguments and the volume it is endowed for one start. /// -/// A file server is told its role, and for LOG and BOOT the partition's GUID +/// A file server is told its role. SYSTEM is handed ROOT's image. For LOG and BOOT, the partition's GUID /// the loader named, with `block` minted for that partition alone, writing for /// the log and reading for the boot volume; DATA it finds by type, through /// `block` minted for every DATA partition. A log or boot role the loader /// named no partition for, and a block port closed for good, each refuse the /// start: the role is then absent, never served from memory. -fn storage_endowment(role: Option<&str>, syscap: &SysCap, grants: &Grants<'_>) -> Result { +fn storage_endowment(role: Option<&str>, syscap: &SysCap, grants: &Grants<'_>, boot: &Boot) -> Result { use toyos_abi::inventory::Role; let mut storage = Extra::default(); let Some(role) = role else { @@ -3207,6 +3279,12 @@ fn storage_endowment(role: Option<&str>, syscap: &SysCap, grants: &Grants<'_>) - }; storage.args.push(role.to_string()); let served = match role { + toyos_manifest::SYSTEM => { + let root = toyos_abi::syscall::dup(boot.root.as_handle()) + .map_err(|e| std::io::Error::other(format!("ROOT's image did not duplicate: {e:?}")))?; + storage.root = Some(root); + return Ok(storage); + } "data" => BlockGrant { scope: Scope::Kind(toyos_gpt::Guid::TOYOS_DATA.0), writes: true }, "log" | "boot" => { let records = inventory(syscap).map_err(|why| StartError::Other(std::io::Error::other(why)))?; @@ -3225,35 +3303,13 @@ fn storage_endowment(role: Option<&str>, syscap: &SysCap, grants: &Grants<'_>) - Ok(storage) } -/// A file of ROOT, through the kernel's own `open`. -/// -/// **Not through std**: std resolves a path through this process's namespace, -/// and the first path it resolves fixes that namespace for the process's life. -/// The supervisor builds its namespace out of the manifest, so ROOT's files are read -/// before std is asked anything. -fn read_root(path: &str) -> Result, SyscallError> { - use toyos_abi::syscall::{self, OpenFlags}; - let handle = syscall::open(path.as_bytes(), OpenFlags::READ)?; - let mut bytes = Vec::new(); - let mut buf = [0u8; 4096]; - let read = loop { - match syscall::read(handle, &mut buf) { - Ok(0) => break Ok(()), - Ok(n) => bytes.extend_from_slice(&buf[..n]), - Err(e) => break Err(e), - } - }; - syscall::close(handle); - read.map(|()| bytes) -} - -/// Say which build this machine runs: the first thing it says, so a boot -/// that dies past here names its build in its log. -fn say_release() { +/// Say which build this machine runs: the first file the supervisor reads, +/// so a boot that dies past here names its build in its log. +fn say_release(server: &Service<'_>) { let path = toyos_osrelease::GUEST_PATH; - let bytes = read_root(path).unwrap_or_else(|e| panic!("supervisor: cannot read {path}: {e:?}")); - let release = toyos_osrelease::parse(&bytes) - .unwrap_or_else(|e| panic!("supervisor: {path} is not one the build wrote: {e:?}")); + let text = read_system(path, server); + let release = toyos_osrelease::parse(text.as_bytes()) + .unwrap_or_else(|e| bootstrap_fails(format!("{path} is not one the build wrote: {e:?}"))); say!( "{}{} {}, committed {} UTC, toolchain {}, {}", toyos_osrelease::SAID, @@ -3264,3 +3320,81 @@ fn say_release() { release.arch.machine() ); } + +/// The file `path` of `/system`, read before the loop exists to start SYSTEM +/// again: a server that ended or will not answer within [`FILES_BOUND`] is +/// this boot's end, said by name. +fn read_system(path: &'static str, server: &Service<'_>) -> String { + let (answer, answered) = std::sync::mpsc::channel(); + let reader = std::thread::Builder::new() + .name("bootstrap".into()) + .spawn(move || { + // The receiver is gone once the bound has passed. + let _ = answer.send(std::fs::read_to_string(path)); + }) + .expect("supervisor: no thread to read /system on"); + let read = answered.recv_timeout(FILES_BOUND); + // Answered, so it is on its way out. + if read.is_ok() { + reader.join().expect("supervisor: the bootstrap reader panicked"); + } + match read { + Ok(Ok(text)) => text, + Ok(Err(e)) => bootstrap_fails(format!("cannot read {path}: {e}")), + Err(_) => bootstrap_fails(format!( + "{path} was not read in {} s; {} {}", + FILES_BOUND.as_secs(), + server.label(), + match server.child.as_ref().map(exit_of) { + Some(Some(exit)) => format!("ended, {exit}"), + _ => "is running and did not answer".to_string(), + } + )), + } +} + +/// The boot's end before `logkeeper` runs, which no ring reaches yet: said on +/// the console the kernel gave the supervisor, then a panic like any other. +fn bootstrap_fails(why: String) -> ! { + let line = format!("supervisor: {why}\n"); + let _ = toyos::fd::with(0, |console| toyos_abi::syscall::write(console.handle, line.as_bytes())); + panic!("supervisor: {why}"); +} + +/// The images the kernel hands the supervisor beside the system capability: +/// the file server's executable and ROOT's bytes, which nobody writes +/// (`toyos_abi::boot::BootImage`). Kept for the machine's life, since SYSTEM +/// is started again from them. +/// +/// Handles and not mappings: the supervisor reads neither, it hands them on. +struct Boot { + fileserver: toyos::OwnedHandle, + root: toyos::OwnedHandle, +} + +impl Boot { + fn take() -> Self { + let image = |label: &str| { + let handle: toyos::OwnedHandle = Endowments::get() + .take(label) + .unwrap_or_else(|| panic!("supervisor: the kernel spawns this program holding `{label}`")); + let kind = toyos_abi::syscall::fstat(handle.as_handle()).map(|stat| stat.file_type); + assert_eq!(kind, Ok(FileType::ImmutableMemory), "supervisor: `{label}` is no image nobody writes"); + handle + }; + Self { fileserver: image(FILESERVER_IMAGE_LABEL), root: image(ROOT_IMAGE_LABEL) } + } +} + +/// The row SYSTEM is started under, which no manifest declares since it +/// serves the manifest: the file server's boot image, serving ROOT, started +/// again when it ends, and holding nothing but its role's port and ROOT. +fn system_row() -> Program { + Program { + name: "fileserver".to_string(), + source: Source::BootFileServer, + roles: vec![toyos_manifest::SYSTEM.to_string()], + restart: true, + ..Program::default() + } +} diff --git a/system/swap/src/main.rs b/system/swap/src/main.rs index a164ddf02fe..b2910e4b896 100644 --- a/system/swap/src/main.rs +++ b/system/swap/src/main.rs @@ -1,5 +1,5 @@ //! `/system/bin/swap `: a running service's binary -//! replaced by the one on standard input, handed to `/system/bin/supervisor`, which +//! replaced by the one on standard input, handed to the supervisor, which //! alone can swap it. //! //! An ordinary program: `ssh swap netstack < netstack` diff --git a/system/toybox/src/ps.rs b/system/toybox/src/ps.rs index 8e1e07cc6c3..7d3841408d3 100644 --- a/system/toybox/src/ps.rs +++ b/system/toybox/src/ps.rs @@ -5,7 +5,7 @@ //! the image's `[programs.toybox]` row declares — a config that does not name //! `roster` there builds an image whose `ps` says it cannot and changes nothing //! else. Nothing here asks for the capability: it is either in the endowment -//! table `/system/bin/supervisor` filled at spawn or it does not exist for this process. +//! table the supervisor filled at spawn or it does not exist for this process. //! What the roster says is [`crate::roster`]'s to read. //! //! `free` is the other half of the same syscall and needs none of this: the diff --git a/system/toybox/src/reboot.rs b/system/toybox/src/reboot.rs index db15510b95c..7cb34074f6a 100644 --- a/system/toybox/src/reboot.rs +++ b/system/toybox/src/reboot.rs @@ -1,4 +1,4 @@ -//! Return the machine to firmware, by asking `/system/bin/supervisor`, which has the +//! Return the machine to firmware, by asking the supervisor, which has the //! log made whole first ([`toyos::power`]). **The `power` connector is the //! whole of the authority**: this is `/system/bin/toybox` under another name, //! holding what `[programs.toybox]` declares. diff --git a/system/toybox/src/shutdown.rs b/system/toybox/src/shutdown.rs index 63e48da3bdf..23c351e595f 100644 --- a/system/toybox/src/shutdown.rs +++ b/system/toybox/src/shutdown.rs @@ -1,4 +1,4 @@ -//! Power the machine off, by asking `/system/bin/supervisor`, which has the log made +//! Power the machine off, by asking the supervisor, which has the log made //! whole first ([`toyos::power`]). //! //! **The `power` connector is the whole of the authority.** `/system/bin/shutdown` diff --git a/system/update/copies/src/installing.rs b/system/update/copies/src/installing.rs index 35812eb1ca4..b6c73b25394 100644 --- a/system/update/copies/src/installing.rs +++ b/system/update/copies/src/installing.rs @@ -5,7 +5,7 @@ use toyos_disk::DiskError; use toyos_update::slots::{Slot, Table}; use toyos_disk::{Disk, BLOCK}; -use toyos_update::image::{Header, SIGNED_BYTES}; +use toyos_update::image::{Header, Sections, SIGNED_BYTES}; use toyos_update::slots::{self, Which}; use toyos_update::sig; @@ -47,7 +47,9 @@ const SLOT_A: Slot = Slot { boot: [1; 16], root: [2; 16], version: 100 }; const SLOT_B: Slot = Slot { boot: [3; 16], root: [4; 16], version: 0 }; const KERNEL: &[u8] = b"an update's kernel, whose bytes are no ELF"; -const CMDLINE: &[u8] = b"root=update-test"; +const CMDLINE: &[u8] = b"update-test"; +const SUPERVISOR: &[u8] = b"an update's supervisor"; +const FILESERVER: &[u8] = b"an update's file server"; /// A machine running slot A, its B empty: the table's partition, and B's FAT /// volume and ROOT. @@ -66,12 +68,13 @@ fn root_bytes() -> Vec { (0..3 * BLOCK).map(|i| if i % BLOCK == 0 { (i / BLOCK) as u8 } else { (i * 7) as u8 }).collect() } -/// The image `ssh update` takes: the signed header, then the kernel, -/// the boot parameter and ROOT. +/// The image `ssh update` takes: the signed header, then every +/// section, ROOT last. fn image(seed: &[u8; 32], version: u64) -> Vec { let root = root_bytes(); - let header = Header::of(version, KERNEL, CMDLINE, &root).encode(); - [&header[..], &sig::sign(seed, &header), KERNEL, CMDLINE, &root].concat() + let sections = Sections { kernel: KERNEL, cmdline: CMDLINE, supervisor: SUPERVISOR, fileserver: FILESERVER, root: &root }; + let header = Header::of(version, §ions).encode(); + [&header[..], &sig::sign(seed, &header), KERNEL, CMDLINE, SUPERVISOR, FILESERVER, &root].concat() } fn install_on(table: &mut Mem, boot: &mut Mem, root: &mut Mem, image: &[u8]) -> Result { @@ -117,9 +120,25 @@ fn a_signed_image_is_written_into_the_idle_slot_and_marked() { assert!(complaints.is_empty(), "{}", toyos_fat32_check::describe(&complaints)); assert_eq!(file_on(&mut boot, slots::KERNEL_FILE), KERNEL); assert_eq!(file_on(&mut boot, slots::CMDLINE_FILE), CMDLINE); + assert_eq!(file_on(&mut boot, slots::SUPERVISOR_FILE), SUPERVISOR); + assert_eq!(file_on(&mut boot, slots::FILESERVER_FILE), FILESERVER); assert_eq!(file_on(&mut boot, slots::SIGNED_FILE), image[..SIGNED_BYTES]); } +/// A signed image whose supervisor is not the bytes its header names is +/// refused by that section's name before its slot's volume or table is +/// written, so the machine boots what it booted. +#[test] +fn an_image_whose_supervisor_was_bent_is_refused_and_marks_nothing() { + let (mut table, mut boot, mut root) = machine(); + let (table_before, boot_before) = (table.0.clone(), boot.0.clone()); + let mut bent = image(&SEED, 200); + bent[SIGNED_BYTES + KERNEL.len() + CMDLINE.len()] ^= 1; + let refused = install_on(&mut table, &mut boot, &mut root, &bent).unwrap_err(); + assert!(refused.contains("supervisor is not the bytes its signed header names"), "{refused}"); + assert!(table.0 == table_before && boot.0 == boot_before, "a refused image wrote the table or the volume"); +} + /// An image another key signed is refused before anything is written: every /// partition reads as it did, so the machine boots what it booted. #[test] diff --git a/system/update/src/install/mod.rs b/system/update/src/install/mod.rs index 319ac3d0be1..d38f6f1547f 100644 --- a/system/update/src/install/mod.rs +++ b/system/update/src/install/mod.rs @@ -89,6 +89,8 @@ pub fn install( let kernel = take(input, header.kernel().len, header.kernel().sha256, "kernel")?; let cmdline = take(input, header.cmdline().len, header.cmdline().sha256, "cmdline")?; + let supervisor = take(input, header.supervisor().len, header.supervisor().sha256, "supervisor")?; + let fileserver = take(input, header.fileserver().len, header.fileserver().sha256, "fileserver")?; let streamed = Instant::now(); stream_root(input, &mut *root.disk, header.root().len, header.root().sha256)?; let root_ms = streamed.elapsed().as_millis(); @@ -99,7 +101,14 @@ pub fn install( Err(e) => return Err(format!("the input's end would not read: {e}")), } - write_volume(&mut *boot.disk, &kernel, &cmdline, &signed, now)?; + let files = [ + (slots::SIGNED_FILE, &signed[..]), + (slots::KERNEL_FILE, &kernel), + (slots::CMDLINE_FILE, &cmdline), + (slots::SUPERVISOR_FILE, &supervisor), + (slots::FILESERVER_FILE, &fileserver), + ]; + write_volume(&mut *boot.disk, &files, now)?; root.disk.flush().map_err(|e| format!("slot {}'s ROOT is not durable: {e:?}", idle.letter()))?; boot.disk.flush().map_err(|e| format!("slot {}'s volume is not durable: {e:?}", idle.letter()))?; @@ -148,18 +157,14 @@ fn stream_root(input: &mut impl Read, root: &mut dyn Disk, len: u64, sha256: toy Ok(()) } -/// The kernel, its boot parameter and the signed header onto the idle slot's -/// FAT volume, replacing whatever was there. -fn write_volume(boot: &mut dyn Disk, kernel: &[u8], cmdline: &[u8], signed: &[u8], now: u64) -> Result<(), String> { +/// Every file a slot carries onto the idle slot's FAT volume, replacing +/// whatever was there. +fn write_volume(boot: &mut dyn Disk, files: &[(&str, &[u8])], now: u64) -> Result<(), String> { let mut fs = toyos_fat32::Fat32::mount(volume::Cached::new(boot)) .map_err(|e| format!("the idle slot's volume does not mount: {e:?}"))?; let time = toyos_fat32::FatTime::from_unix_secs(now); fs.create_dir_all("toyos", time).map_err(|e| format!("toyos/ on the idle volume: {e:?}"))?; - for (path, bytes) in [ - (slots::SIGNED_FILE, signed), - (slots::KERNEL_FILE, kernel), - (slots::CMDLINE_FILE, cmdline), - ] { + for &(path, bytes) in files { if fs.exists(path).map_err(|e| format!("{path}: {e:?}"))? { fs.remove(path).map_err(|e| format!("removing the old {path}: {e:?}"))?; } diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index f30ba3011d3..0746f958b65 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -7,7 +7,7 @@ The mechanics live where the work is: profiles and shapes in `tests/common/`, re - **A host that sleeps mid-run is reported, not measured** — a run whose wall clock jumped against the monotonic one reports `INVL` per test and exits 2. A wild outlier *not* marked that way is a real finding. - **A machine-wide kernel panic reds whichever test was running** — that red's name is the workload, never the cause. `QEMU died before ===READY=== (status 0)` is the same thing said silently: a guest that reset itself is a kernel death, and its evidence is the boot log. - **A machine-wide death during boot is reproduced by boots, not by suites** — parallel `bootable.img` guests, each waited on its completion marker and never on a fixed timer; the baseline is measured in the same session as the arm; a death counts whether or not a marker printed, so run guests with `-action reboot=shutdown -action shutdown=pause` and read a silent one's registers over QMP; a defect whose rate is set by interrupts per unit of guest work is measured on the *slowest* instrument — TCG can be the stronger oracle. -- **`/system/bin/supervisor` speaks in every program's name before that program runs** — a predicate keyed on a `: ` prefix is satisfied by the wrong speaker; wait for the whole line, in the constant the assertion also reads. +- **The supervisor speaks in every program's name before that program runs** — a predicate keyed on a `: ` prefix is satisfied by the wrong speaker; wait for the whole line, in the constant the assertion also reads. - **A guest binary cannot ask what a handle it does not hold does** — the probe ends its caller with a handle fault, so it runs in a child, one fault per child. - **A boot's capture has two pieces** — `boot_log()` ends at the ready marker, `run_test`'s capture begins at `===TEST_START===`, and a job's capture is every program's lines of its window, a server's late boot lines among them. - **A guest of an architecture the host is not runs under TCG** (`Arch::accel`) — TCG prices an uncontended atomic read-modify-write unlike hardware, and an x86-64 guest there boots `qemu64`, which has no PCID: every `INVPCID` path is dead, so a change gated on a CPUID feature is unverified by a green TCG suite. diff --git a/tests/common/metal.rs b/tests/common/metal.rs index f314fd00463..4e0f56822f2 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -66,6 +66,10 @@ pub struct Arm { /// fit the bound every boot has, and a measurement's own span for one that /// runs longer on purpose. Two arms on one boot add both. pub adds_ms: u64, + /// A slot file bent in the marked slot, with the other slot carrying the + /// image whole (`super::slots`): a boot whose subject is the loader's + /// refusal and fallback. `None` for the one-slot image every other boot is. + pub bent: Option<&'static str>, } /// The ordinary arm: one boot, and the fields a caller must still say. @@ -79,7 +83,7 @@ pub const fn once( params: &'static [&'static str], jobs: &'static [&'static str], ) -> Arm { - Arm { boot, config, params, jobs, features: &[], last: None, adds_ms: 0 } + Arm { boot, config, params, jobs, features: &[], last: None, adds_ms: 0, bent: None } } /// One boot carrying members that are **discovered rather than registered**. @@ -560,6 +564,7 @@ pub struct Batch { config: &'static str, params: Vec<&'static str>, features: &'static [&'static str], + bent: Option<&'static str>, /// What the runner spawns, in order. pub jobs: Vec, /// What its arms and the members riding it add to the bound of its list. @@ -600,6 +605,7 @@ impl Batch { /// The batch of the boot `boot`, which `who` rides as `(config, params, /// features)`: a new one, or the one an earlier rider made where the two agree. +#[allow(clippy::too_many_arguments)] fn ride<'a>( out: &'a mut BTreeMap, who: &str, @@ -607,22 +613,24 @@ fn ride<'a>( config: &'static str, params: &'static [&'static str], features: &'static [&'static str], + bent: Option<&'static str>, ) -> Result<&'a mut Batch, String> { let batch = out.entry(boot.to_string()).or_insert_with(|| Batch { config, params: params.to_vec(), features, + bent, jobs: Vec::new(), adds_ms: 0, last: None, files: Vec::new(), links: Vec::new(), }); - if batch.config != config || batch.params != params || batch.features != features { + if batch.config != config || batch.params != params || batch.features != features || batch.bent != bent { return Err(format!( - "{who} rides the boot {boot:?} as ({config}, {params:?}, {features:?}) and another rides \ - it as ({}, {:?}, {:?}); one boot is one image", - batch.config, batch.params, batch.features + "{who} rides the boot {boot:?} as ({config}, {params:?}, {features:?}, bent {bent:?}) and another \ + rides it as ({}, {:?}, {:?}, bent {:?}); one boot is one image", + batch.config, batch.params, batch.features, batch.bent )); } Ok(batch) @@ -654,7 +662,7 @@ pub fn batches( for (name, decl) in tests { let Metal { arms, .. } = decl; for arm in *arms { - let batch = ride(&mut out, name, arm.boot, arm.config, arm.params, arm.features)?; + let batch = ride(&mut out, name, arm.boot, arm.config, arm.params, arm.features, arm.bent)?; batch.add(arm.jobs.iter().map(|j| (*j).to_string())); batch.adds_ms += arm.adds_ms; match (batch.last, arm.last) { @@ -682,7 +690,7 @@ pub fn batches( return Err(format!("two shared boots are both named {:?}", boot.boot)); } let who = format!("the shared boot {:?}", boot.boot); - let batch = ride(&mut out, &who, &boot.boot, boot.config, boot.params, boot.features)?; + let batch = ride(&mut out, &who, &boot.boot, boot.config, boot.params, boot.features, None)?; batch.add(boot.members.iter().map(|member| member.job.clone())); batch.adds_ms += boot.members_ms(); batch.files.extend(boot.files.iter().cloned()); @@ -837,7 +845,14 @@ fn build( params.push(&deadline); let mut plan = toyos_build::build::Plan::new(toyos_build::arch::Arch::X86_64, &config, features, ¶ms); plan.machines = toyos_build::microcode::Machines::Benches; - let bytes = toyos_build::build::build_test_image(root, &plan, quiet, &extra); + let bytes = match batch.bent { + None => toyos_build::build::build_test_image(root, &plan, quiet, &extra), + Some(file) => { + let parts = toyos_build::build::build_test_parts(root, &plan, quiet, &extra); + let signing = toyos_build::image::Signing { key: toyos_build::signing::key(), version: plan.version }; + super::slots::bent_fallback(plan.arch, &parts, &plan.params.join(","), signing, file) + } + }; let image = home.join("image.img"); std::fs::write(&image, &bytes).map_err(|e| format!("{}: {e}", image.display()))?; Ok(image) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index de03db65db1..fdc7276f9b2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,5 +21,6 @@ pub mod ps2server; pub mod qemu; pub mod screen; pub mod serial; +pub mod slots; /// A VNC client, as far as resizing a host window takes one. pub mod vnc; diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index ba2940e518b..22dcdab6e0f 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1185,6 +1185,9 @@ pub struct BootOptions { /// The instant the machine's RTC starts at, as QEMU's `-rtc base=` takes /// it, and the host's own UTC with `None`. pub rtc_base: Option<&'static str>, + /// A slot file (`toyos_update::slots::*_FILE`) bent in the marked slot, + /// with the other slot carrying the image whole ([`super::slots`]). + pub bent: Option<&'static str>, } impl BootOptions { @@ -1225,6 +1228,7 @@ impl Default for BootOptions { data_bytes: None, psci_trace: None, rtc_base: None, + bent: None, } } } @@ -1261,6 +1265,9 @@ pub struct QemuInstance { boot_image: PathBuf, /// The variable store this boot copied for itself. vars: PathBuf, + /// The 16550's file, where a machine whose console is virtio-serial keeps + /// what the loader said. + uart_log: PathBuf, boot_log: String, /// This guest's vCPU count, kept so its liveness ceilings can be widened by /// its own oversubscription on a host with fewer cores than vCPUs — see @@ -1291,7 +1298,7 @@ fn kernel_of(options: &BootOptions) -> Vec<&'static str> { toyos_build::build::TEST_KERNEL.to_vec() } -// Eight, because an image is its architecture as much as its files and its kernel. +// Nine, because an image is its architecture and its slots as much as its files and its kernel. #[allow(clippy::too_many_arguments)] fn build_boot_image_with( arch: Arch, @@ -1302,6 +1309,7 @@ fn build_boot_image_with( kernel_features: &[&str], kernel_params: &[&str], debug_wait: bool, + bent: Option<&str>, ) -> Vec { // **The two fields have the same type, so swapping them compiles.** It // happened once, in this file's own conversion: the shared boot handed @@ -1368,7 +1376,15 @@ fn build_boot_image_with( let quiet = !VERBOSE.load(Ordering::Relaxed); let plan = toyos_build::build::Plan::new(arch, config, kernel_features, kernel_params); - toyos_build::build::build_test_image(&compile::repo_root(), &plan, quiet, &extra_files) + let root = compile::repo_root(); + match bent { + None => toyos_build::build::build_test_image(&root, &plan, quiet, &extra_files), + Some(file) => { + let parts = toyos_build::build::build_test_parts(&root, &plan, quiet, &extra_files); + let signing = toyos_build::image::Signing { key: toyos_build::signing::key(), version: plan.version }; + super::slots::bent_fallback(arch, &parts, &plan.params.join(","), signing, file) + } + } } /// Build all binaries in a test crate. @@ -1455,6 +1471,7 @@ impl QemuInstance { &features, ¶ms, options.debug_wait, + options.bent, ); let storage = match (options.profile.shape().storage, options.data_bytes) { (storage, None) => storage, @@ -1650,6 +1667,14 @@ impl QemuInstance { &self.boot_log } + /// What the loader said: the 16550's file so far on a machine whose + /// console is virtio-serial, and [`Self::boot_log`] on one whose console + /// is the serial port the loader writes too. + pub fn loader_log(&self) -> String { + let uart = fs::read_to_string(&self.uart_log).unwrap_or_default(); + format!("{uart}\n{}", self.boot_log) + } + /// The disk image this boot runs from: the stick under a stick profile. /// What the guest wrote to it is whole once [`Self::await_exit`] answers. pub fn boot_image(&self) -> &Path { @@ -2620,15 +2645,16 @@ fn spawn_and_wait_ready(mut qemu: Command, options: BootOptions, files: Files) - wait_for_ready(&mut child, &rx, &options, &uart_log) }; // The ready marker is test-runner's, which the supervisor starts after its - // first line, so a boot that reached it has said its build, and the kernel - // named the ROOT that carries it before that. Both are read off the image - // this boot was handed, never off the checkout, which may have moved since. + // first line, so a boot that reached it has said its build, and the file + // server serving `/system` named the ROOT that carries it before that. + // Both are read off the image this boot was handed, never off the + // checkout, which may have moved since. if options.ready_marker == DEFAULT_READY && !options.mute { let (root, bytes) = toyos_build::image::root_file_on(&boot_image, toyos_osrelease::PATH) .unwrap_or_else(|why| panic!("[qemu] {why}")); let release = toyos_osrelease::parse(&bytes) .unwrap_or_else(|why| panic!("[qemu] {} on {} is not the build's: {why:?}", toyos_osrelease::PATH, boot_image.display())); - let named = format!("boot: root={root}"); + let named = format!("{}{root}", toyos_osrelease::ROOT_SAID); let said = format!( "{}{} {}, committed {} UTC, toolchain {}, {}", toyos_osrelease::SAID, @@ -2657,6 +2683,7 @@ fn spawn_and_wait_ready(mut qemu: Command, options: BootOptions, files: Files) - screendump, boot_image, vars, + uart_log, boot_log, smp: options.smp, carried, diff --git a/tests/common/slots.rs b/tests/common/slots.rs new file mode 100644 index 00000000000..fbf3d145701 --- /dev/null +++ b/tests/common/slots.rs @@ -0,0 +1,71 @@ +//! An image whose marked slot's `file` carries one bent byte and whose other +//! slot carries the same image whole: the loader refuses the marked slot by +//! the section that file is, before it exits boot services, and boots the +//! other. A boot that falls back, rather than one with nothing to boot, is the +//! only refusal a metal bench comes back from, since a loader with no slot +//! powers the machine off. + +use std::io::{Seek, SeekFrom, Write}; + +use toyos_build::build::Parts; +use toyos_build::image::{self, SecondSlot, Signing}; + +/// The bytes of `parts` as such an image, its slot A's `file` (a +/// `toyos_update::slots::*_FILE`) bent. +pub fn bent_fallback( + arch: toyos_build::arch::Arch, + parts: &Parts, + params: &str, + signing: Signing<'_>, + file: &str, +) -> Vec { + let userland = &parts.userland; + let room = SecondSlot { root_bytes: userland.root.len() as u64 }; + let disk = image::create_boot_image(arch, &parts.bootloader, userland.release(&parts.kernel), params, signing, Some(room)); + let scratch = toyos_tmpdir::TempDir::new("bent-slot"); + let path = scratch.join("disk.img"); + std::fs::write(&path, &disk).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + let mut f = std::fs::OpenOptions::new().read(true).write(true).open(&path).expect("the image just written"); + let table = image::slot_table_of(&mut f).expect("the image's slot table"); + let (a, b) = (table.slots[0].expect("slot A"), table.slots[1].expect("slot B")); + let extent = |f: &mut std::fs::File, guid| image::partition_extent(f, guid).expect("a slot's partition"); + let (a_boot, b_boot) = (extent(&mut f, a.boot), extent(&mut f, b.boot)); + let (a_root, b_root) = (extent(&mut f, a.root), extent(&mut f, b.root)); + assert_eq!(a_boot.1, b_boot.1, "the two slots' volumes are one size"); + assert!(b_root.1 >= userland.root.len() as u64, "slot B's ROOT holds slot A's"); + + // Slot B: slot A's volume and ROOT, byte for byte, so it verifies under + // the one signed header both carry. + let mut copy = |from: (u64, u64), to: u64| { + let mut bytes = vec![0u8; from.1 as usize]; + std::io::Read::read_exact(&mut (&disk[from.0 as usize..]), &mut bytes).expect("inside the image"); + f.seek(SeekFrom::Start(to)).and_then(|_| f.write_all(&bytes)).expect("write slot B"); + }; + copy(a_boot, b_boot.0); + copy((a_root.0, userland.root.len() as u64), b_root.0); + + // Slot A: one byte of the file bent where it sits on the volume, found by + // a run of its own bytes that occurs once on that volume. + let carried = image::read_file_on(&mut f, a.boot, file).expect("slot A carries the file"); + let volume = &disk[a_boot.0 as usize..(a_boot.0 + a_boot.1) as usize]; + let once = |probe: &[u8]| { + let mut hits = volume.windows(probe.len()).enumerate().filter(|(_, w)| *w == probe).map(|(i, _)| i); + match (hits.next(), hits.next()) { + (Some(at), None) => Some(at), + _ => None, + } + }; + let at = (1..16) + .map(|sixteenth| carried.len() * sixteenth / 16) + .find_map(|from| once(&carried[from..from + 64])) + .unwrap_or_else(|| panic!("no run of {file}'s bytes occurs once on slot A's volume")); + let byte = a_boot.0 + at as u64; + f.seek(SeekFrom::Start(byte)).and_then(|_| f.write_all(&[volume[at] ^ 1])).expect("bend slot A's file"); + drop(f); + + let bent = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + let mut reread = std::fs::File::open(&path).expect("the bent image"); + assert_ne!(image::read_file_on(&mut reread, a.boot, file).expect("still a file"), carried, "{file} was not bent"); + assert_eq!(image::read_file_on(&mut reread, b.boot, file).expect("slot B's copy"), carried, "slot B's copy was bent"); + bent +} diff --git a/tests/corpus/tinycc/210_stat_identity.c b/tests/corpus/tinycc/210_stat_identity.c index 9899e9e3647..3b021e2445e 100644 --- a/tests/corpus/tinycc/210_stat_identity.c +++ b/tests/corpus/tinycc/210_stat_identity.c @@ -62,7 +62,7 @@ static void judge(const char *what, int is, int want) { } int main(void) { - struct stat a, b, a2, opened, moved, toybox, cat, supervisor; + struct stat a, b, a2, opened, moved, toybox, cat, release; struct stat bin, bin2, etc, sys, tmpdir, catlink, cplink; int fd; @@ -90,10 +90,10 @@ int main(void) { close(fd); if (stat_of("/system/bin/toybox", &toybox) || stat_of("/system/bin/cat", &cat) || - stat_of("/system/bin/supervisor", &supervisor)) + stat_of("/system/etc/os-release", &release)) return 1; judge("a link is the file it names", same(&toybox, &cat), 1); - judge("two files on /system are two", same(&toybox, &supervisor), 0); + judge("two files on /system are two", same(&toybox, &release), 0); judge("/system and /tmp are two volumes", toybox.st_dev == a.st_dev, 0); if (mkdir("/tmp/210_d", 0755) != 0 || stat_of("/system/bin", &bin) || stat_of("/system/etc/../bin", &bin2) || diff --git a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs index d33ff284f54..5ea1bd48bce 100644 --- a/tests/guest/rust-tests/src/bin/abuse_handle_table.rs +++ b/tests/guest/rust-tests/src/bin/abuse_handle_table.rs @@ -28,6 +28,9 @@ //! **A pipe and a port are two inserts answered as one**: at the cap, and one //! slot short of fitting, each is refused with the table as it was. +#[path = "../image.rs"] +mod image; + use toyos_abi::syscall::{ self, EndowEntry, MmapFlags, MmapProt, SpawnArgs, SyscallError, MAX_SPAWN_ENDOWMENTS, QUIT_LABEL, SELF_LABEL, }; @@ -52,10 +55,18 @@ const REGION: usize = 4 * 1024 * 1024; /// be in range or the refusal would be the label check rather than the repeat. const LABELS: &[u8] = b"twice"; +/// This binary as an image, read before the table is filled: a full table has +/// no slot for the object a read makes. +fn own() -> &'static toyos::process::Image { + static OWN: std::sync::OnceLock = std::sync::OnceLock::new(); + OWN.get_or_init(|| image::image_of(SELF)) +} + fn main() { if std::env::args().nth(1).as_deref() == Some(CHILD) { return; } + own(); let region = unsafe { syscall::mmap( core::ptr::null_mut(), @@ -70,6 +81,8 @@ fn main() { const ARGV0: &[u8] = b"/system/bin/echo\0"; unsafe { core::ptr::copy_nonoverlapping(ARGV0.as_ptr(), argv, ARGV0.len()) }; + let echo = image::image_of("/system/bin/echo"); + let (image, image_len) = echo.spawn_words(); let spawn_with = |endow_count: u64| { let args = SpawnArgs { path_ptr: argv as u64, @@ -84,8 +97,8 @@ fn main() { labels_len: LABELS.len() as u64, cwd_ptr: CWD.as_ptr() as u64, cwd_len: CWD.len() as u64, - image: 0, - image_len: 0, + image, + image_len, place: u64::from(toyos_abi::HANDLE_INVALID.0), }; unsafe { syscall::spawn(&args) } @@ -179,6 +192,7 @@ fn a_pair_is_refused_whole(filled: &mut Vec) { /// `entries` into the blob `labels`. fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result { let argv = format!("{SELF}\0{CHILD}"); + let (image, image_len) = own().spawn_words(); // SAFETY: every pointer names a live local for the whole call. unsafe { syscall::spawn(&SpawnArgs { @@ -194,8 +208,8 @@ fn spawn_endowed(entries: &[EndowEntry], labels: &[u8]) -> Result [u8; LEN] { out } -/// A fresh handle on this binary at offset 0, so every `read` has bytes to give. +/// A fresh handle on this binary at offset 0, so every `read` has bytes to give: +/// its copy in the kernel's `/tmp`, made once. fn open_self() -> RawHandle { - syscall::open(SELF_PATH.as_bytes(), OpenFlags::READ).expect("open self") + static OWN: std::sync::OnceLock = std::sync::OnceLock::new(); + let own = OWN.get_or_init(|| image::in_tmp(SELF_PATH)); + syscall::open(own.as_bytes(), OpenFlags::READ).expect("open self") } /// `len` bytes at `addr`, as the buffer a `read` is asked to fill. diff --git a/tests/guest/rust-tests/src/bin/endowment_denied.rs b/tests/guest/rust-tests/src/bin/endowment_denied.rs index 1340c67893b..f8ba99bee95 100644 --- a/tests/guest/rust-tests/src/bin/endowment_denied.rs +++ b/tests/guest/rust-tests/src/bin/endowment_denied.rs @@ -21,7 +21,7 @@ //! reading the roster of every process in the machine, and powering the machine //! off //! — and each is one bit on a handle to a `SysCap` the kernel mints exactly once, -//! for `/system/bin/supervisor`. A handle that carries the wrong *bit* is refused with a word, +//! for the supervisor. A handle that carries the wrong *bit* is refused with a word, //! because probing what an attenuated capability can still do is what //! attenuation is for; a handle that is **no handle at all** ends the caller. //! diff --git a/tests/guest/rust-tests/src/bin/hierarchy_paths.rs b/tests/guest/rust-tests/src/bin/hierarchy_paths.rs index a03a3990be6..715e305420c 100644 --- a/tests/guest/rust-tests/src/bin/hierarchy_paths.rs +++ b/tests/guest/rust-tests/src/bin/hierarchy_paths.rs @@ -66,7 +66,7 @@ fn root_refuses_every_way_of_changing_it() { "rmdir of a mount point was permitted" ); assert_eq!( - syscall::symlink(b"/system/bin/supervisor", b"/intruder"), + syscall::symlink(b"/system/etc/os-release", b"/intruder"), Err(SyscallError::PermissionDenied), "a symlink at / was permitted" ); @@ -74,7 +74,7 @@ fn root_refuses_every_way_of_changing_it() { // The machine is still here, and / still holds what it held. let after = names("/"); assert_eq!(after, ROOT_ENTRIES, "a refused operation changed / to {after:?}"); - assert!(fs::metadata("/system/bin/supervisor").is_ok(), "/system is unreadable after the refusals"); + assert!(fs::metadata("/system/etc/os-release").is_ok(), "/system is unreadable after the refusals"); println!(" PASS create, unlink, rename, mkdir, rmdir and symlink are all refused at /"); } diff --git a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs index b413c16b16b..88eb5d945f3 100644 --- a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs +++ b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs @@ -21,6 +21,9 @@ //! it moved to a child that still lives: the watch stays armed through each, //! and is answered once that holder goes. +#[path = "../image.rs"] +mod image; + use std::io::Read; use std::os::toyos::process::CommandExt; use std::process::{Command, Stdio}; @@ -74,7 +77,8 @@ fn test() { let (client, server) = connection(); refused(client.as_handle(), "a connection"); - let file = syscall::open(SELF_PATH.as_bytes(), OpenFlags::READ).expect("this binary's own file"); + let own = image::in_tmp(SELF_PATH); + let file = syscall::open(own.as_bytes(), OpenFlags::READ).expect("this binary's own file"); refused(file, "a file"); syscall::close(file); println!(" a connection and a file were refused the question"); diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 963a39dfa14..b86719d967f 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -24,6 +24,9 @@ //! //! Every wait is unbounded: the harness ceiling is the only clock. +#[path = "../image.rs"] +mod image; + use std::io::{BufRead, BufReader}; use std::os::toyos::process::{ChildExt, CommandExt}; use std::process::{Child, ChildStdin, Command, Stdio}; @@ -224,6 +227,8 @@ fn launcher_copy() -> RawHandle { /// `SYS_SPAWN` of this binary as a C under `place`, killed again if it starts. fn spawn_under(place: RawHandle) -> Result { let argv = format!("{SELF_PATH}\0c"); + let own = image::image_of(SELF_PATH); + let (image, image_len) = own.spawn_words(); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -237,8 +242,8 @@ fn spawn_under(place: RawHandle) -> Result { labels_len: 0, cwd_ptr: "/".as_ptr() as u64, cwd_len: 1, - image: 0, - image_len: 0, + image, + image_len, place: u64::from(place.0), }; // SAFETY: every pointer names a buffer of this frame that outlives the call. diff --git a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs index da9aaf1fdf5..cf35e7ff939 100644 --- a/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs +++ b/tests/guest/rust-tests/src/bin/spawn_child_ends_first.rs @@ -14,6 +14,9 @@ //! //! Every wait is unbounded: the runner's deadline is the only clock. +#[path = "../image.rs"] +mod image; + use toyos_abi::syscall::{self, debug_action, EndowEntry, SpawnArgs, SyscallError}; use toyos_abi::RawHandle; use toyos_abi::end::End; @@ -89,6 +92,8 @@ fn spawn(role: &str, stdout: Option) -> Result = stdout.into_iter().map(|handle| EndowEntry { label_off: 0, label_len: LABEL.len() as u32, handle, _pad: 0 }).collect(); + let own = image::image_of(SELF_PATH); + let (image, image_len) = own.spawn_words(); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -102,8 +107,8 @@ fn spawn(role: &str, stdout: Option) -> Result Result { let argv = format!("{SELF_PATH}\0child"); + let own = image::image_of(SELF_PATH); + let (image, image_len) = own.spawn_words(); let args = SpawnArgs { path_ptr: SELF_PATH.as_ptr() as u64, path_len: SELF_PATH.len() as u64, @@ -89,8 +94,8 @@ fn spawn_under(place: RawHandle) -> Result { labels_len: 0, cwd_ptr: "/".as_ptr() as u64, cwd_len: 1, - image: 0, - image_len: 0, + image, + image_len, place: u64::from(place.0), }; // SAFETY: every pointer names a buffer of this frame that outlives the call. diff --git a/tests/guest/rust-tests/src/image.rs b/tests/guest/rust-tests/src/image.rs new file mode 100644 index 00000000000..7a6d9a368eb --- /dev/null +++ b/tests/guest/rust-tests/src/image.rs @@ -0,0 +1,28 @@ +//! What a raw `SYS_SPAWN` names as its program and what a raw `SYS_OPEN` +//! opens, now that the kernel opens no file of `/system`: a program read whole +//! into a memory object of this process's own, and a file copied to the +//! kernel's `/tmp`. + +// Each binary takes the half it needs. +#![allow(dead_code)] + +use std::io::Read as _; + +use toyos::process::Image; + +/// `path`, read whole into a memory object of this process's own, which a +/// `SpawnArgs` names by [`Image::spawn_words`]. +pub fn image_of(path: &str) -> Image { + let mut file = std::fs::File::open(path).unwrap_or_else(|e| panic!("open {path}: {e}")); + let len = file.metadata().unwrap_or_else(|e| panic!("stat {path}: {e}")).len(); + Image::read(len, |buf| file.read(buf)).unwrap_or_else(|why| panic!("read {path} into an image: {why:?}")) +} + +/// `path` copied to `/tmp` under its own name, which the kernel opens: the +/// path to open. +pub fn in_tmp(path: &str) -> String { + let name = path.rsplit('/').next().expect("a path names a file"); + let copy = format!("/tmp/{name}"); + std::fs::copy(path, ©).unwrap_or_else(|e| panic!("copy {path} to {copy}: {e}")); + copy +} diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 220fdbda60a..df858e9ad6d 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -135,6 +135,9 @@ pub enum Refusal { Table(String), /// The table does not hold exactly one partition of a type the loop needs. Partitions { what: &'static str, matched: u32 }, + /// The table holds neither one nor two ROOTs, which are the slots the + /// loader boots. + Roots { matched: u32 }, /// The image is armed with a parameter [`FLASHABLE`] does not clear. Armed { name: String }, /// **The image carries no bound on its own boot.** Every metal image is @@ -230,6 +233,9 @@ impl fmt::Display for Refusal { Self::Partitions { what, matched } => { write!(f, "the image carries {matched} {what} partitions and this loop needs one") } + Self::Roots { matched } => { + write!(f, "the image carries {matched} TOYOS-ROOT partitions and this loop needs one or two") + } Self::NoBound { staged_a_wedge } => write!( f, "the image carries no `{}` on its parameter line{}. Every image the \ @@ -899,13 +905,14 @@ pub fn judge_arms(armed: &[String]) -> Result { } } -/// [`whole_disk`], and exactly one partition of each of the three types at the -/// numbers the installed rule names. +/// [`whole_disk`]: exactly one ESP and one TOYOS-LOG at the numbers the +/// installed rule names, one slot table, and one or two ROOTs, the slots the +/// loader boots. fn admit(path: &Path, target: &Target) -> Result { let (mut file, bytes) = whole_disk(path).map_err(Refusal::Disk)?; let esp = one_partition(&mut file, toyos_gpt::Guid::EFI_SYSTEM, "ESP")?; let log = one_partition(&mut file, toyos_gpt::Guid::MICROSOFT_BASIC, "TOYOS-LOG")?; - one_partition(&mut file, toyos_gpt::Guid::TOYOS_ROOT, "TOYOS-ROOT")?; + one_partition(&mut file, toyos_gpt::Guid::TOYOS_SLOTS, "TOYOS-SLOTS")?; for (what, got, want) in [("the ESP", esp.index, target.esp_part), ("TOYOS-LOG", log.index, target.log_part)] { @@ -913,6 +920,12 @@ fn admit(path: &Path, target: &Target) -> Result { return Err(Refusal::PartitionIndex { what, want, got }); } } + let roots = toyos_gpt::locate_type(&mut toyos_build::image::FileSectors(&mut file), toyos_gpt::Guid::TOYOS_ROOT, &mut []) + .map_err(|e| Refusal::Table(format!("{e:?}")))? + .matched; + if !(1..=2).contains(&roots) { + return Err(Refusal::Roots { matched: roots }); + } Ok(Flashable { path: path.to_path_buf(), bytes, esp, log }) } @@ -3062,7 +3075,8 @@ mod tests { std::fs::write(&at, bytes).expect("write"); at }; - let (esp, basic, root) = (toyos_gpt::Guid::EFI_SYSTEM, toyos_gpt::Guid::MICROSOFT_BASIC, toyos_gpt::Guid::TOYOS_ROOT); + let (esp, basic, slots, root) = + (toyos_gpt::Guid::EFI_SYSTEM, toyos_gpt::Guid::MICROSOFT_BASIC, toyos_gpt::Guid::TOYOS_SLOTS, toyos_gpt::Guid::TOYOS_ROOT); assert_eq!( admit(&write("ragged.img", &vec![0u8; 1000]), &t), @@ -3082,24 +3096,36 @@ mod tests { Err(Refusal::Disk(Unfit::BackupHeader { .. })) )); - let two = disk(&[("a", esp), ("b", esp), ("log", basic), ("root", root)]); + let two = disk(&[("a", esp), ("b", esp), ("log", basic), ("slots", slots)]); assert_eq!( admit(&write("two-esps.img", &two), &t), Err(Refusal::Partitions { what: "ESP", matched: 2 }) ); - let no_log = disk(&[("esp", esp), ("root", root)]); + let no_log = disk(&[("esp", esp), ("slots", slots)]); assert_eq!( admit(&write("no-log.img", &no_log), &t), Err(Refusal::Partitions { what: "TOYOS-LOG", matched: 0 }) ); // The log where the installed rule does not name it: p2, not p3. - let moved = disk(&[("esp", esp), ("log", basic), ("root", root)]); + let moved = disk(&[("esp", esp), ("log", basic), ("slots", slots)]); assert_eq!( admit(&write("moved-log.img", &moved), &t), Err(Refusal::PartitionIndex { what: "TOYOS-LOG", want: 3, got: 2 }) ); - // And the shape `build/image.rs` writes, which is the one admitted. - let built = disk(&[("esp", esp), ("root", root), ("log", basic)]); + let rootless = disk(&[("esp", esp), ("slots", slots), ("log", basic)]); + assert_eq!(admit(&write("rootless.img", &rootless), &t), Err(Refusal::Roots { matched: 0 })); + let three = disk(&[("esp", esp), ("slots", slots), ("log", basic), ("a", root), ("b", root), ("c", root)]); + assert_eq!(admit(&write("three-roots.img", &three), &t), Err(Refusal::Roots { matched: 3 })); + let no_table = disk(&[("esp", esp), ("root", root), ("log", basic)]); + assert_eq!( + admit(&write("no-table.img", &no_table), &t), + Err(Refusal::Partitions { what: "TOYOS-SLOTS", matched: 0 }) + ); + // And the shapes `build/image.rs` writes, one slot or two, which are + // the ones admitted. + let two_slots = disk(&[("esp", esp), ("slots", slots), ("log", basic), ("a", root), ("b", root)]); + assert!(admit(&write("two-slots.img", &two_slots), &t).is_ok(), "a two-slot image is refused"); + let built = disk(&[("esp", esp), ("slots", slots), ("log", basic), ("root", root)]); let ok = admit(&write("built.img", &built), &t).expect("the built shape is admitted"); assert_eq!((ok.esp.index, ok.log.index), (1, 3)); assert_eq!(ok.bytes % u64::from(LBA), 0); diff --git a/tests/toyos.rs b/tests/toyos.rs index d8bb58a8aea..4cc2209bfc5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -512,6 +512,17 @@ const MACHINE_TESTS: &[&str] = &[ // `images/console.toml`'s image, which runs no job and hands no machine // back: a metal boot of it ends with a hand on the power button. "console_image_boots", + // The UEFI loader's check of a boot program, which no host test reaches: + // a slot whose supervisor is bent is refused before boot services end, + // and the other slot boots. On both architectures' QEMU machines; the + // `bent_supervisor_falls_back` metal row runs it on a bench. + "a_bent_supervisor_is_refused_and_the_other_slot_boots", + // Memory the kernel frees only when a process's address space goes: a + // child spawned from an image (every program of `/system` is) holds the + // image until then, and a region its parent still maps must not keep + // that space alive. The kernel's object lifetimes, which no host test + // holds; one boot of QEMU's x86-64 machine. + "a_spawn_from_an_image_frees_it_when_the_child_ends", // The boot's log written through usbd onto the stick it booted from, and // still there once the machine has stopped: the path every metal boot's // `/log` takes, which no other QEMU row reads, because their console is @@ -609,6 +620,10 @@ const MACHINE_TESTS: &[&str] = &[ /// than derived from its config and parameters, because sharing is not always /// safe and only the author knows. const METAL: &[(&str, metal::Metal)] = &[ + ( + "bent_supervisor_falls_back", + metal::Metal { arms: BENT_SUPERVISOR, judge: |b| bent_supervisor_fell_back(&b[0].loader(), &b[0].kernel()) }, + ), // ---- one image: tests/images/testcases.toml, no parameters, one job list ---- ( "blackbox_unclaimed_page", @@ -2463,6 +2478,47 @@ fn console_image_boots() -> Result<(), String> { said.must_be_clean() } +/// Megabytes in use, by `free`'s line, after `free` itself has ended. +fn used_mib(qemu: &mut QemuInstance) -> Result { + let said = qemu.run_test("free", Duration::from_secs(30)).stdout; + let line = said.lines().find(|l| l.trim_end().ends_with('M')).ok_or_else(|| format!("free said no figures:\n{said}"))?; + let used = line.split_whitespace().nth(1).and_then(|w| w.strip_suffix('M')?.parse().ok()); + used.ok_or_else(|| format!("free's line {line:?} names no used megabytes")) +} + +/// Eight spawns of a program read from `/system` into an image, each of a +/// child that maps the log ring its parent keeps: what is in use after them is +/// what was before, give or take one 2 MiB image. A child's ended address +/// space kept alive by its parent's ring holds the image, 2 MiB a spawn. +fn a_spawn_from_an_image_frees_it_when_the_child_ends(test_config: &Path) -> Result<(), String> { + let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], BootOptions::default()); + let before = used_mib(&mut qemu)?; + for _ in 0..8 { + used_mib(&mut qemu)?; + } + let after = used_mib(&mut qemu)?; + match after <= before + 2 { + true => Ok(()), + false => Err(format!("{} MiB more is in use after eight spawns than before them: {before} MiB, then {after}", after - before)), + } +} + +/// What a boot whose marked slot's supervisor is bent says: the loader refused +/// that slot by the section, and the kernel was booted from the other. +fn bent_supervisor_fell_back(loader: &serial::Serial, kernel: &serial::Serial) -> Result<(), String> { + let refused = toyos_update::policy::Refusal::Hash(toyos_update::image::SUPERVISOR); + loader.must_say(&format!("Slot A: REFUSED, {refused}"))?; + // `kernel/src/params.rs`'s `SLOT_RECORD`, which the kernel's binary keeps. + kernel.must_say(&format!("boot: slot B, because the marked slot A was refused: {}", refused.word()))?; + Ok(()) +} + +/// [`bent_supervisor_fell_back`] on a bench, whose loader keeps its own log. +const BENT_SUPERVISOR: &[metal::Arm] = &[metal::Arm { + bent: Some(toyos_update::slots::SUPERVISOR_FILE), + ..metal::once("bentsupervisor", "tests/images/testcases.toml", &[], &[]) +}]; + /// Boot `tests/images/testcases.toml` from a USB stick, as a metal boot does, /// write the stick through usbd ([`metal::STICK_LOAD_JOB`]), press the power /// button while it writes, and read the stick's log volume back with the metal @@ -3782,7 +3838,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re "percpu: BSP cpu_id=0", "GIC: v", "clock: the generic timer counts at", - "spawned /system/bin/supervisor pid=", + "spawned the supervisor pid=", SPAWNED, ] { if !serial.contains(want) { @@ -4121,6 +4177,16 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "bar_map_behind_a_bridge" => bar_map_behind_a_bridge(test_config), "lent_device_memory_refused" => lent_device_memory_refused(test_config), "console_image_boots" => console_image_boots(), + "a_spawn_from_an_image_frees_it_when_the_child_ends" => a_spawn_from_an_image_frees_it_when_the_child_ends(test_config), + "a_bent_supervisor_is_refused_and_the_other_slot_boots" => { + [qemu::Profile::Headless, qemu::Profile::Virt].into_iter().try_for_each(|profile| { + let options = BootOptions { profile, bent: Some(toyos_update::slots::SUPERVISOR_FILE), ..Default::default() }; + let qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); + let loader = serial::Serial::named("loader's serial", qemu.loader_log()); + let kernel = serial::Serial::named("boot's console", qemu.boot_log().to_string()); + bent_supervisor_fell_back(&loader, &kernel) + }) + } "the_log_lands_on_the_boot_stick" => the_log_lands_on_the_boot_stick(test_config), "a_usb_key_reaches_the_console" => a_usb_key_reaches_the_console(), "desktop_without_x2apic" => desktop_without_x2apic(),