diff --git a/Cargo.lock b/Cargo.lock index ff8266e5bee..5fdda3642ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,17 @@ dependencies = [ "ctutils", ] +[[package]] +name = "disks" +version = "0.1.0" +dependencies = [ + "diskserver", + "toyos", + "toyos-blockring", + "toyos-gpt", + "toyos-manifest", +] + [[package]] name = "diskserver" version = "0.0.0" @@ -771,6 +782,7 @@ dependencies = [ "toyos-blockdev", "toyos-blockring", "toyos-blockserve", + "toyos-gpt", ] [[package]] @@ -3540,6 +3552,7 @@ dependencies = [ name = "toyos-blockserve" version = "0.1.0" dependencies = [ + "bcachefs", "toyos", "toyos-abi", "toyos-blockhold", @@ -3563,7 +3576,6 @@ dependencies = [ "fatfs", "flate2", "getrandom 0.3.4", - "gpt", "iced-x86", "kernel", "libc", diff --git a/Cargo.toml b/Cargo.toml index 729e066502f..7026709d242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ members = [ "system/compositor/desktop", "system/compositor/virtio-gpu", "system/console", + "system/disks", "system/diskserver", "system/filepicker", "system/fileserver", @@ -301,8 +302,6 @@ pem = "4" [dev-dependencies] # The FAT implementation `build/fatformat.rs` is judged against, byte for byte. fatfs = "0.3.6" -# The GPT writer `build/gptwrite.rs` is judged against, byte for byte. -gpt = "3.1.0" # `USER_TOP`, so the harness judges a held `rsp` against the bound the kernel # checks every user pointer against and not against a second copy of it. toyos-userbound = { path = "lib/userbound" } diff --git a/build/build.rs b/build/build.rs index 2a8d7a7ed1f..5b25b5052cd 100644 --- a/build/build.rs +++ b/build/build.rs @@ -193,6 +193,8 @@ struct ProgramConfig { syscap: Vec, /// The idle slot (`toyos_manifest::Program::slots`). slots: bool, + /// Disk authorizations minted at its start (`toyos_manifest::authorize`). + authorize: Vec, /// A system service: the supervisor starts it with `HOME` at its own `/state/` /// and makes that directory, where every other row gets the session's. service: bool, @@ -717,6 +719,22 @@ fn held_by_their_holders_alone(config: &SystemConfig) -> Result<(), String> { if program.slots && config.boot.start.contains(name) { return Err(format!("`{name}` asks for `slots` and is in `[boot] start`, and the slots are granted to a launch alone")); } + // A row any login could start would hand every login what it holds, + // and so would a login authority, whose namespace reaches every + // program a login runs directly: an authorization that changes a + // disk is the image's to grant, to a row that is neither. + for word in &program.authorize { + let Some(authorization) = toyos_manifest::authorize::Authorization::parse(word) else { + return Err(format!("`{name}` asks to be authorized for `{word}`, which is no disk authorization")); + }; + let starters: Vec<&String> = config.programs.iter().filter(|(_, p)| p.starts.contains(name)).map(|(n, _)| n).collect(); + if authorization.changes() && !starters.is_empty() { + return Err(format!("`{name}` is authorized for `{word}`, which changes a disk, and {starters:?} may start it")); + } + if authorization.changes() && program.login { + return Err(format!("`{name}` is authorized for `{word}`, which changes a disk, and is a login authority")); + } + } } for (port, holders) in HELD_ALONE { if config.apps.receives.iter().any(|r| r == port) { @@ -844,6 +862,7 @@ fn render_manifest(config: &SystemConfig) -> Vec { drives: cfg.drives.clone(), syscap: cfg.syscap.clone(), slots: cfg.slots, + authorize: cfg.authorize.clone(), service: cfg.service, state: cfg.state, roles: cfg.roles.clone(), @@ -2967,6 +2986,36 @@ mod tests { ); } + /// No image this repository ships authorizes a change to a disk: that is + /// a test image's, for the one boot that makes it. + #[test] + fn no_shipped_boot_config_authorizes_a_disk_change() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + for boot in [Boot::shipped(root), Boot::diag(root), Boot::console(root)] { + let config = parse_config(&boot.config); + for (name, program) in &config.programs { + let changes = program.authorize.iter().filter_map(|w| toyos_manifest::authorize::Authorization::parse(w)).any(|a| a.changes()); + assert!(!changes, "{} authorizes `{name}` to change a disk: {:?}", boot.config.display(), program.authorize); + } + } + } + + /// An authorization that changes a disk goes to a row only the boot + /// starts, never one a login can launch; reading the tables may go to + /// either; and a word that names nothing is refused. + #[test] + fn a_disk_changing_authorization_goes_to_no_row_a_login_can_start() { + let gate = |text: &str| held_by_their_holders_alone(&toml::from_str(text).unwrap()); + assert!(gate("[programs.runner]\nauthorize = [\"designate:data\"]\n").is_ok()); + assert!(gate("[programs.disks]\nauthorize = [\"tables\"]\n[programs.shell]\nstarts = [\"disks\"]\n").is_ok()); + let launched = gate("[programs.disks]\nauthorize = [\"designate:data\"]\n[programs.shell]\nstarts = [\"disks\"]\n"); + assert!(launched.unwrap_err().contains("may start it")); + let login = gate("[boot]\nstart = [\"sshserver\"]\n[programs.sshserver]\nlogin = true\nauthorize = [\"designate:data\"]\n"); + assert!(login.unwrap_err().contains("is a login authority")); + assert!(gate("[programs.sshserver]\nlogin = true\nauthorize = [\"tables\"]\n").is_ok(), "reading the tables is no change"); + assert!(gate("[programs.disks]\nauthorize = [\"format:everything\"]\n").unwrap_err().contains("no disk authorization")); + } + /// No image this repository ships starts sshserver. /// /// It listens on every interface and authenticates against a file that is @@ -3200,6 +3249,7 @@ mod tests { "tests/images/acpicase.toml", "tests/images/benchcase.toml", "tests/images/clipboardcase.toml", + "tests/images/designatecase.toml", "tests/images/drivescase.toml", "tests/images/hostedclangcase.toml", "tests/images/jobcase.toml", diff --git a/build/gptwrite.rs b/build/gptwrite.rs deleted file mode 100644 index a01c19f6b88..00000000000 --- a/build/gptwrite.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! The partition table the build writes onto every disk it makes: a -//! protective MBR, the primary GPT header and its entry array at the front, -//! and the backup array and header at the back, in 512-byte logical blocks -//! with the 128 entries of 128 bytes UEFI §5.3 requires room for. -//! -//! Here and not in `toyos-gpt`, which is the parser the kernel and the loader -//! read the table with; the build is the one program that writes one. -//! `toyos-gpt`'s CRC-32 and GUIDs are shared, because they are the format's -//! own and the parser is what proves them. `the_gpt_crate_writes_the_same_table` -//! holds every byte against the `gpt` crate this replaced. - -use toyos_gpt::Guid; - -const LBA: u64 = crate::image::LBA as u64; -const ENTRIES: u64 = 128; -const ENTRY_BYTES: u64 = 128; -const ARRAY_LBAS: u64 = ENTRIES * ENTRY_BYTES / LBA; -/// The MBR, the header and the array before it. -const FIRST_USABLE: u64 = 2 + ARRAY_LBAS; -const HEADER_BYTES: usize = 92; -/// A name is at most 36 UTF-16 code units. -const NAME_UNITS: usize = 36; - -/// One partition: its type, its own GUID, its first and last block, and its name. -pub struct Entry<'a> { - pub kind: Guid, - pub unique: Guid, - pub first_lba: u64, - pub last_lba: u64, - pub name: &'a str, -} - -/// A table's two halves: `primary` goes at byte 0 of the disk and `backup` -/// ends at its last byte. -pub struct Table { - pub primary: Vec, - pub backup: Vec, -} - -/// The last block a partition on a disk of `lbas` blocks may use. -fn last_usable(lbas: u64) -> u64 { - lbas - 2 - ARRAY_LBAS -} - -/// The table of a disk of `lbas` blocks named `disk`, carrying `entries` in -/// their order. -pub fn table(lbas: u64, disk: Guid, entries: &[Entry<'_>]) -> Table { - assert!(entries.len() as u64 <= ENTRIES, "{} partitions in a table of {ENTRIES}", entries.len()); - let last_usable = last_usable(lbas); - let mut array = vec![0u8; (ENTRIES * ENTRY_BYTES) as usize]; - for (entry, slot) in entries.iter().zip(array.chunks_mut(ENTRY_BYTES as usize)) { - assert!( - FIRST_USABLE <= entry.first_lba && entry.first_lba <= entry.last_lba && entry.last_lba <= last_usable, - "partition {:?} at blocks {}..={} is not inside {FIRST_USABLE}..={last_usable}", - entry.name, - entry.first_lba, - entry.last_lba - ); - let name: Vec = entry.name.encode_utf16().collect(); - assert!(name.len() <= NAME_UNITS, "the partition name {:?} is over {NAME_UNITS} UTF-16 units", entry.name); - slot[..16].copy_from_slice(&entry.kind.0); - slot[16..32].copy_from_slice(&entry.unique.0); - slot[32..40].copy_from_slice(&entry.first_lba.to_le_bytes()); - slot[40..48].copy_from_slice(&entry.last_lba.to_le_bytes()); - for (unit, at) in name.iter().zip((56..).step_by(2)) { - slot[at..at + 2].copy_from_slice(&unit.to_le_bytes()); - } - } - let array_crc = toyos_gpt::crc32(&array); - let header = |at: u64, other: u64, array_at: u64| { - let mut block = vec![0u8; LBA as usize]; - block[..8].copy_from_slice(b"EFI PART"); - block[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes()); - block[12..16].copy_from_slice(&(HEADER_BYTES as u32).to_le_bytes()); - block[24..32].copy_from_slice(&at.to_le_bytes()); - block[32..40].copy_from_slice(&other.to_le_bytes()); - block[40..48].copy_from_slice(&FIRST_USABLE.to_le_bytes()); - block[48..56].copy_from_slice(&last_usable.to_le_bytes()); - block[56..72].copy_from_slice(&disk.0); - block[72..80].copy_from_slice(&array_at.to_le_bytes()); - block[80..84].copy_from_slice(&(ENTRIES as u32).to_le_bytes()); - block[84..88].copy_from_slice(&(ENTRY_BYTES as u32).to_le_bytes()); - block[88..92].copy_from_slice(&array_crc.to_le_bytes()); - let crc = toyos_gpt::crc32(&block[..HEADER_BYTES]); - block[16..20].copy_from_slice(&crc.to_le_bytes()); - block - }; - - let mut primary = protective_mbr(lbas); - primary.extend(header(1, lbas - 1, 2)); - primary.extend(&array); - let mut backup = array; - backup.extend(header(lbas - 1, 1, last_usable + 1)); - Table { primary, backup } -} - -/// LBA 0: one partition of type 0xEE over the whole disk but its first -/// block, or as much of it as 32 bits count, at the CHS UEFI §5.2.3 gives it. -fn protective_mbr(lbas: u64) -> Vec { - let mut block = vec![0u8; LBA as usize]; - block[446..454].copy_from_slice(&[0x00, 0x00, 0x02, 0x00, 0xEE, 0xFF, 0xFF, 0xFF]); - block[454..458].copy_from_slice(&1u32.to_le_bytes()); - block[458..462].copy_from_slice(&u32::try_from(lbas - 1).unwrap_or(u32::MAX).to_le_bytes()); - block[510..512].copy_from_slice(&[0x55, 0xAA]); - block -} - -/// A version 4 GUID from the operating system's random source, in the -/// on-disk order: the version is the high nibble of byte 7, the third field -/// being little-endian, and the variant the top two bits of byte 8. -pub fn random_guid() -> Guid { - let mut bytes = [0u8; 16]; - getrandom::fill(&mut bytes).expect("the operating system's random source"); - bytes[7] = (bytes[7] & 0x0F) | 0x40; - bytes[8] = (bytes[8] & 0x3F) | 0x80; - Guid(bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::image::ImageSectors; - use std::collections::BTreeMap; - use std::io::{Cursor, Read, Seek, SeekFrom}; - - const ALIGN: u64 = 2048; - - /// The partitions of a boot image with the DATA an install puts first, - /// one of them a length the 1 MiB alignment does not round. - const ROWS: [(&str, Guid, u64); 6] = [ - ("ToyOS data", Guid::TOYOS_DATA, 3 << 20), - ("ESP", Guid::EFI_SYSTEM, 34 << 20), - ("ToyOS slots", Guid::TOYOS_SLOTS, 1 << 20), - ("ToyOS log", Guid::MICROSOFT_BASIC, 34 << 20), - ("ToyOS slot A", Guid::TOYOS_BOOT, (40 << 20) + 4096), - ("ToyOS root A", Guid::TOYOS_ROOT, 7 << 20), - ]; - - /// `ROWS` under `uniques`, each on the first 1 MiB boundary after the last. - fn entries(uniques: &[Guid]) -> Vec> { - let mut at = ALIGN; - ROWS.iter() - .zip(uniques) - .map(|((name, kind, len), unique)| { - let first = at; - at = (first + len / LBA).next_multiple_of(ALIGN); - Entry { kind: *kind, unique: *unique, first_lba: first, last_lba: first + len / LBA - 1, name } - }) - .collect() - } - - fn disk_of(total: u64, table: &Table) -> Vec { - let mut disk = vec![0u8; total as usize]; - disk[..table.primary.len()].copy_from_slice(&table.primary); - let back = disk.len() - table.backup.len(); - disk[back..].copy_from_slice(&table.backup); - disk - } - - /// The `gpt` crate's table for `ROWS`, laid as `build/image.rs` laid it with - /// that crate, on a disk of `total` bytes: its bytes, its disk GUID and - /// the unique GUIDs it drew. - fn theirs(total: u64) -> (Vec, Guid, Vec) { - let mut bytes = vec![0u8; total as usize]; - let mut cursor = Cursor::new(&mut bytes); - gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(total / LBA - 1).unwrap_or(u32::MAX)) - .overwrite_lba0(&mut cursor) - .expect("the protective MBR"); - let mut disk = gpt::GptConfig::default() - .initialized(false) - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .create_from_device(Box::new(cursor), None) - .expect("a table"); - disk.update_partitions(BTreeMap::new()).expect("an empty table"); - for (name, kind, len) in ROWS { - let kind = gpt::partition_types::Type { - guid: kind.to_string().leak(), - os: gpt::partition_types::OperatingSystem::None, - }; - disk.add_partition(name, len, kind, 0, Some(ALIGN)).expect("a partition"); - } - let mut device = disk.write().expect("the table"); - let mut out = vec![0u8; total as usize]; - device.seek(SeekFrom::Start(0)).and_then(|_| device.read_exact(&mut out)).expect("read back"); - let guid = |at: usize| Guid(out[at..at + 16].try_into().unwrap()); - let disk_guid = guid(LBA as usize + 56); - let uniques = (0..ROWS.len()).map(|i| guid(2 * LBA as usize + i * ENTRY_BYTES as usize + 16)).collect(); - (out, disk_guid, uniques) - } - - /// **The differential oracle**: the `gpt` crate, a third party's GPT - /// writer sharing no code with this one, lays the same partitions on the - /// same disk under the same GUIDs into the same bytes, every one of them, - /// on a disk the alignment rounds and on one it does not. - #[test] - fn the_gpt_crate_writes_the_same_table() { - for total in [128u64 << 20, (160 << 20) + 3 * 4096] { - let (theirs, disk, uniques) = theirs(total); - let ours = disk_of(total, &table(total / LBA, disk, &entries(&uniques))); - let differing = (0..total as usize).filter(|&i| ours[i] != theirs[i]).count(); - assert_eq!(differing, 0, "{total}: {differing} bytes differ from the gpt crate's"); - } - } - - /// RFC 9562's version and variant, where the canonical text shows them. - #[test] - fn a_random_guid_is_version_4_of_the_rfc_variant() { - let text = random_guid().to_string(); - assert_eq!(&text[14..15], "4", "{text}"); - assert!(matches!(&text[19..20], "8" | "9" | "A" | "B"), "{text}"); - } - - /// `toyos-gpt`, the parser the loader and the kernel read with, finds - /// every partition at its blocks under its type, from the primary and, - /// with the primary torn, from the backup. - #[test] - fn toyos_gpt_reads_it_from_either_copy() { - let total: u64 = 128 << 20; - let uniques: Vec = ROWS.iter().map(|_| random_guid()).collect(); - let entries = entries(&uniques); - let disk = disk_of(total, &table(total / LBA, random_guid(), &entries)); - let mut torn = disk.clone(); - torn[LBA as usize + 30] ^= 1; - for image in [&disk, &torn] { - for entry in &entries { - let found = toyos_gpt::locate(&mut ImageSectors(image), entry.unique).expect("the partition").partition(); - assert_eq!( - (found.type_guid(), found.first_lba(), found.lba_count().get()), - (entry.kind, entry.first_lba, entry.last_lba - entry.first_lba + 1), - "{}", - entry.name - ); - } - } - } -} diff --git a/build/image.rs b/build/image.rs index 409fc64efed..19e6a2fdac3 100644 --- a/build/image.rs +++ b/build/image.rs @@ -1,6 +1,5 @@ use std::collections::BTreeMap; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; -use std::num::NonZeroU64; use std::path::Path; use bcachefs::engine::{self, Meta, NodeRef, Tree}; @@ -9,6 +8,7 @@ use crate::arch::Arch; use toyos_blockdev::{block_on_ready, Bytes, ReadOnlyBytes, BLOCK}; use toyos_crypto::Random; use toyos_fat32::{Fat32, FatTime}; +use toyos_gpt::edit::{Geometry, Slot, Step, Table}; use toyos_gpt::Guid; /// The image that goes on the ROOT partition: a bcachefs volume holding @@ -246,9 +246,9 @@ pub fn create_boot_image( // kernel. The kernel is given the partition by name; nothing anywhere goes // looking for one by type or by format. Every slot partition is named the // same way, by the slot table. - let (log_guid, esp_guid, table_guid) = (crate::gptwrite::random_guid(), crate::gptwrite::random_guid(), crate::gptwrite::random_guid()); - 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 (log_guid, esp_guid, table_guid) = (random_guid(), random_guid(), random_guid()); + let a = (random_guid(), random_guid()); + let b = second.map(|room| (random_guid(), random_guid(), room)); let s = signed(release, params, signing); // Room for the slot's files twice over, so an update can write new ones @@ -659,8 +659,8 @@ pub fn designate_data_disk(path: &Path, len: u64) -> (u64, u64) { .write(true) .open(path) .unwrap_or_else(|e| panic!("open {} to partition it: {e}", path.display())); - let starts = lay(&mut file, len, &[data_row(crate::gptwrite::random_guid(), data_bytes)]); - write_at(&mut file, starts[0], &designation(data_bytes / SECTOR as u64), "DATA partition's designation"); + let starts = lay(&mut file, len, &[data_row(random_guid(), data_bytes)]); + write_at(&mut file, starts[0], &bcachefs::designation(data_bytes / SECTOR as u64), "DATA partition's designation"); (starts[0], data_bytes) } @@ -669,6 +669,28 @@ fn data_row(guid: Guid, len: u64) -> Row { Row { what: "DATA partition".into(), name: "ToyOS data".into(), kind: Guid::TOYOS_DATA, guid, len } } +/// A version 4 GUID from the host's random source. +pub fn random_guid() -> Guid { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes).expect("the operating system's random source"); + Guid::random_v4(bytes) +} + +/// A protective MBR and both copies of a GPT carrying `slots` in their +/// order, under `disk_guid`, on `disk` of `total` bytes. +pub fn write_gpt(disk: &mut (impl Write + Seek), total: u64, disk_guid: Guid, slots: &[Slot]) { + let geometry = Geometry::new(LBA, total / u64::from(LBA)).expect("a disk that holds a table"); + let mut table = Table::empty(&geometry, disk_guid); + assert!(slots.len() <= table.slots.len(), "{} partitions in a table of {}", slots.len(), table.slots.len()); + table.slots[..slots.len()].copy_from_slice(slots); + let encoded = table.encode(&geometry).unwrap_or_else(|why| panic!("the partition table is not one UEFI admits: {why:?}")); + for step in encoded.steps(true) { + if let Step::Write { lba, bytes } = step { + write_at(disk, lba * u64::from(LBA), bytes, "partition table"); + } + } +} + /// Write `bytes` at byte `at` of `disk`; `what` they are is what a refusal names. fn write_at(disk: &mut (impl Write + Seek), at: u64, bytes: &[u8], what: &str) { disk.seek(SeekFrom::Start(at)) @@ -708,7 +730,7 @@ pub fn install(image: &[u8], disk: &Path, data_bytes: u64) { .collect(); let (mut rows, volumes): (Vec, Vec<&[u8]>) = carried.into_iter().unzip(); - rows.insert(0, data_row(crate::gptwrite::random_guid(), data_bytes)); + rows.insert(0, data_row(random_guid(), data_bytes)); let total = disk_bytes(rows.iter().map(|row| row.len)); let mut file = std::fs::OpenOptions::new() @@ -720,7 +742,7 @@ pub fn install(image: &[u8], disk: &Path, data_bytes: u64) { file.set_len(total).unwrap_or_else(|e| panic!("size {} to {total} bytes: {e}", disk.display())); let starts = lay(&mut file, total, &rows); - write_at(&mut file, starts[0], &designation(data_bytes / SECTOR as u64), "DATA partition's designation"); + write_at(&mut file, starts[0], &bcachefs::designation(data_bytes / SECTOR as u64), "DATA partition's designation"); for ((row, volume), start) in rows[1..].iter().zip(volumes).zip(&starts[1..]) { for (i, block) in volume.chunks(PARTITION_ALIGN).enumerate().filter(|(_, block)| block.iter().any(|byte| *byte != 0)) { write_at(&mut file, start + (i * PARTITION_ALIGN) as u64, block, &row.what); @@ -733,15 +755,6 @@ fn name_of(part: &toyos_gpt::Partition) -> String { String::from_utf16(part.name()).unwrap_or_else(|e| panic!("partition {}'s name is no UTF-16: {e}", part.unique_guid())) } -/// Block 0 of a volume: the magic and its block count. -fn designation(blocks: u64) -> [u8; SECTOR] { - let mut block = [0u8; SECTOR]; - block[..bcachefs::DESIGNATION_MAGIC.len()].copy_from_slice(&bcachefs::DESIGNATION_MAGIC); - let at = bcachefs::DESIGNATION_BLOCKS_OFFSET; - block[at..at + 8].copy_from_slice(&blocks.to_le_bytes()); - block -} - /// A disk file as logical blocks, for a reader that may not hold the image. pub struct FileSectors<'a>(pub &'a mut std::fs::File); @@ -754,10 +767,6 @@ impl toyos_gpt::Sectors for FileSectors<'_> { self.0.metadata().map(|m| m.len()).unwrap_or(0) / u64::from(LBA) } - fn lba_count_granularity(&self) -> NonZeroU64 { - NonZeroU64::new(1).expect("one is not zero") - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { self.0.seek(SeekFrom::Start(lba * u64::from(LBA))).is_ok() && self.0.read_exact(buf).is_ok() } @@ -821,20 +830,15 @@ fn lay(disk: &mut (impl Write + Seek), total: u64, rows: &[Row]) -> Vec { }) .collect(); let lba = u64::from(LBA); - let entries: Vec> = rows + let slots: Vec = rows .iter() .zip(&starts) - .map(|(row, start)| crate::gptwrite::Entry { - kind: row.kind, - unique: row.guid, - first_lba: start / lba, - last_lba: (start + row.len) / lba - 1, - name: &row.name, + .map(|(row, start)| { + Slot::new(row.kind, row.guid, start / lba, (start + row.len) / lba - 1, &row.name) + .unwrap_or_else(|| panic!("the partition name {:?} is over {} UTF-16 units", row.name, toyos_gpt::edit::NAME_UNITS)) }) .collect(); - let table = crate::gptwrite::table(total / lba, crate::gptwrite::random_guid(), &entries); - write_at(disk, 0, &table.primary, "partition table"); - write_at(disk, total - table.backup.len() as u64, &table.backup, "backup partition table"); + write_gpt(disk, total, random_guid(), &slots); starts } @@ -872,10 +876,6 @@ impl toyos_gpt::Sectors for ImageSectors<'_> { self.0.len() as u64 / u64::from(LBA) } - fn lba_count_granularity(&self) -> NonZeroU64 { - NonZeroU64::new(1).expect("one is not zero") - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { let at = lba as usize * LBA as usize; let Some(block) = self.0.get(at..at + buf.len()) else { return false }; @@ -1026,7 +1026,7 @@ mod tests { 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())), + ("ESP", create_esp_volume(Arch::X86_64, b"bootloader", random_guid())), ("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()), @@ -1114,7 +1114,7 @@ mod tests { #[test] fn the_esp_and_a_slot_carry_what_the_bootloader_looks_for() { assert_eq!( - files_of(create_esp_volume(Arch::X86_64, b"bootloader", crate::gptwrite::random_guid())), + files_of(create_esp_volume(Arch::X86_64, b"bootloader", random_guid())), ["EFI/BOOT/BOOTx64.EFI", "toyos/log.guid"] ); let key = key(); @@ -1235,7 +1235,7 @@ mod tests { ); let mut stamp = [0u8; SECTOR]; disk.seek(SeekFrom::Start(PARTITION_ALIGN as u64)).and_then(|_| disk.read_exact(&mut stamp)).expect("read DATA"); - assert_eq!(stamp, designation(DATA / SECTOR as u64)); + assert_eq!(stamp, bcachefs::designation(DATA / SECTOR as u64)); drop(disk); let theirs = dir.join("theirs.img"); diff --git a/build/lib.rs b/build/lib.rs index e3dc4360e14..35b292200a1 100644 --- a/build/lib.rs +++ b/build/lib.rs @@ -19,7 +19,6 @@ pub mod flash; pub mod fpgate; #[cfg(test)] pub mod gitfixture; -pub mod gptwrite; pub mod hostedclang; pub mod flags; pub mod hostws; diff --git a/images/system.toml b/images/system.toml index 1135b3ad838..30b0a4fb44a 100644 --- a/images/system.toml +++ b/images/system.toml @@ -168,6 +168,13 @@ login = true [programs.logout] receives = ["session-admin"] +# The machine's disks and their tables, read: `launch disks`. Nothing here +# lets it change a disk; an authorization that does (`designate:data`) is the +# build's to refuse any row a login can start (`build/build.rs`), until the +# administrative authorization every system change will share exists. +[programs.disks] +authorize = ["tables"] + # `swap` is the authority to replace a running service's binary; the build # refuses it to any other program (`build/build.rs`). `ssh swap netstack # < netstack` runs it for a login. @@ -225,7 +232,7 @@ starts = ["shell"] [programs.shell] receives = ["surface"] -starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "logout", "/apps"] +starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "paint", "files", "host", "inspect", "pkg", "sshserver", "swap", "toyfetch", "update", "grants", "logout", "disks", "/apps"] [programs.doom] receives = ["compositor", "soundserver"] diff --git a/issues/a-first-format-interrupted-mid-flush-can-leave-a-superblock-over-a-bitmap-it-never-wrote.md b/issues/a-first-format-interrupted-mid-flush-can-leave-a-superblock-over-a-bitmap-it-never-wrote.md new file mode 100644 index 00000000000..c4c28e4e1c9 --- /dev/null +++ b/issues/a-first-format-interrupted-mid-flush-can-leave-a-superblock-over-a-bitmap-it-never-wrote.md @@ -0,0 +1,29 @@ +--- +status: open +kind: defect +opened: 2026-10-28 +--- + +# A first format interrupted mid-flush can leave a superblock over a bitmap it never wrote + +`bcachefs::Formatted::format` writes the allocation bitmap, the root node and +then the superblock, but through the DATA file server's cache +(`system/fileserver/src/cache.rs`), which writes every dirty block at a +flush lowest first and in runs that may be in flight together, unordered. +Block 0 is the lowest, so the superblock can reach the medium before the +bitmap does. A machine that stops between the two has, in place of the +designation stamp, a superblock whose bitmap blocks hold whatever the +partition held before — and `BitmapAllocator::open` reads them as the +allocation state. The designation a bench runs from inside ToyOS +(`bench_data_designated`) formats 170 GiB on its next boot, about 1,361 +bitmap blocks, so the window is that one flush. + +Found by inspection in the review of the partitions work; no boot has +shown it. + +**Owner:** the DATA file server and the bcachefs crate. + +**Exit condition:** a format reaches the medium superblock-last — every +other block written and made durable before block 0 is written — and a host +test cuts the format's writes at every point against a stamped partition and +finds either the stamp or a whole volume at block 0. diff --git a/issues/a-foreign-data-partition-is-answered-with-memory.md b/issues/a-foreign-data-partition-is-answered-with-memory.md deleted file mode 100644 index c176d6bf72b..00000000000 --- a/issues/a-foreign-data-partition-is-answered-with-memory.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# A foreign DATA partition is answered with memory - -`system/fileserver/src/main.rs`'s `data_on` answers `Probed::Foreign` — a -TOYOS-DATA partition that holds no volume of ours and no designation stamp — -with `ram()`: `/apps`, `/config`, `/home` and `/state` are served from memory, -and everything written under them is lost at the next reboot. That is a disk -that is there and cannot be used, answered as though the machine had none, -which is the harm `fileserver::absent::Absent` exists to refuse; a refused NVMe -claim and two DATA partitions are refused by name for the same reason. - -`foreign_disk_untouched`, the guest test that booted this arm, asserted the -partition is not written and the `no volume of ours` line, and nothing about -what stands in for it; it was cut in `520c0d129`. - -**Exit condition:** a foreign DATA partition serves DATA absent by name, and -a test of `data_on`'s `Probed::Foreign` arm reads the absent line and that the -in-memory line is never said. diff --git a/issues/a-stops-file-sync-and-its-failure-are-never-recorded.md b/issues/a-stops-file-sync-and-its-failure-are-never-recorded.md new file mode 100644 index 00000000000..3a910b4980c --- /dev/null +++ b/issues/a-stops-file-sync-and-its-failure-are-never-recorded.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-10-28 +--- + +# A stop's file sync, and its failure, are never recorded + +`Supervisor::halt` (`system/supervisor/src/stopping.rs`) has `logkeeper` make +the log whole, then syncs every writable file server (`sync_files`), then +stops the drivers and asks the kernel to stop. Whatever `sync_files` and the +drivers' stop say — a server that would not sync, one that did not answer in +`toyos_quiesce::SYNC_MS`, a driver's shutdown not complete in its bound — is +said after the log's last write, so it reaches the kernel's ring and no +file: a boot's readback can never tell a stop that made DATA durable from +one that lost it. The partitions work's first designation on the AMD bench +(PR #1085) formatted 600 GiB at boot with 4,803 blocks dirty when the +reboot began, and whether they reached the medium was not observable in its +readback; it took the next boot's mount to say. + +**Owner:** the supervisor's stop and `logkeeper`. + +**Exit condition:** a stop's sync outcome for each file server, and each +disk driver's shutdown outcome, are on the log partition when the machine +stops — the log made whole after them, its own server synced last — and a +guest test stops a machine whose DATA server will not sync and reads the +refusal off the log the next boot sees. diff --git a/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md b/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md index a73b62462b9..cd75748385a 100644 --- a/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md +++ b/issues/an-images-guids-are-drawn-from-the-hosts-random-source.md @@ -8,7 +8,7 @@ opened: 2026-10-12 Two builds of one plan write different images, so ToyOS rebuilding itself cannot reproduce the host's bytes. Every GUID an image carries comes from -`gptwrite::random_guid`, the operating system's random source, drawn again on +`image::random_guid`, the operating system's random source, drawn again on every `create_boot_image` call: it draws the log partition's, the ESP's, the slot table's and each slot's boot and ROOT GUIDs, and writes the log's again into the ESP's `/toyos/log.guid` and each slot's into the slot table; `lay` diff --git a/issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md b/issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md new file mode 100644 index 00000000000..af9c8e256ee --- /dev/null +++ b/issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-10-28 +--- + +# The scratch disk `--adopt-scratch` makes is not served + +`toyos-metal --adopt-scratch` (`tests/metal/src/lib.rs`, `Job::Scratch`) +writes a table whose one partition runs from LBA 2048 to the last usable +sector (`sgdisk --new=1:0:0`), so on a stick whose sector count is not whole +4 KiB blocks the partition is not whole blocks either, and usbd offers it +and refuses every session on it by name: a bench boot's log read +`partition … of type 51CC09F9-7442-435B-BC03-0FD8718B0FB5 is not served: LBA +2048+60060419 is not whole 4 KiB blocks of 512`. A destructive test given +that stick has nothing to write. + +**Exit condition:** a scratch disk is initialized from inside ToyOS with one +partition on whole 4 KiB blocks (`issues/toyos-manages-its-own-partitions.md`, +stage 3), a bench boot serves a session on it, and `--adopt-scratch` is +deleted. diff --git a/issues/toyos-manages-its-own-partitions.md b/issues/toyos-manages-its-own-partitions.md new file mode 100644 index 00000000000..66679d7de49 --- /dev/null +++ b/issues/toyos-manages-its-own-partitions.md @@ -0,0 +1,47 @@ +--- +status: open +kind: track +opened: 2026-10-28 +--- + +# ToyOS manages its own partitions + +The disk driver that drives a disk owns its table and makes every change to +it (`lib/blockserve`); diskserver routes a request to it under the badge its +connector was minted with (`toyos_blockring::wire::Badge`); `toyos_gpt::edit` +is the one GPT writer, laid in UEFI 2.10 §5.3.2's order; `/system/bin/disks` +is the command. An authorization is one action on one target, and no badge +that changes a disk opens a session or writes a byte a client chose. + +Built: `disks list` (`authorize = ["tables"]`) and the designation of the +machine's one DATA partition (`authorize = ["designate:data"]`), which the +build grants only a row no login can start (`build/build.rs`), so today only +a signed test image's job holds it. + +## Stages, in order + +1. **Interactive authorization.** `disks` changes nothing for a person at a + shell or over ssh: no row a login can start may hold an authorization that + changes a disk, and none is built to fill the gap. Its exit is the + administrative authorization every system change shares (the planning + architect, 2026-10-28) — one userland path, not a disk-specific trust + system — through which `disks` asks for one authorization of one action + on one target, and the supervisor mints it. Its product choice (how the + owner approves a system change) is the owner's. +2. **Table edits.** `Edit` (one disk's table replaced by an exact intended + table: create, delete, retype, rename, and restoring a copy, which §5.3.2 + requires be reported) and `Init` (a fresh table on one disk). Each + authorization names the plan it permits and is bound to the disk's state + when it was authorized — a token the driver draws at bring-up and after + every change it makes, compared in the turn it writes. The driver refuses + a change to a held partition or one the running system is on + (`--active`), writes sector-granular at the disk's true last LBA, flushes + between the two copies, and reads both back. Unrelated partitions are + untouched by construction; another system's are reclaimed only when the + plan names them. +3. **Scratch disks.** `Init` with one aligned partition of + `Guid::TOYOS_SCRATCH` replaces `toyos-metal --adopt-scratch` + (`issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md`). +4. **The installer** (`issues/storage-is-layers-and-a-role-is-a-filesystem.md`, + stage 3) lays its layout through stage 2's library and authorization, and + `toyos-metal --install-internal`'s `sgdisk` and `partx` go with it. diff --git a/lib/bcachefs/src/lib.rs b/lib/bcachefs/src/lib.rs index 2d01bf93eb1..ec4e1141e63 100644 --- a/lib/bcachefs/src/lib.rs +++ b/lib/bcachefs/src/lib.rs @@ -27,7 +27,7 @@ pub use block_io::{BlockIO, BlockBuf, BlockNum, DeviceError, TransferError}; #[cfg(feature = "std")] pub use block_io::VecBlockIO; pub use fs::{file_entry_fits, Formatted, Mounted, ReadOnly, ReadWrite, Recorded, FsError, Extent}; -pub use superblock::{DESIGNATION_BLOCKS_OFFSET, DESIGNATION_MAGIC, FsUuid, Superblock}; +pub use superblock::{carries_volume, designated, designation, FsUuid, Superblock}; /// Records the largest single allocation each test thread makes, so a test can /// assert what parsing a crafted block asks the allocator for. diff --git a/lib/bcachefs/src/superblock.rs b/lib/bcachefs/src/superblock.rs index ace3abef5b2..8b3209943a1 100644 --- a/lib/bcachefs/src/superblock.rs +++ b/lib/bcachefs/src/superblock.rs @@ -7,8 +7,8 @@ use crate::fs::FsError; pub const MAGIC: [u8; 4] = *b"BCFS"; pub const VERSION: u32 = 2; -/// The designation stamp: the *only* thing that authorises ToyOS to destroy -/// what is on a block device. +/// The designation stamp: the only thing that authorises a file server to +/// format a partition that holds no volume of this format. /// /// Not a bcachefs concept, and it lives here anyway, because it occupies the /// same block as the superblock and the one invariant that matters is that @@ -17,16 +17,35 @@ pub const VERSION: u32 = 2; /// /// Bytes 0..16 are this magic and bytes 16..24 the little-endian block count /// of the device the stamp designates, so a stamped image copied onto a -/// different disk designates nothing. The rest of the block is ignored. -/// -/// Reading it is safe on any disk; writing it is the act of designation, and -/// it destroys whatever partition table was there. That is the point: there -/// is no way to designate a disk by accident, and no way to do it without -/// having already decided to lose its contents. -pub const DESIGNATION_MAGIC: [u8; 16] = *b"TOYOS-FORMAT-ME\0"; +/// different disk designates nothing. The rest of the block is zero when +/// written and ignored when read. +const DESIGNATION_MAGIC: [u8; 16] = *b"TOYOS-FORMAT-ME\0"; + +const DESIGNATION_BLOCKS_OFFSET: usize = 16; + +/// Block 0 of a device of `blocks` blocks that designates it, whole. +pub fn designation(blocks: u64) -> [u8; BLOCK_SIZE] { + let mut block = [0u8; BLOCK_SIZE]; + block[..DESIGNATION_MAGIC.len()].copy_from_slice(&DESIGNATION_MAGIC); + block[DESIGNATION_BLOCKS_OFFSET..DESIGNATION_BLOCKS_OFFSET + 8].copy_from_slice(&blocks.to_le_bytes()); + block +} + +/// Whether `block0` begins as a volume of this format's does, whatever state +/// the rest of the volume is in. +pub fn carries_volume(block0: &[u8]) -> bool { + block0.get(..MAGIC.len()) == Some(&MAGIC) +} -/// Bytes 16..24 of a designation stamp. -pub const DESIGNATION_BLOCKS_OFFSET: usize = 16; +/// The block count a stamp at the front of `block0` names, or `None` where +/// `block0` carries none. +pub fn designated(block0: &[u8]) -> Option { + if block0.get(..DESIGNATION_MAGIC.len())? != DESIGNATION_MAGIC { + return None; + } + let count = block0.get(DESIGNATION_BLOCKS_OFFSET..DESIGNATION_BLOCKS_OFFSET + 8)?; + Some(u64::from_le_bytes(count.try_into().ok()?)) +} const _: () = assert!( DESIGNATION_MAGIC[0] != MAGIC[0] @@ -298,3 +317,23 @@ fn write_u32(buf: &mut [u8], off: usize, val: u32) { fn write_u64(buf: &mut [u8], off: usize, val: u64) { buf[off..off + 8].copy_from_slice(&val.to_le_bytes()); } + +#[cfg(test)] +mod designation_tests { + use super::*; + + /// A stamp names the count it was made for, and a block that does not + /// begin with the magic names nothing, a superblock's first among them. + #[test] + fn a_stamp_names_its_block_count_and_nothing_else_is_one() { + for blocks in [1, 43_000_000, u64::MAX] { + assert_eq!(designated(&designation(blocks)), Some(blocks)); + } + let mut superblock = [0u8; BLOCK_SIZE]; + superblock[..4].copy_from_slice(&MAGIC); + assert_eq!(designated(&superblock), None); + assert_eq!(designated(&[0u8; BLOCK_SIZE]), None); + assert_eq!(designated(&designation(7)[..20]), None, "a block too short for its count"); + assert!(designation(7)[24..].iter().all(|b| *b == 0), "the rest of the stamp's block is zero"); + } +} diff --git a/lib/blockhold/src/lib.rs b/lib/blockhold/src/lib.rs index 3fa9e13e315..5a39f81d04a 100644 --- a/lib/blockhold/src/lib.rs +++ b/lib/blockhold/src/lib.rs @@ -142,11 +142,7 @@ impl Holds { /// On an empty span: the caller refuses one before asking. pub fn hold(&mut self, first: u64, end: u64, holder: H) -> Result<(), H> { assert!(first < end, "an empty span is refused before it is held"); - if let Some(held) = self - .spans - .iter() - .find_map(|span| span.holder.filter(|_| span.overlaps(first, end))) - { + if let Some(held) = self.holder(first, end) { return Err(held); } let mut account = Account::default(); @@ -170,6 +166,11 @@ impl Holds { Ok(()) } + /// Who holds a block of `first..end`, if anyone does. + pub fn holder(&self, first: u64, end: u64) -> Option { + self.spans.iter().find_map(|span| span.holder.filter(|_| span.overlaps(first, end))) + } + /// The hold beginning at `first` ends; an account owing a report stays /// with its blocks. /// diff --git a/lib/blockring/src/disk.rs b/lib/blockring/src/disk.rs index e6b0e2695d4..1cc086826bc 100644 --- a/lib/blockring/src/disk.rs +++ b/lib/blockring/src/disk.rs @@ -51,6 +51,13 @@ pub const MSG_GONE: u32 = 4; /// Service to driver: serve this session, a [`Session`]. Handles, in order: /// the client's connection, and the region it sent with its open. pub const MSG_SESSION: u32 = 5; +/// Service to driver: describe this disk to the client, a [`disk`]-number +/// payload. Handles: the client's connection, which the driver answers on. +pub const MSG_DESCRIBE: u32 = 6; +/// Service to driver: carry out this designation for the client, a +/// [`Designation`]. Handles: the client's connection, which the driver +/// answers on. +pub const MSG_DESIGNATE: u32 = 7; pub const MAX_LABEL: usize = 64; /// The partitions one offer may name: a GPT's whole entry array at the @@ -184,6 +191,46 @@ impl Session { } } +/// A designation the service admitted, as [`MSG_DESIGNATE`] carries it: on +/// the driver's disk `disk`, the partition `unique`; `active` for a partition +/// the running system is on, which the driver refuses; `commit` to write +/// rather than judge. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Designation { + pub disk: u32, + pub unique: [u8; GUID_BYTES], + pub active: bool, + pub commit: bool, +} + +impl Designation { + pub const BYTES: usize = 4 + GUID_BYTES + 2; + + pub fn encode(&self) -> [u8; Self::BYTES] { + let mut out = [0u8; Self::BYTES]; + out[..4].copy_from_slice(&self.disk.to_le_bytes()); + out[4..20].copy_from_slice(&self.unique); + out[20] = u8::from(self.active); + out[21] = u8::from(self.commit); + out + } + + pub fn decode(bytes: &[u8]) -> Option { + let b: &[u8; Self::BYTES] = bytes.try_into().ok()?; + let flag = |v: u8| match v { + 0 => Some(false), + 1 => Some(true), + _ => None, + }; + Some(Self { + disk: u32::from_le_bytes(b[..4].try_into().ok()?), + unique: b[4..20].try_into().ok()?, + active: flag(b[20])?, + commit: flag(b[21])?, + }) + } +} + /// A [`MSG_DRIVER`] payload: the driver's name for the service's lines, which /// never says who the driver is. pub fn label(bytes: &[u8]) -> Option { diff --git a/lib/blockring/src/wire.rs b/lib/blockring/src/wire.rs index e907af7777f..85eca5ab47b 100644 --- a/lib/blockring/src/wire.rs +++ b/lib/blockring/src/wire.rs @@ -18,6 +18,10 @@ //! server's ends and its answer together: nothing else makes the ends, and an //! answer is otherwise only read off the wire ([`Opened::decode`]). +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + use toyos::transport::Word; use crate::layout::{self, ServerRings, RING_WORDS}; @@ -35,6 +39,23 @@ pub const MSG_REFUSED: u32 = 3; pub const MSG_LIST: u32 = 4; /// The answer to [`MSG_LIST`]: one [`Listed`] after another. pub const MSG_LISTED: u32 = 5; +/// What one disk's table holds, for a [`Badge::Tables`] connection: the +/// payload is the service's number for the disk, four bytes. Its driver +/// answers [`MSG_DISK`], a [`MSG_SLOT`] for each used entry and +/// [`MSG_DESCRIBED`]; or [`MSG_REFUSED`]. +pub const MSG_DESCRIBE: u32 = 6; +/// A [`DiskHead`]. +pub const MSG_DISK: u32 = 7; +/// A [`SlotLine`]. +pub const MSG_SLOT: u32 = 8; +/// The description is whole. No payload. +pub const MSG_DESCRIBED: u32 = 9; +/// Designate the machine's DATA partition, for a [`Badge::DesignateData`] +/// connection: payload one byte, 1 to write the stamp and 0 to judge only. +/// Answered [`MSG_DESIGNATED`] or [`MSG_REFUSED`]. +pub const MSG_DESIGNATE: u32 = 10; +/// A [`Designated`]. +pub const MSG_DESIGNATED: u32 = 11; /// The bytes of a GUID payload. pub const GUID_BYTES: usize = 16; @@ -235,6 +256,11 @@ pub enum Refusal { /// minter of this protocol stamps, reaches none, and its listing is /// refused the same. NotGranted, + /// The partition backs the running system, which no authorization + /// changes beneath it. + Active, + /// The device did not do what the operation needed of it. + Device, } impl Refusal { @@ -247,6 +273,8 @@ impl Refusal { Self::Exhausted => 5, Self::ClaimRefused => 6, Self::NotGranted => 7, + Self::Active => 8, + Self::Device => 9, } } @@ -259,6 +287,8 @@ impl Refusal { 5 => Some(Self::Exhausted), 6 => Some(Self::ClaimRefused), 7 => Some(Self::NotGranted), + 8 => Some(Self::Active), + 9 => Some(Self::Device), _ => None, } } @@ -272,6 +302,217 @@ impl Refusal { } } +/// What a connector to the block service reaches: the badge the supervisor +/// minted it with. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Badge { + /// Sessions on the partitions a [`Grant`] admits. + Partitions(Grant), + /// Every disk's table, read: a listing of every partition and each disk's + /// description. No session and no change. + Tables, + /// Designations of the machine's DATA partition, resolved when one is + /// asked for: the one partition of `TOYOS_DATA`'s type the service + /// serves. Nothing else. + DesignateData, +} + +impl Badge { + pub fn encode(&self) -> Vec { + match self { + Self::Partitions(grant) => grant.encode().to_vec(), + Self::Tables => vec![3], + Self::DesignateData => vec![4], + } + } + + /// `None` for bytes no minter of this protocol stamps. + pub fn decode(bytes: &[u8]) -> Option { + match bytes.first()? { + 1 | 2 => Grant::decode(bytes).map(Self::Partitions), + 3 => (bytes.len() == 1).then_some(Self::Tables), + 4 => (bytes.len() == 1).then_some(Self::DesignateData), + _ => None, + } + } + + /// Whether a listing through it names the partition `unique` of type `kind`. + pub fn lists(&self, unique: [u8; GUID_BYTES], kind: [u8; GUID_BYTES]) -> bool { + match self { + Self::Partitions(grant) => grant.admits(unique, kind), + Self::Tables => true, + Self::DesignateData => false, + } + } +} + +/// The longest copy state a [`DiskHead`] carries. +pub const MAX_STATE: usize = 200; + +/// One disk as its driver describes it: its size, and what each copy of its +/// table is — `valid` or why not. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiskHead { + pub lba_bytes: u32, + pub sectors: u64, + /// Both copies valid and the same table. + pub agree: bool, + pub primary: String, + pub backup: String, +} + +impl DiskHead { + pub fn encode(&self) -> Vec { + let mut out = Vec::with_capacity(18 + self.primary.len() + self.backup.len() + 4); + out.extend_from_slice(&self.lba_bytes.to_le_bytes()); + out.extend_from_slice(&self.sectors.to_le_bytes()); + out.push(u8::from(self.agree)); + for state in [&self.primary, &self.backup] { + let state = &state.as_bytes()[..floor_char(state, MAX_STATE)]; + out.extend_from_slice(&(state.len() as u16).to_le_bytes()); + out.extend_from_slice(state); + } + out + } + + pub fn decode(bytes: &[u8]) -> Option { + let (lba_bytes, rest) = bytes.split_first_chunk::<4>()?; + let (sectors, rest) = rest.split_first_chunk::<8>()?; + let (&[agree], rest) = rest.split_first_chunk::<1>()?; + let agree = match agree { + 0 => false, + 1 => true, + _ => return None, + }; + let (primary, rest) = text(rest)?; + let (backup, rest) = text(rest)?; + rest.is_empty().then(|| Self { + lba_bytes: u32::from_le_bytes(*lba_bytes), + sectors: u64::from_le_bytes(*sectors), + agree, + primary, + backup, + }) + } +} + +/// The longest prefix of `s` within `max` bytes that ends on a character. +fn floor_char(s: &str, max: usize) -> usize { + (0..=s.len().min(max)).rev().find(|&i| s.is_char_boundary(i)).unwrap_or(0) +} + +/// A length-prefixed UTF-8 string of at most [`MAX_STATE`] bytes, and what follows it. +fn text(bytes: &[u8]) -> Option<(String, &[u8])> { + let (len, rest) = bytes.split_first_chunk::<2>()?; + let len = u16::from_le_bytes(*len) as usize; + if len > MAX_STATE { + return None; + } + let (s, rest) = rest.split_at_checked(len)?; + Some((core::str::from_utf8(s).ok()?.into(), rest)) +} + +/// One used entry of the table in force, as [`MSG_SLOT`] carries it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SlotLine { + pub index: u32, + pub kind: [u8; GUID_BYTES], + pub unique: [u8; GUID_BYTES], + pub first: u64, + pub last: u64, + pub attributes: u64, + pub name: [u16; 36], + /// A session or the driver's own work holds it now. + pub held: bool, + /// The driver serves sessions on it. + pub served: bool, +} + +impl SlotLine { + pub const BYTES: usize = 4 + 2 * GUID_BYTES + 3 * 8 + 72 + 2; + + pub fn encode(&self) -> [u8; Self::BYTES] { + let mut out = [0u8; Self::BYTES]; + out[..4].copy_from_slice(&self.index.to_le_bytes()); + out[4..20].copy_from_slice(&self.kind); + out[20..36].copy_from_slice(&self.unique); + out[36..44].copy_from_slice(&self.first.to_le_bytes()); + out[44..52].copy_from_slice(&self.last.to_le_bytes()); + out[52..60].copy_from_slice(&self.attributes.to_le_bytes()); + for (pair, unit) in out[60..132].as_chunks_mut::<2>().0.iter_mut().zip(self.name) { + *pair = unit.to_le_bytes(); + } + out[132] = u8::from(self.held); + out[133] = u8::from(self.served); + out + } + + pub fn decode(bytes: &[u8]) -> Option { + let b: &[u8; Self::BYTES] = bytes.try_into().ok()?; + let flag = |v: u8| match v { + 0 => Some(false), + 1 => Some(true), + _ => None, + }; + let mut name = [0u16; 36]; + for (unit, pair) in name.iter_mut().zip(b[60..132].as_chunks::<2>().0) { + *unit = u16::from_le_bytes(*pair); + } + Some(Self { + index: u32::from_le_bytes(b[..4].try_into().ok()?), + kind: b[4..20].try_into().ok()?, + unique: b[20..36].try_into().ok()?, + first: u64::from_le_bytes(b[36..44].try_into().ok()?), + last: u64::from_le_bytes(b[44..52].try_into().ok()?), + attributes: u64::from_le_bytes(b[52..60].try_into().ok()?), + name, + held: flag(b[132])?, + served: flag(b[133])?, + }) + } +} + +/// What a designation came to, once it was admitted. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Designated { + /// The stamp is on the medium and read back. + Written, + /// A judge-only request: the stamp would be written. + WouldWrite, + /// A stamp naming this partition's size is there already; nothing written. + Already, + /// The partition holds a volume of ToyOS's own; nothing written. + Volume, + /// The partition carries another format's signature, named; nothing written. + Foreign(String), +} + +impl Designated { + pub fn encode(&self) -> Vec { + let (word, text): (u32, &str) = match self { + Self::Written => (1, ""), + Self::WouldWrite => (2, ""), + Self::Already => (3, ""), + Self::Volume => (4, ""), + Self::Foreign(name) => (5, &name[..floor_char(name, MAX_STATE)]), + }; + [&word.to_le_bytes()[..], text.as_bytes()].concat() + } + + pub fn decode(bytes: &[u8]) -> Option { + let (word, text) = bytes.split_first_chunk::<4>()?; + let empty = text.is_empty(); + match u32::from_le_bytes(*word) { + 1 if empty => Some(Self::Written), + 2 if empty => Some(Self::WouldWrite), + 3 if empty => Some(Self::Already), + 4 if empty => Some(Self::Volume), + 5 if text.len() <= MAX_STATE => Some(Self::Foreign(core::str::from_utf8(text).ok()?.into())), + _ => None, + } + } +} + /// A GUID payload, exactly [`GUID_BYTES`] long. pub fn guid(bytes: &[u8]) -> Option<[u8; GUID_BYTES]> { bytes.try_into().ok() diff --git a/lib/blockserve/Cargo.toml b/lib/blockserve/Cargo.toml index f05172ef34a..37a30901706 100644 --- a/lib/blockserve/Cargo.toml +++ b/lib/blockserve/Cargo.toml @@ -11,3 +11,4 @@ toyos = { path = "../../sdk/toyos" } toyos-blockring = { path = "../blockring" } toyos-blockhold = { path = "../blockhold" } toyos-gpt = { path = "../gpt" } +bcachefs = { path = "../bcachefs" } diff --git a/lib/blockserve/src/content.rs b/lib/blockserve/src/content.rs new file mode 100644 index 00000000000..0e3684aec71 --- /dev/null +++ b/lib/blockserve/src/content.rs @@ -0,0 +1,112 @@ +//! What the front of a partition says it holds, judged before a designation +//! writes there. +//! +//! **A bug fence, not a licence**: a partition is designated because its +//! authorization says so, and a signature recognised here refuses it, since +//! a volume somebody can name is the likeliest mistake. Finding none proves +//! nothing about what the partition holds. Each offset is the format's own; +//! where a published document defines it, it is named beside it. + +/// How much of a partition [`judge`] reads: every signature below lies in it. +pub const READ_BYTES: usize = 1 << 20; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Content { + /// A volume of ToyOS's own format. + Volume, + /// A designation stamp naming this many blocks. + Designated(u64), + /// Another format's signature, by name. + Foreign(&'static str), + /// Nothing recognised. + Unrecognised, +} + +/// `bytes` at `at` in `front`. +fn has(front: &[u8], at: usize, bytes: &[u8]) -> bool { + front.get(at..at + bytes.len()) == Some(bytes) +} + +/// Every format recognised, by name: where its signature is, and what it is. +const SIGNATURES: &[(&str, usize, &[u8])] = &[ + // The ext4 Disk Layout: the superblock at 1024, `s_magic` 0xEF53 at its byte 0x38. + ("an ext2/3/4 file system", 1024 + 0x38, &[0x53, 0xEF]), + // XFS Algorithms & Data Structures, the superblock: `sb_magicnum` "XFSB" at 0. + ("an XFS file system", 0, b"XFSB"), + // Btrfs on-disk format: the superblock at 64 KiB, its magic at 0x40. + ("a btrfs file system", 0x1_0000 + 0x40, b"_BHRfS_M"), + // NTFS boot sector: the OEM ID at 3. + ("an NTFS file system", 3, b"NTFS "), + // exFAT File System Specification §3.1.2: FileSystemName at 3. + ("an exFAT file system", 3, b"EXFAT "), + // Microsoft FAT Specification: BS_FilSysType at 54 (FAT12/16) and 82 (FAT32). + ("a FAT file system", 54, b"FAT1"), + ("a FAT32 file system", 82, b"FAT32 "), + // LUKS1 On-Disk Format Specification and LUKS2's: the magic at 0. + ("a LUKS encrypted volume", 0, b"LUKS\xBA\xBE"), + // Linux swap: "SWAPSPACE2" ending a 4 KiB page. + ("a Linux swap area", 4096 - 10, b"SWAPSPACE2"), + // LVM2's physical volume label: "LABELONE" opening the second sector. + ("an LVM2 physical volume", 512, b"LABELONE"), + // md RAID's version 1.1 and 1.2 superblocks: magic 0xa92b4efc at 0 and 4 KiB. + ("an md RAID member", 0, &[0xFC, 0x4E, 0x2B, 0xA9]), + ("an md RAID member", 4096, &[0xFC, 0x4E, 0x2B, 0xA9]), + // ECMA-119: the first volume descriptor at 32 KiB, its identifier "CD001" at 1. + ("an ISO 9660 image", 0x8000 + 1, b"CD001"), + // Apple File System Reference: the container superblock's `nx_magic` "NXSB" at 32. + ("an APFS container", 32, b"NXSB"), + // Technical Note TN1150: the HFS Plus volume header at 1024, signature "H+" or "HX". + ("an HFS Plus volume", 1024, b"H+"), + ("an HFSX volume", 1024, b"HX"), + // F2FS: the superblock at 1024, magic 0xF2F52010. + ("an F2FS file system", 1024, &[0x10, 0x20, 0xF5, 0xF2]), + // UEFI 2.10 §5.3.2: a GPT header, "EFI PART", at LBA 1 of 512-byte blocks. + ("a GPT partition table", 512, b"EFI PART"), + // An MBR or boot sector's signature, 0x55AA at 510. + ("a boot sector", 510, &[0x55, 0xAA]), +]; + +/// What the front of a partition of `blocks` blocks holds: `front` is its +/// first [`READ_BYTES`], or all of it on a smaller one. +pub fn judge(front: &[u8], blocks: u64) -> Content { + if bcachefs::carries_volume(front) { + return Content::Volume; + } + if let Some(stamped) = bcachefs::designated(front) { + return if stamped == blocks { Content::Designated(stamped) } else { Content::Foreign("a stamp for another size") }; + } + match SIGNATURES.iter().find(|(_, at, bytes)| has(front, *at, bytes)) { + Some((name, _, _)) => Content::Foreign(name), + None => Content::Unrecognised, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Each signature is recognised where its format puts it, a ToyOS volume + /// and a stamp are told apart from them, and a stamp for another size is + /// not taken for this partition's. + #[test] + fn each_format_is_recognised_where_it_puts_its_signature() { + let blank = vec![0u8; READ_BYTES]; + assert_eq!(judge(&blank, 100), Content::Unrecognised); + for (name, at, bytes) in SIGNATURES { + let mut front = blank.clone(); + front[*at..*at + bytes.len()].copy_from_slice(bytes); + assert_eq!(judge(&front, 100), Content::Foreign(name), "{name}"); + let mut moved = blank.clone(); + moved[*at + 1..*at + 1 + bytes.len()].copy_from_slice(bytes); + assert_ne!(judge(&moved, 100), Content::Foreign(name), "{name} one byte off"); + } + let mut stamped = blank.clone(); + stamped[..4096].copy_from_slice(&bcachefs::designation(100)); + assert_eq!(judge(&stamped, 100), Content::Designated(100)); + assert_eq!(judge(&stamped, 99), Content::Foreign("a stamp for another size")); + let mut ours = blank.clone(); + ours[..4].copy_from_slice(b"BCFS"); + assert_eq!(judge(&ours, 100), Content::Volume); + assert_eq!(judge(&blank[..4096], 1), Content::Unrecognised, "a partition shorter than the read"); + } +} diff --git a/lib/blockserve/src/designate.rs b/lib/blockserve/src/designate.rs new file mode 100644 index 00000000000..ead324504c2 --- /dev/null +++ b/lib/blockserve/src/designate.rs @@ -0,0 +1,315 @@ +//! A designation's steps, apart from the device and memory they run on: read +//! the front of the partition, judge it ([`crate::content`]), and only for a +//! partition it does not recognise and a request that writes, write the stamp +//! as one whole block built in memory, flush, read it back and compare. +//! +//! The caller issues each [`Ask`] on its device and reports each command's +//! answer; one command is on the device at a time. + +use toyos_blockring::wire::{Designated, Refusal}; +use toyos_blockring::{BLOCK_BYTES, MAX_REQUEST_BLOCKS}; + +use crate::content::{self, Content}; + +/// Where in the job's memory each piece is: the front read, the stamp, and +/// the stamp read back, after a first block the job does not use. +pub const FRONT: usize = BLOCK_BYTES; +pub const STAMP: usize = FRONT + content::READ_BYTES; +pub const BACK: usize = STAMP + BLOCK_BYTES; +/// The memory a job needs: a session's region holds it. +pub const BYTES: usize = BACK + BLOCK_BYTES; +const _: () = assert!(BYTES <= toyos_blockring::SESSION_BYTES); + +/// The job's memory, which the device moves blocks into and out of. +pub trait Memory { + fn copy_out(&self, at: usize, out: &mut [u8]); + fn copy_in(&mut self, at: usize, data: &[u8]); +} + +/// What the job asks of its caller next. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Ask { + /// Nothing until a command is answered, or until the device has room. + Wait, + /// Read `count` blocks from the partition's block `block` into memory at `at`. + Read { block: u64, count: u32, at: usize }, + /// Write the block in memory at `at` to the partition's block `block`. + Write { block: u64, at: usize }, + /// Flush the device's volatile cache; a device with none is answered at once. + Flush, + Done(Result), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Phase { + Read { next: u64, end: u64 }, + Write, + Flush, + Back, + Compare, +} + +pub struct Designation { + blocks: u64, + commit: bool, + phase: Phase, + /// A command is on the device. + issued: bool, + /// A command failed, or the device was reset beneath one. + failed: bool, +} + +impl Designation { + /// A designation of a partition of `blocks` blocks: one that writes when + /// `commit`, and judges only otherwise. + pub fn new(blocks: u64, commit: bool) -> Self { + let end = blocks.min((content::READ_BYTES / BLOCK_BYTES) as u64); + Self { blocks, commit, phase: Phase::Read { next: 0, end }, issued: false, failed: false } + } + + /// The next step, given whether the device has `room` for a command. + pub fn next(&mut self, memory: &mut dyn Memory, room: bool) -> Ask { + if self.issued { + return Ask::Wait; + } + if self.failed { + return Ask::Done(Err(Refusal::Device)); + } + let needs_device = match self.phase { + Phase::Read { next, end } => next < end, + Phase::Compare => false, + Phase::Write | Phase::Flush | Phase::Back => true, + }; + if needs_device && !room { + return Ask::Wait; + } + match self.phase { + Phase::Read { next, end } if next < end => { + let count = (end - next).min(u64::from(MAX_REQUEST_BLOCKS)); + self.phase = Phase::Read { next: next + count, end }; + self.issue(Ask::Read { block: next, count: count as u32, at: FRONT + next as usize * BLOCK_BYTES }) + } + Phase::Read { end, .. } => { + let mut front = vec![0u8; end as usize * BLOCK_BYTES]; + memory.copy_out(FRONT, &mut front); + match content::judge(&front, self.blocks) { + Content::Volume => Ask::Done(Ok(Designated::Volume)), + Content::Designated(_) => Ask::Done(Ok(Designated::Already)), + Content::Foreign(name) => Ask::Done(Ok(Designated::Foreign(name.into()))), + Content::Unrecognised if !self.commit => Ask::Done(Ok(Designated::WouldWrite)), + Content::Unrecognised => { + memory.copy_in(STAMP, &bcachefs::designation(self.blocks)); + self.phase = Phase::Write; + self.next(memory, room) + } + } + } + Phase::Write => { + self.phase = Phase::Flush; + self.issue(Ask::Write { block: 0, at: STAMP }) + } + Phase::Flush => { + self.phase = Phase::Back; + self.issue(Ask::Flush) + } + Phase::Back => { + self.phase = Phase::Compare; + self.issue(Ask::Read { block: 0, count: 1, at: BACK }) + } + Phase::Compare => { + let (mut back, mut stamp) = ([0u8; BLOCK_BYTES], [0u8; BLOCK_BYTES]); + memory.copy_out(BACK, &mut back); + memory.copy_out(STAMP, &mut stamp); + Ask::Done(if back == stamp { Ok(Designated::Written) } else { Err(Refusal::Device) }) + } + } + } + + fn issue(&mut self, ask: Ask) -> Ask { + self.issued = true; + ask + } + + /// The command on the device was answered. + pub fn answered(&mut self, ok: bool) { + assert!(self.issued, "an answer for a designation with nothing on the device"); + self.issued = false; + self.failed |= !ok; + } + + /// The device was reset beneath the command on it, which is never answered. + pub fn reset(&mut self) { + if self.issued { + self.issued = false; + self.failed = true; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + impl Memory for Vec { + fn copy_out(&self, at: usize, out: &mut [u8]) { + out.copy_from_slice(&self[at..at + out.len()]); + } + fn copy_in(&mut self, at: usize, data: &[u8]) { + self[at..at + data.len()].copy_from_slice(data); + } + } + + /// A partition of whole blocks, a device with a volatile cache that holds + /// writes until a flush, and every command the job issued. + struct Disk { + blocks: Vec<[u8; BLOCK_BYTES]>, + cached: Vec<(u64, [u8; BLOCK_BYTES])>, + cache: bool, + /// The command number that fails, counted from 0. + fails: Option, + /// A device that answers a write it did not keep. + drops_writes: bool, + commands: Vec, + } + + impl Disk { + fn new(blocks: usize, fill: impl Fn(usize) -> u8) -> Self { + let blocks = (0..blocks).map(|b| [fill(b); BLOCK_BYTES]).collect(); + Self { blocks, cached: Vec::new(), cache: true, fails: None, drops_writes: false, commands: Vec::new() } + } + + /// Run `job` to its end, answering each command as the device would. + fn run(&mut self, job: &mut Designation, memory: &mut Vec) -> Result { + loop { + let ask = job.next(memory, true); + let n = self.commands.len(); + self.commands.push(ask.clone()); + let ok = self.fails != Some(n); + match ask { + Ask::Done(done) => return done, + Ask::Wait => panic!("a job with nothing on the device waits"), + Ask::Read { block, count, at } => { + for i in 0..count as usize { + let b = block as usize + i; + let data = self.cached.iter().rev().find(|(c, _)| *c as usize == b).map_or(self.blocks[b], |(_, d)| *d); + memory.copy_in(at + i * BLOCK_BYTES, &data); + } + } + Ask::Write { block, at } => { + let mut data = [0u8; BLOCK_BYTES]; + memory.copy_out(at, &mut data); + if !self.drops_writes { + if self.cache { + self.cached.push((block, data)); + } else { + self.blocks[block as usize] = data; + } + } + } + Ask::Flush => { + for (b, d) in self.cached.drain(..) { + self.blocks[b as usize] = d; + } + } + } + job.answered(ok); + } + } + } + + fn memory() -> Vec { + vec![0; BYTES] + } + + /// An unrecognised partition is stamped on the medium, flushed before it + /// is read back, and the rest of it is untouched; the front it judged is + /// the first 1 MiB, in requests no larger than the ring allows. + #[test] + fn an_unrecognised_partition_is_stamped_flushed_and_read_back() { + let mut disk = Disk::new(1000, |b| (b % 251) as u8 | 1); + let mut job = Designation::new(1000, true); + assert_eq!(disk.run(&mut job, &mut memory()), Ok(Designated::Written)); + assert!(disk.blocks[0] == bcachefs::designation(1000), "the stamp is on the medium, not in the cache"); + assert!(disk.blocks[1..].iter().enumerate().all(|(i, b)| b[0] == ((i + 1) % 251) as u8 | 1)); + let reads: u64 = disk + .commands + .iter() + .filter_map(|c| match c { + Ask::Read { count, at, .. } if *at >= FRONT && *at < STAMP => Some(u64::from(*count)), + _ => None, + }) + .sum(); + assert_eq!(reads, 256); + assert!(disk.commands.iter().all(|c| !matches!(c, Ask::Read { count, .. } if *count > MAX_REQUEST_BLOCKS))); + let order: Vec<&str> = disk + .commands + .iter() + .filter_map(|c| match c { + Ask::Write { .. } => Some("write"), + Ask::Flush => Some("flush"), + Ask::Read { at, .. } if *at == BACK => Some("back"), + _ => None, + }) + .collect(); + assert_eq!(order, ["write", "flush", "back"]); + } + + /// What the judge recognises is answered and nothing is written; a judge-only + /// request writes nothing either. + #[test] + fn a_recognised_or_judged_partition_is_never_written() { + let mut ours = Disk::new(300, |_| 0); + ours.blocks[0][..4].copy_from_slice(b"BCFS"); + let mut stamped = Disk::new(300, |_| 0); + stamped.blocks[0] = bcachefs::designation(300); + let mut ext4 = Disk::new(300, |_| 0); + ext4.blocks[0][1080..1082].copy_from_slice(&[0x53, 0xEF]); + let cases = [ + (ours, true, Designated::Volume), + (stamped, true, Designated::Already), + (ext4, true, Designated::Foreign("an ext2/3/4 file system".into())), + (Disk::new(300, |_| 7), false, Designated::WouldWrite), + ]; + for (mut disk, commit, want) in cases { + let before = disk.blocks.clone(); + let mut job = Designation::new(300, commit); + assert_eq!(disk.run(&mut job, &mut memory()), Ok(want.clone())); + assert!(disk.blocks == before && disk.commands.iter().all(|c| !matches!(c, Ask::Write { .. } | Ask::Flush)), "{want:?}"); + } + } + + /// A failed command, a device that drops the write it answered, and a + /// reset beneath a command each end the job refused `Device`, never + /// `Written`. + #[test] + fn a_failed_or_unkept_write_is_never_answered_written() { + let mut job = Designation::new(300, true); + let mut probe = Disk::new(300, |_| 3); + probe.run(&mut job, &mut memory()).unwrap(); + let write = probe.commands.iter().position(|c| matches!(c, Ask::Write { .. })).unwrap(); + for fails in [0, write, write + 1, write + 2] { + let mut disk = Disk { fails: Some(fails), ..Disk::new(300, |_| 3) }; + assert_eq!(disk.run(&mut Designation::new(300, true), &mut memory()), Err(Refusal::Device), "command {fails} failed"); + } + let mut dropping = Disk { drops_writes: true, ..Disk::new(300, |_| 3) }; + assert_eq!(dropping.run(&mut Designation::new(300, true), &mut memory()), Err(Refusal::Device)); + + let mut job = Designation::new(300, true); + let mut mem = memory(); + assert!(matches!(job.next(&mut mem, true), Ask::Read { .. })); + job.reset(); + assert_eq!(job.next(&mut mem, true), Ask::Done(Err(Refusal::Device))); + } + + /// No room on the device is a wait, never a skipped step; a device with no + /// cache answers the flush at once and the stamp still reads back. + #[test] + fn a_full_device_waits_and_a_cacheless_one_still_reads_back() { + let mut job = Designation::new(10, true); + let mut mem = memory(); + assert_eq!(job.next(&mut mem, false), Ask::Wait); + let mut disk = Disk { cache: false, ..Disk::new(10, |_| 9) }; + assert_eq!(disk.run(&mut job, &mut mem), Ok(Designated::Written)); + assert!(disk.blocks[0] == bcachefs::designation(10), "the stamp is on the medium"); + } +} diff --git a/lib/blockserve/src/lib.rs b/lib/blockserve/src/lib.rs index 401ee903359..78477be870a 100644 --- a/lib/blockserve/src/lib.rs +++ b/lib/blockserve/src/lib.rs @@ -7,6 +7,8 @@ //! Every disk driver links this, whatever its controller: what differs is the //! [`sessions::Device`] beneath. +pub mod content; +pub mod designate; pub mod link; pub mod region; pub mod sessions; diff --git a/lib/blockserve/src/link.rs b/lib/blockserve/src/link.rs index 4f091143fa7..e7a698d9201 100644 --- a/lib/blockserve/src/link.rs +++ b/lib/blockserve/src/link.rs @@ -17,12 +17,21 @@ use toyos::ipc::{Connection, FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos_blockring::disk::{self, Offer, Part, Settled}; -/// A session the service handed over: what to open, the client's connection -/// and the region it sent. -pub struct Handed { - pub session: disk::Session, - pub conn: Connection, - pub region: toyos::RawHandle, +/// What the service handed over, each with the client's connection the +/// driver answers on. +pub enum Handed { + /// A session to open, over the region the client sent. + Session { session: disk::Session, conn: Connection, region: toyos::RawHandle }, + /// A description of one of this driver's disks. + Describe { disk: u32, conn: Connection }, + Designate { order: disk::Designation, conn: Connection }, +} + +/// The longest frame the service sends: a session, a designation, or a disk's number. +const FRAME: usize = max(max(disk::Session::BYTES, disk::Designation::BYTES), 4); + +const fn max(a: usize, b: usize) -> usize { + if a > b { a } else { b } } /// Fresh connections in a row that may refuse what the driver says before it @@ -33,7 +42,7 @@ pub struct Link { label: String, /// `None` once the service's port has closed for good. conn: Option, - rx: FrameRx<{ disk::Session::BYTES }>, + rx: FrameRx<{ FRAME }>, offers: BTreeMap>, settled: Option, } @@ -134,7 +143,7 @@ impl Link { } } - /// Every session the service handed over since the last call. Anything + /// Everything the service handed over since the last call. Anything /// else it sends is the service breaking the protocol, which ends the /// connection. pub fn take(&mut self) -> Vec { @@ -151,7 +160,7 @@ impl Link { match (session, handles) { (Some(session), Some([conn, region])) => { let conn = Connection::received(conn); - handed.push(Handed { session, conn, region }); + handed.push(Handed::Session { session, conn, region }); continue; } (_, Some(handles)) => { @@ -161,6 +170,26 @@ impl Link { (_, None) => "a session without its two handles", } } + RxStep::Frame { msg_type: msg_type @ (disk::MSG_DESCRIBE | disk::MSG_DESIGNATE), payload_len, .. } => { + let payload = self.rx.payload(payload_len).to_vec(); + match self.rx.take_handles::<1>() { + Some([conn]) => { + let conn = Connection::received(conn); + let asked = match msg_type { + disk::MSG_DESCRIBE => disk::disk(&payload).map(|disk| Handed::Describe { disk, conn }), + _ => disk::Designation::decode(&payload).map(|order| Handed::Designate { order, conn }), + }; + match asked { + Some(asked) => { + handed.push(asked); + continue; + } + None => "a description or designation frame this protocol does not make", + } + } + None => "a description or designation without the client's connection", + } + } RxStep::Frame { .. } => "a frame this protocol does not make", }; self.ended(why); diff --git a/lib/blockserve/src/region.rs b/lib/blockserve/src/region.rs index 1200b36e160..8ffd6cc93f0 100644 --- a/lib/blockserve/src/region.rs +++ b/lib/blockserve/src/region.rs @@ -51,6 +51,12 @@ impl Region { whole.sub(run.span().offset, run.span().len) } + /// The whole region, for the driver's own work on a region it made. + pub fn window(&self) -> Window { + // SAFETY: as `arena`'s. + unsafe { Window::new(self.memory.as_ptr(), SESSION_BYTES) } + } + /// A second handle to the region, for a send. pub fn share(&self) -> Result { self.memory.share() diff --git a/lib/blockserve/src/sessions.rs b/lib/blockserve/src/sessions.rs index 77f20259f1b..f8eb4ecde97 100644 --- a/lib/blockserve/src/sessions.rs +++ b/lib/blockserve/src/sessions.rs @@ -12,6 +12,12 @@ //! **A server never blocks on a client.** The open is answered with one //! `try_send` on the client's own connection, and a doorbell is one //! non-blocking write whose refusal is a full pipe — a doorbell already rung. +//! +//! **A designation is the driver's own work on one partition** ([`Job`]): it +//! holds the partition as a session would for as long as it runs, so nothing +//! opens it meanwhile, and it reaches the device through the same [`Device`] +//! as every session. What it writes is the stamp alone, which it makes +//! itself; no client's bytes reach the disk through it. use std::collections::BTreeMap; @@ -23,10 +29,13 @@ use toyos_blockring::disk::Part; use toyos_blockring::entry::{Completion, Op}; use toyos_blockring::layout::{ServerRings, DEPTH}; use toyos_blockring::server::{ServerSession, Taken}; -use toyos_blockring::wire::{self, Opened, Refusal}; +use toyos_blockring::disk::Designation; +use toyos_blockring::wire::{self, Designated, DiskHead, Opened, Refusal, SlotLine}; use toyos_blockring::Status; +use crate::designate::{self, Ask}; use crate::region::Region; +use crate::table::{self, Read}; use crate::Partition; /// Sessions one disk holds at once. Each costs its region's mapping in the @@ -72,21 +81,57 @@ struct Served { posted: bool, } +/// The session id the driver's own work answers under: no session is ever +/// numbered so. +const JOB: u64 = u64::MAX; + +/// A designation in progress, on a partition it holds. +struct Job { + conn: Connection, + order: Designation, + steps: designate::Designation, + region: Region, + device_addr: u64, + first: u64, +} + +impl designate::Memory for Region { + fn copy_out(&self, at: usize, out: &mut [u8]) { + self.window().copy_out(at, out); + } + fn copy_in(&mut self, at: usize, data: &[u8]) { + self.window().copy_in(at, data); + } +} + /// Every session on one disk, and the partitions its table names. pub struct Sessions { /// Who says this disk's lines. name: String, parts: Vec, + read: Read, holds: Holds, /// The device's loss count: every reset may have dropped its cache. losses: u64, sessions: BTreeMap, next_id: u64, + job: Option, } impl Sessions { - pub fn new(name: String, parts: Vec) -> Self { - Self { name, parts, holds: Holds::new(), losses: 0, sessions: BTreeMap::new(), next_id: 0 } + /// The disk `read` describes. + pub fn new(name: String, read: Read) -> Self { + let parts = table::say(&name, &read); + Self { + name, + parts, + read, + holds: Holds::new(), + losses: 0, + sessions: BTreeMap::new(), + next_id: 0, + job: None, + } } /// What the block service is offered of this disk. @@ -191,6 +236,13 @@ impl Sessions { /// Hand one device answer to the session it is for. pub fn answered(&mut self, owner: Owner, ok: bool, status: u32) { + if owner.session == JOB { + let job = self.job.as_mut().unwrap_or_else(|| panic!("{}: an answer for a designation that ended", self.name)); + if !ok { + println!("{}: the designation's command failed with status {status:#x}", self.name); + } + return job.steps.answered(ok); + } let s = self.sessions.get_mut(&owner.session).unwrap_or_else(|| { panic!("{}: an answer for session {}, which ended with requests on the device", self.name, owner.session) }); @@ -259,6 +311,7 @@ impl Sessions { } s.rings.0.release(s.region.words()); } + self.work(dev); } /// Publish what each session was answered, and ring its doorbell. @@ -319,6 +372,9 @@ impl Sessions { /// dropped the device's cache. Answers how many were the sessions'. pub fn reset(&mut self) -> usize { self.losses += 1; + if let Some(job) = &mut self.job { + job.steps.reset(); + } let mut answered = 0; for s in self.sessions.values_mut() { for c in s.state.abort_all() { @@ -333,6 +389,7 @@ impl Sessions { /// client told, and every session ended. pub fn end(&mut self, dev: &mut impl Device) { self.reset(); + self.work(dev); for s in self.sessions.values_mut() { s.closing = true; } @@ -343,6 +400,123 @@ impl Sessions { pub fn losses(&self) -> u64 { self.losses } + + /// Describe the disk to `conn`: its head, then each used entry of the + /// table in force, then the end. One `try_send` each; a client that will + /// not take them is let go. + pub fn describe(&self, conn: Connection) { + let copies = self.read.copies.as_deref(); + let head = DiskHead { + lba_bytes: self.read.lba_bytes, + sectors: self.read.sectors, + agree: copies.is_some_and(|c| c.agree()), + primary: copies.map_or_else(|| "unread".into(), |c| table::state(&c.primary)), + backup: copies.map_or_else(|| "unread".into(), |c| table::state(&c.backup)), + }; + if conn.try_send_bytes(wire::MSG_DISK, &head.encode()).is_err() { + return; + } + if let Some((_, in_force)) = copies.and_then(|c| c.in_force()) { + for (index, slot) in in_force.used() { + let span = self.parts.iter().find(|p| p.unique == slot.unique_guid.0).and_then(|p| p.span.as_ref().ok()); + let line = SlotLine { + index, + kind: slot.type_guid.0, + unique: slot.unique_guid.0, + first: slot.first, + last: slot.last, + attributes: slot.attributes, + name: slot.name, + held: span.is_some_and(|&(first, blocks)| self.holds.holder(first, first + blocks).is_some()), + served: span.is_some(), + }; + if conn.try_send_bytes(wire::MSG_SLOT, &line.encode()).is_err() { + return; + } + } + } + let _ = conn.try_send_bytes(wire::MSG_DESCRIBED, &[]); + } + + /// The span `order` designates, held for it; or its refusal, judged + /// against this disk's own state in this turn: the running system's + /// partitions, its table and its holds. + fn admit(&mut self, order: &Designation, dev: &mut impl Device) -> Result<(u64, u64), Refusal> { + if self.job.is_some() { + return Err(Refusal::Exhausted); + } + if order.active { + return Err(Refusal::Active); + } + self.place(order.unique, dev) + } + + /// Begin the designation the service admitted, answering `conn` when it + /// ends; or refuse it now ([`Self::admit`]). + pub fn designate(&mut self, dev: &mut impl Device, conn: Connection, order: Designation) { + let what = crate::guid_text(order.unique); + let name = self.name.clone(); + let refuse = |conn: &Connection, why: Refusal| { + println!("{name}: a designation of {what} refused: {why:?}"); + let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); + }; + let (first, blocks) = match self.admit(&order, dev) { + Ok(span) => span, + Err(why) => return refuse(&conn, why), + }; + let region = Region::create().and_then(|region| dev.dma_map(region.handle()).map(|at| (region, at))); + let (region, device_addr) = match region { + Ok(mapped) => mapped, + Err(why) => { + self.holds.release(first); + println!("{}: no region for a designation: {why:?}", self.name); + return refuse(&conn, Refusal::Exhausted); + } + }; + println!("{}: designating {what} ({blocks} blocks){}", self.name, if order.commit { "" } else { ", judging only" }); + let steps = designate::Designation::new(blocks, order.commit); + self.job = Some(Job { conn, order, steps, region, device_addr, first }); + self.work(dev); + } + + /// Move the designation on as far as the device lets it. + fn work(&mut self, dev: &mut impl Device) { + while let Some(job) = &mut self.job { + let owner = |write| Owner { session: JOB, tag: 0, write }; + match job.steps.next(&mut job.region, dev.has_room()) { + Ask::Wait => return, + Ask::Read { block, count, at } => dev.submit_io(false, job.first + block, count, job.device_addr + at as u64, owner(false)), + Ask::Write { block, at } => dev.submit_io(true, job.first + block, 1, job.device_addr + at as u64, owner(true)), + Ask::Flush => { + if !dev.submit_flush(owner(false)) { + job.steps.answered(true); + } + } + Ask::Done(outcome) => return self.finish(dev, outcome), + } + } + } + + /// End the designation: its region out of the controller's domain, its + /// partition free, and its client answered. + fn finish(&mut self, dev: &mut impl Device, outcome: Result) { + let job = self.job.take().expect("a designation"); + if let Err(why) = dev.dma_unmap(job.device_addr) { + panic!("{}: the kernel would not take a designation's region back: {why:?}", self.name); + } + self.holds.release(job.first); + let what = crate::guid_text(job.order.unique); + match outcome { + Ok(done) => { + println!("{}: designation of {what}: {done:?}", self.name); + let _ = job.conn.try_send_bytes(wire::MSG_DESIGNATED, &done.encode()); + } + Err(why) => { + println!("{}: designation of {what} failed: {why:?}", self.name); + let _ = job.conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); + } + } + } } #[cfg(test)] @@ -372,13 +546,34 @@ mod tests { } fn disk() -> Sessions { - Sessions::new( - "test".into(), - vec![ - Partition { unique: [1; 16], kind: [9; 16], span: Ok((0, 100)) }, - Partition { unique: [2; 16], kind: [9; 16], span: Err("not whole blocks".into()) }, - ], - ) + let parts = vec![ + Partition { unique: [1; 16], kind: [9; 16], span: Ok((0, 100)) }, + Partition { unique: [2; 16], kind: [9; 16], span: Err("not whole blocks".into()) }, + ]; + Sessions::new("test".into(), Read { lba_bytes: 512, sectors: 1 << 20, copies: None, parts: Ok(parts) }) + } + + fn order(unique: [u8; 16], active: bool) -> Designation { + Designation { disk: 0, unique, active, commit: true } + } + + /// A designation is judged against the disk's own state before it takes + /// anything: a partition the running system is on, one a session holds, + /// one the table does not name and one it will not serve are each + /// refused, and none of them holds the partition. + #[test] + fn a_designation_is_refused_on_the_disks_own_state_before_it_holds_anything() { + let mut disk = disk(); + let refusals = [ + (order([1; 16], true), Refusal::Active), + (order([3; 16], false), Refusal::NotFound), + (order([2; 16], false), Refusal::Unusable), + ]; + for (order, want) in refusals { + assert_eq!(disk.admit(&order, &mut Nothing), Err(want), "{order:?}"); + } + assert_eq!(disk.place([1; 16], &mut Nothing), Ok((0, 100)), "nothing refused held it"); + assert_eq!(disk.admit(&order([1; 16], false), &mut Nothing), Err(Refusal::Held)); } /// What only the disk knows refuses an open the service admitted: a diff --git a/lib/blockserve/src/table.rs b/lib/blockserve/src/table.rs index 661e471d6b4..3557df64067 100644 --- a/lib/blockserve/src/table.rs +++ b/lib/blockserve/src/table.rs @@ -1,4 +1,5 @@ -//! Every partition a disk's table names, read once at the disk's bring-up. +//! Every partition a disk's table names, read once at the disk's bring-up, +//! with what the disk holds of the table: its size and each copy. //! //! Every partition lies outside every other and the table outside all of them, //! so no session can change it: the table a driver read is the one it serves @@ -6,6 +7,7 @@ //! that its own table refuses, is offered and refused by name. use toyos_blockring::BLOCK_BYTES; +use toyos_gpt::edit::Copies; /// One partition of the table, in blocks of the disk. #[derive(Clone, Debug, PartialEq, Eq)] @@ -16,10 +18,28 @@ pub struct Partition { pub span: Result<(u64, u64), String>, } -/// Every partition the table on `sectors` names, each checked past -/// `toyos_gpt::locate`'s range and overlap checks and held to whole blocks; -/// or why the disk carries no table this reads. Pure: [`say`] says it. -pub fn read(sectors: &mut dyn toyos_gpt::Sectors) -> Result, String> { +/// What a driver read of one disk at its bring-up. +pub struct Read { + pub lba_bytes: u32, + pub sectors: u64, + /// LBA 0 and each copy of the table, each read alone; `None` for a disk + /// with no room for a table or whose LBA 0 would not read. + pub copies: Option>, + /// Every partition the table names, or why the disk carries no table + /// this reads. + pub parts: Result, String>, +} + +/// What the table on `sectors` says: every partition it names, each checked +/// past `toyos_gpt::locate`'s range and overlap checks and held to whole +/// blocks, and both copies as they stand. Pure: [`say`] says it. +pub fn read(sectors: &mut dyn toyos_gpt::Sectors) -> Read { + let (lba_bytes, count) = (sectors.lba_bytes(), sectors.lba_count()); + let copies = toyos_gpt::edit::read_copies(sectors).map(Box::new); + Read { lba_bytes, sectors: count, copies, parts: partitions(sectors) } +} + +fn partitions(sectors: &mut dyn toyos_gpt::Sectors) -> Result, String> { let per = BLOCK_BYTES as u64 / u64::from(sectors.lba_bytes()); let mut found = [None; 128]; if let Err(why) = toyos_gpt::list(sectors, &mut found) { @@ -54,9 +74,9 @@ pub fn read(sectors: &mut dyn toyos_gpt::Sectors) -> Result, Stri /// Say what [`read`] found, as `name`, and answer the partitions: none for a /// disk with no table. -pub fn say(name: &str, read: Result, String>) -> Vec { - let parts = match read { - Ok(parts) => parts, +pub fn say(name: &str, read: &Read) -> Vec { + let parts = match &read.parts { + Ok(parts) => parts.clone(), Err(why) => { println!("{name}: the disk carries no partition table this driver reads: {why}"); return Vec::new(); @@ -69,5 +89,19 @@ pub fn say(name: &str, read: Result, String>) -> Vec { Err(why) => println!("{name}: partition {guid} of type {kind} is not served: {why}"), } } + if let Some(copies) = &read.copies + && copies.in_force().is_some() + && !copies.agree() + { + println!("{name}: the table's two copies do not agree: primary {:?}, backup {:?}", state(&copies.primary), state(&copies.backup)); + } parts } + +/// One copy as a line says it. +pub fn state(copy: &Result) -> String { + match copy { + Ok(_) => "valid".into(), + Err(why) => format!("{why:?}"), + } +} diff --git a/lib/gpt/Cargo.toml b/lib/gpt/Cargo.toml index 45ff37c3c48..f19d2150285 100644 --- a/lib/gpt/Cargo.toml +++ b/lib/gpt/Cargo.toml @@ -1,16 +1,16 @@ -# A member of the host workspace (root `Cargo.toml`): the -# kernel depends on it by path and its tests run on the host, with no +# A member of the host workspace (root `Cargo.toml`): the loader and the +# disk drivers depend on it by path and its tests run on the host, with no # target-triple juggling. A GPT is bytes somebody else wrote, so the # parser has to be exercised against hostile bytes far more often than a QEMU # boot allows. [package] name = "toyos-gpt" -description = "GPT parser that finds the partition firmware booted from (no_std, no alloc, forbid(unsafe_code))." +description = "GPT: the parser that finds the partition firmware booted from, and the one writer of a whole table (no_std, no alloc, forbid(unsafe_code))." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" [dev-dependencies] -# The differential oracle of `tests/oracle.rs`. +# The differential oracle of `tests/oracle.rs` and `tests/edit.rs`. gpt = "3.1.0" diff --git a/lib/gpt/src/edit.rs b/lib/gpt/src/edit.rs new file mode 100644 index 00000000000..31a770eb1e0 --- /dev/null +++ b/lib/gpt/src/edit.rs @@ -0,0 +1,420 @@ +//! A whole table: read from either copy, checked, and written back as UEFI +//! 2.10 §5.3.2 requires — the backup before the primary, every CRC resealed. +//! +//! Only the entry array every table in practice has is read or written, +//! [`ENTRIES`] entries of [`ENTRY_BYTES`]; a copy of another shape reads as +//! [`CopyError::Shape`]. The writer lays the arrays where §5.3.1 puts them: +//! the primary's at LBA 2, the backup's ending just before the device's last +//! LBA, which holds the backup header. + +use crate::{check_protective_mbr, header_crc, parse_header, read, Block, Crc32, Disk, GptError, Guid, LbaSize, Sectors}; + +pub const ENTRIES: usize = 128; +pub const ENTRY_BYTES: usize = 128; +/// The array's bytes, which UEFI requires be at least 16,384 (§5.3.1). +pub const ARRAY_BYTES: usize = 16_384; +/// A name is at most 36 UTF-16 code units (§5.3.3). +pub const NAME_UNITS: usize = 36; +const HEADER_BYTES: u32 = 92; +const ENTRIES_U32: u32 = 128; +const ENTRY_BYTES_U32: u32 = 128; +const ARRAY_BYTES_U64: u64 = 16_384; + +/// One entry, every field the table stores. A zero type is an unused entry. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Slot { + pub type_guid: Guid, + pub unique_guid: Guid, + pub first: u64, + /// Inclusive, as GPT stores it. + pub last: u64, + pub attributes: u64, + pub name: [u16; NAME_UNITS], +} + +impl Slot { + pub const UNUSED: Self = + Self { type_guid: Guid::ZERO, unique_guid: Guid::ZERO, first: 0, last: 0, attributes: 0, name: [0; NAME_UNITS] }; + + /// A used slot of no attributes; `None` for a name longer than + /// [`NAME_UNITS`]. + pub fn new(type_guid: Guid, unique_guid: Guid, first: u64, last: u64, name: &str) -> Option { + Some(Self { type_guid, unique_guid, first, last, attributes: 0, name: Self::named(name)? }) + } + + pub const fn is_used(&self) -> bool { + !self.type_guid.is_zero() + } + + /// The name up to its first zero unit. + pub fn name(&self) -> &[u16] { + self.name.split(|unit| *unit == 0).next().unwrap_or_default() + } + + /// `text` as the name field, `None` for one longer than [`NAME_UNITS`]. + pub fn named(text: &str) -> Option<[u16; NAME_UNITS]> { + let mut name = [0; NAME_UNITS]; + let mut units = text.encode_utf16(); + for unit in name.iter_mut() { + *unit = units.next().unwrap_or(0); + } + units.next().is_none().then_some(name) + } + + /// The number of blocks it spans, for a used slot whose range is ordered. + pub fn lba_count(&self) -> Option { + self.last.checked_sub(self.first)?.checked_add(1) + } + + fn decode(raw: &[u8]) -> Option { + let name = crate::name(raw)?; + Some(Self { + type_guid: crate::bytes(raw, 0).map(Guid)?, + unique_guid: crate::bytes(raw, 16).map(Guid)?, + first: crate::le_u64(raw, 32)?, + last: crate::le_u64(raw, 40)?, + attributes: crate::le_u64(raw, 48)?, + name, + }) + } + + fn encode(&self, out: &mut [u8]) { + put(out, 0, &self.type_guid.0); + put(out, 16, &self.unique_guid.0); + put(out, 32, &self.first.to_le_bytes()); + put(out, 40, &self.last.to_le_bytes()); + put(out, 48, &self.attributes.to_le_bytes()); + if let Some(field) = out.get_mut(56..) { + for (pair, unit) in field.as_chunks_mut::<2>().0.iter_mut().zip(self.name) { + *pair = unit.to_le_bytes(); + } + } + } +} + +/// A disk's size as the table is laid on it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Geometry { + lba: LbaSize, + lba_count: u64, + array_lbas: u64, +} + +impl Geometry { + /// A device of `lba_count` blocks of `lba_bytes`: `None` for a block size + /// [`crate::LbaSize`] does not parse, or a device with no room for an MBR, + /// both copies and one usable block. + pub fn new(lba_bytes: u32, lba_count: u64) -> Option { + let lba = LbaSize::of(lba_bytes)?; + let array_lbas = ARRAY_BYTES_U64.checked_div(u64::from(lba_bytes))?; + let table = array_lbas.checked_add(1)?.checked_mul(2)?.checked_add(1)?; + (lba_count > table).then_some(Self { lba, lba_count, array_lbas }) + } + + pub const fn lba_bytes(&self) -> u32 { + self.lba.bytes() + } + + pub const fn lba_count(&self) -> u64 { + self.lba_count + } + + /// Where the backup header is: the device's last LBA. + pub const fn backup_header_lba(&self) -> u64 { + // `new` proved `lba_count` past the whole table. + self.lba_count.saturating_sub(1) + } + + pub const fn backup_array_lba(&self) -> u64 { + self.backup_header_lba().saturating_sub(self.array_lbas) + } + + /// The widest usable range: from just past the primary array to just + /// before the backup's. + pub const fn usable(&self) -> (u64, u64) { + (self.array_lbas.saturating_add(2), self.backup_array_lba().saturating_sub(1)) + } +} + +/// Why a table is not one this writes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Invalid { + /// The disk GUID is zero. + DiskGuid, + /// The usable range is empty or reaches into either copy. + UsableRange { first: u64, last: u64 }, + /// A used entry's range is reversed or leaves the usable range. + OutsideUsable { index: u32 }, + /// An entry of zero type with any other field set: unused entries are + /// all zero (§5.3.3). + UnusedNotZero { index: u32 }, + ZeroUnique { index: u32 }, + DuplicateUnique { first: u32, second: u32 }, + Overlap { first: u32, second: u32 }, +} + +/// A table's contents, apart from where its two copies lie. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Table { + pub disk_guid: Guid, + pub first_usable: u64, + pub last_usable: u64, + pub slots: [Slot; ENTRIES], +} + +impl Table { + /// An empty table across the whole of `geometry`. + pub fn empty(geometry: &Geometry, disk_guid: Guid) -> Self { + let (first_usable, last_usable) = geometry.usable(); + Self { disk_guid, first_usable, last_usable, slots: [Slot::UNUSED; ENTRIES] } + } + + /// Each used slot with its index. + pub fn used(&self) -> impl Iterator { + (0u32..).zip(self.slots.iter()).filter(|(_, s)| s.is_used()) + } + + /// Everything §5.3 requires of a table on `geometry`. + pub fn check(&self, geometry: &Geometry) -> Result<(), Invalid> { + if self.disk_guid.is_zero() { + return Err(Invalid::DiskGuid); + } + let (first, last) = geometry.usable(); + if self.first_usable < first || self.last_usable > last || self.first_usable > self.last_usable { + return Err(Invalid::UsableRange { first: self.first_usable, last: self.last_usable }); + } + for (index, slot) in (0u32..).zip(self.slots.iter()) { + if !slot.is_used() { + if *slot != Slot::UNUSED { + return Err(Invalid::UnusedNotZero { index }); + } + continue; + } + if slot.unique_guid.is_zero() { + return Err(Invalid::ZeroUnique { index }); + } + if slot.first > slot.last || slot.first < self.first_usable || slot.last > self.last_usable { + return Err(Invalid::OutsideUsable { index }); + } + } + for (first, a) in self.used() { + for (second, b) in self.used().filter(|(i, _)| *i > first) { + if a.unique_guid == b.unique_guid { + return Err(Invalid::DuplicateUnique { first, second }); + } + if a.first <= b.last && b.first <= a.last { + return Err(Invalid::Overlap { first, second }); + } + } + } + Ok(()) + } + + /// The bytes of both copies, once [`Self::check`] holds. + pub fn encode(&self, geometry: &Geometry) -> Result { + self.check(geometry)?; + let mut array = [0u8; ARRAY_BYTES]; + for (slot, raw) in self.slots.iter().zip(array.as_chunks_mut::().0) { + slot.encode(raw); + } + let array_crc = crate::crc32(&array); + let last = geometry.backup_header_lba(); + let primary = self.header(1, last, 2, array_crc); + let backup = self.header(last, 1, geometry.backup_array_lba(), array_crc); + Ok(Encoded { geometry: *geometry, array, primary, backup, mbr: protective_mbr(geometry) }) + } + + fn header(&self, my: u64, alternate: u64, array_lba: u64, array_crc: u32) -> Block { + let mut block: Block = [0; 4096]; + put(&mut block, 0, crate::HEADER_SIGNATURE); + put(&mut block, 8, &crate::HEADER_REVISION_1_0.to_le_bytes()); + put(&mut block, 12, &HEADER_BYTES.to_le_bytes()); + put(&mut block, 24, &my.to_le_bytes()); + put(&mut block, 32, &alternate.to_le_bytes()); + put(&mut block, 40, &self.first_usable.to_le_bytes()); + put(&mut block, 48, &self.last_usable.to_le_bytes()); + put(&mut block, 56, &self.disk_guid.0); + put(&mut block, 72, &array_lba.to_le_bytes()); + put(&mut block, 80, &ENTRIES_U32.to_le_bytes()); + put(&mut block, 84, &ENTRY_BYTES_U32.to_le_bytes()); + put(&mut block, 88, &array_crc.to_le_bytes()); + let crc = block.first_chunk::<92>().and_then(|h| header_crc(h)).unwrap_or(0); + put(&mut block, 16, &crc.to_le_bytes()); + block + } +} + +/// LBA 0 as §5.2.3 lays it for a GPT disk: one record of type 0xEE from LBA +/// 1 over the rest of the disk, saturating at what 32 bits count. +fn protective_mbr(geometry: &Geometry) -> Block { + let mut block: Block = [0; 4096]; + put(&mut block, 446, &[0x00, 0x00, 0x02, 0x00, crate::MBR_TYPE_PROTECTIVE, 0xFF, 0xFF, 0xFF]); + put(&mut block, 454, &1u32.to_le_bytes()); + let size = u32::try_from(geometry.backup_header_lba()).unwrap_or(u32::MAX); + put(&mut block, 458, &size.to_le_bytes()); + put(&mut block, 510, &[0x55, 0xAA]); + block +} + +/// `bytes` into `buf` at `at`; the offsets here are constants inside the +/// buffers they name. +fn put(buf: &mut [u8], at: usize, bytes: &[u8]) { + if let Some(dst) = buf.get_mut(at..).and_then(|rest| rest.get_mut(..bytes.len())) { + dst.copy_from_slice(bytes); + } +} + +/// Both copies of a table, encoded. +pub struct Encoded { + geometry: Geometry, + array: [u8; ARRAY_BYTES], + primary: Block, + backup: Block, + mbr: Block, +} + +/// One thing a writer does, in [`Encoded::steps`]'s order. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Step<'a> { + /// Write `bytes`, whole logical blocks, from LBA `lba`. + Write { lba: u64, bytes: &'a [u8] }, + /// Every write before this is on the medium before any after it is + /// issued: a device with a volatile cache is flushed. + Flush, +} + +impl Encoded { + /// The writes and flushes that put this table on the disk, in the order + /// they must reach it: the backup copy, a flush, then the protective MBR + /// when `mbr` asks for it and the primary copy, and a flush. + /// + /// The backup goes first because §5.3.2 requires it. The flush between is + /// what keeps a crash from tearing both copies at once — on a device that + /// persists each sector whole or not at all and has every write before a + /// completed flush on the medium: then the primary is not touched until + /// the backup is whole, and one copy is whole at every instant, the old + /// primary while the backup is written and the new backup while the + /// primary is. A device that breaks either can leave neither copy valid. + pub fn steps<'a>(&'a self, mbr: bool) -> impl Iterator> { + let len = self.geometry.lba.bytes_usize(); + let block = |b: &'a Block| -> &'a [u8] { b.get(..len).unwrap_or_default() }; + let backup_array = Step::Write { lba: self.geometry.backup_array_lba(), bytes: &self.array }; + let backup_header = Step::Write { lba: self.geometry.backup_header_lba(), bytes: block(&self.backup) }; + let protective = mbr.then_some(Step::Write { lba: 0, bytes: block(&self.mbr) }); + let primary_array = Step::Write { lba: 2, bytes: &self.array }; + let primary_header = Step::Write { lba: 1, bytes: block(&self.primary) }; + [Some(backup_array), Some(backup_header), Some(Step::Flush), protective, Some(primary_array), Some(primary_header), Some(Step::Flush)] + .into_iter() + .flatten() + } +} + +/// What LBA 0 holds. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mbr { + /// A GPT's protective MBR. + Protective, + /// An MBR's boot signature and records that are not one protective + /// record: an MBR partition table, or a hybrid one. + Other, + /// No boot signature at all. + Blank, +} + +/// Which copy of a table. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Which { + Primary, + Backup, +} + +/// Why one copy is not a table this reads. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CopyError { + Gpt(GptError), + /// A valid header naming an array of another shape than the one this + /// writes. + Shape { entries: u32, entry_bytes: u32 }, +} + +impl From for CopyError { + fn from(e: GptError) -> Self { + Self::Gpt(e) + } +} + +/// Everything a disk holds of a table: LBA 0 and each copy, read alone. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Copies { + pub geometry: Geometry, + pub mbr: Mbr, + pub primary: Result, + pub backup: Result, +} + +impl Copies { + /// The table this disk carries, by §5.3.2's rule: the primary when it is + /// valid, else the backup. `None` for a disk with no valid copy, or no + /// protective MBR. + pub fn in_force(&self) -> Option<(Which, &Table)> { + if self.mbr != Mbr::Protective { + return None; + } + match (&self.primary, &self.backup) { + (Ok(t), _) => Some((Which::Primary, t)), + (Err(_), Ok(t)) => Some((Which::Backup, t)), + (Err(_), Err(_)) => None, + } + } + + /// Both copies valid and the same table. + pub fn agree(&self) -> bool { + matches!((&self.primary, &self.backup), (Ok(p), Ok(b)) if p == b) + } +} + +/// LBA 0 and both copies of `dev`'s table, each read and checked alone. +/// `None` only for a device [`Geometry::new`] refuses or whose LBA 0 does not +/// read. +pub fn read_copies(dev: &mut dyn Sectors) -> Option { + let geometry = Geometry::new(dev.lba_bytes(), dev.lba_count())?; + let mut block: Block = [0; 4096]; + let lba0 = geometry.lba.of_block(&mut block); + read(dev, 0, lba0).ok()?; + let mbr = if check_protective_mbr(lba0).is_ok() { + Mbr::Protective + } else if lba0.get(510..512) == Some(&[0x55, 0xAA]) { + Mbr::Other + } else { + Mbr::Blank + }; + let disk = Disk { lba: geometry.lba, lba_count: geometry.lba_count, backup_header_lba: geometry.backup_header_lba() }; + let primary = read_copy(dev, &disk, 1); + let backup = read_copy(dev, &disk, geometry.backup_header_lba()); + Some(Copies { geometry, mbr, primary, backup }) +} + +fn read_copy(dev: &mut dyn Sectors, disk: &Disk, header_lba: u64) -> Result { + let mut block: Block = [0; 4096]; + let lba = disk.lba.of_block(&mut block); + read(dev, header_lba, lba)?; + let header = parse_header(lba, disk, header_lba)?; + if header.entry_count != ENTRIES_U32 || header.entry_bytes != ENTRY_BYTES_U32 { + return Err(CopyError::Shape { entries: header.entry_count, entry_bytes: header.entry_bytes }); + } + let mut array = [0u8; ARRAY_BYTES]; + for (at, chunk) in (header.entry_array_lba..header.entry_array_end).zip(array.chunks_mut(disk.lba.bytes_usize())) { + read(dev, at, chunk)?; + } + let mut crc = Crc32::new(); + crc.update(&array); + let computed = crc.finish(); + if computed != header.entry_array_crc { + return Err(GptError::EntryArrayCrc { stored: header.entry_array_crc, computed }.into()); + } + let mut slots = [Slot::UNUSED; ENTRIES]; + for (slot, raw) in slots.iter_mut().zip(array.as_chunks::().0) { + *slot = Slot::decode(raw).ok_or(GptError::EntrySize(header.entry_bytes))?; + } + Ok(Table { disk_guid: header.disk_guid, first_usable: header.first_usable_lba, last_usable: header.last_usable_lba, slots }) +} diff --git a/lib/gpt/src/guid.rs b/lib/gpt/src/guid.rs index 962f32b2265..1f40fb20faa 100644 --- a/lib/gpt/src/guid.rs +++ b/lib/gpt/src/guid.rs @@ -97,6 +97,16 @@ impl Guid { ]) } + /// A version 4 GUID out of sixteen random bytes, in the on-disk order: + /// the version is the high nibble of byte 7, the third field being + /// little-endian, and the variant the top two bits of byte 8 (RFC 9562 + /// §5.4). + pub const fn random_v4(mut bytes: [u8; 16]) -> Self { + bytes[7] = (bytes[7] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + Self(bytes) + } + pub const fn is_zero(&self) -> bool { u128::from_ne_bytes(self.0) == 0 } diff --git a/lib/gpt/src/lib.rs b/lib/gpt/src/lib.rs index 90ba80cb907..493a1004dec 100644 --- a/lib/gpt/src/lib.rs +++ b/lib/gpt/src/lib.rs @@ -17,6 +17,9 @@ //! `no_std`, no allocation, no `unsafe`: the entry array is streamed a block //! at a time through [`Sectors`], so nothing here is sized by a number the //! disk chose. +//! +//! [`edit`] is the other half: a whole table read from either copy, checked +//! and written, for the one writer of a table ToyOS has. #![no_std] #![forbid(unsafe_code)] @@ -33,6 +36,7 @@ )] mod crc32; +pub mod edit; mod guid; use core::num::NonZeroU64; @@ -72,10 +76,8 @@ const MBR_TYPE_PROTECTIVE: u8 = 0xEE; /// inside the parser: the parser must not have to know 4096-byte reads exist. pub trait Sectors { fn lba_bytes(&self) -> u32; + /// Every logical block of the device: the backup table is on the last. fn lba_count(&self) -> u64; - /// Granularity of `lba_count()` in logical blocks. A count floored by a - /// coarser reader can omit at most one less than this value. - fn lba_count_granularity(&self) -> NonZeroU64; /// Fill `buf` — exactly `lba_bytes()` long — with logical block `lba`. /// `false` means the read did not happen and its contents are unknown. fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool; @@ -373,7 +375,6 @@ struct Disk { lba_count: u64, /// `lba_count - 1`, where the backup header is: at least 2. backup_header_lba: u64, - lba_count_slack: u64, } /// A logical block size this crate parses, so one block is a fixed prefix of @@ -385,7 +386,7 @@ struct Disk { /// driver's own accepted range, which is not a coincidence: above 4096 the /// block no longer divides the kernel's 4 KiB block, and below 512 the GPT /// header does not fit in one. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] enum LbaSize { B512, B1024, @@ -416,6 +417,15 @@ impl LbaSize { } } + const fn bytes_usize(self) -> usize { + match self { + Self::B512 => 512, + Self::B1024 => 1024, + Self::B2048 => 2048, + Self::B4096 => 4096, + } + } + fn of_block(self, block: &mut Block) -> &mut [u8] { match self { Self::B512 => &mut block[..512], @@ -437,7 +447,6 @@ fn open_disk(dev: &mut dyn Sectors) -> Result { Some(last) if last >= 2 => last, _ => return Err(GptError::DeviceTooSmall(lba_count)), }; - let lba_count_slack = dev.lba_count_granularity().get().saturating_sub(1); let mut block: Block = [0; 4096]; let block = lba.of_block(&mut block); @@ -445,7 +454,7 @@ fn open_disk(dev: &mut dyn Sectors) -> Result { read(dev, 0, block)?; check_protective_mbr(block)?; - Ok(Disk { lba, lba_count, backup_header_lba, lba_count_slack }) + Ok(Disk { lba, lba_count, backup_header_lba }) } /// [`locate_type`]'s work against one header, primary or backup. @@ -576,7 +585,7 @@ impl StatedHeader { } fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result { - let Disk { lba, lba_count, backup_header_lba, lba_count_slack } = *disk; + let Disk { lba, lba_count, .. } = *disk; let lba_bytes = lba.bytes(); let Some(stated) = StatedHeader::decode(lba1) else { return Err(GptError::NoHeader); @@ -647,13 +656,8 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result= backup_array_lba { return Err(GptError::UsableRangeCoversBackup { last: last_usable_lba, backup_array_lba }); } diff --git a/lib/gpt/tests/edit.rs b/lib/gpt/tests/edit.rs new file mode 100644 index 00000000000..fdeb5c689e2 --- /dev/null +++ b/lib/gpt/tests/edit.rs @@ -0,0 +1,385 @@ +//! The writer: what it writes reads back through both readers, refuses what +//! UEFI 2.10 §5.3 forbids, reports each copy's state, and survives a crash at +//! every point of its write order. + +mod table; + +use std::collections::BTreeMap; +use std::io::{Cursor, Read, Seek, SeekFrom}; + +use table::{Image, Rng}; +use toyos_gpt::edit::{read_copies, CopyError, Geometry, Invalid, Mbr, Slot, Step, Table, Which}; +use toyos_gpt::{GptError, Guid}; + +const SEED: u64 = 0x6770_745F_6564_6974; + +fn image(lba_bytes: u32, lba_count: u64) -> Image { + Image { lba_bytes, lba_count, bytes: vec![0; (lba_bytes as u64 * lba_count) as usize], fail_at: None } +} + +/// One step as a crash model holds it: a run of whole blocks, or a flush. +#[derive(Clone, Debug)] +enum Owned { + Write { lba: u64, bytes: Vec }, + Flush, +} + +fn owned(table: &Table, geometry: &Geometry, mbr: bool) -> Vec { + let encoded = table.encode(geometry).expect("a valid table"); + encoded + .steps(mbr) + .map(|s| match s { + Step::Write { lba, bytes } => Owned::Write { lba, bytes: bytes.to_vec() }, + Step::Flush => Owned::Flush, + }) + .collect() +} + +fn copy(img: &Image) -> Image { + Image { lba_bytes: img.lba_bytes, lba_count: img.lba_count, bytes: img.bytes.clone(), fail_at: None } +} + +fn apply(img: &mut Image, lba: u64, bytes: &[u8]) { + let at = (lba * u64::from(img.lba_bytes)) as usize; + img.bytes[at..at + bytes.len()].copy_from_slice(bytes); +} + +fn write(img: &mut Image, steps: &[Owned]) { + for s in steps { + if let Owned::Write { lba, bytes } = s { + apply(img, *lba, bytes); + } + } +} + +fn slot(rng: &mut Rng, kind: Guid, first: u64, last: u64, name: &str) -> Slot { + Slot { + type_guid: kind, + unique_guid: rng.guid(), + first, + last, + attributes: rng.next() & (1 << 63 | 1), + name: Slot::named(name).expect("a short name"), + } +} + +/// A table of a few partitions spread over `geometry`, at random indices. +fn some_table(rng: &mut Rng, geometry: &Geometry) -> Table { + let mut t = Table::empty(geometry, rng.guid()); + let (first, last) = geometry.usable(); + let span = (last - first + 1) / 8; + let kinds = [Guid::EFI_SYSTEM, Guid::TOYOS_DATA, Guid::TOYOS_ROOT, Guid::MICROSOFT_BASIC]; + for i in 0..rng.range(1, 6) { + let lo = first + i * span; + let index = loop { + let at = rng.below(128) as usize; + if !t.slots[at].is_used() { + break at; + } + }; + let (kind, len) = (rng.pick(&kinds), rng.below(span)); + t.slots[index] = slot(rng, kind, lo, lo + len, &format!("part {i}")); + } + t +} + +/// Each partition by index: its type, unique GUID, first and last block. +type Listing = BTreeMap; + +/// What `toyos_gpt::list`, the reader every disk driver serves from, finds. +fn listed(img: &mut Image) -> Result { + let mut out = [None; 128]; + toyos_gpt::list(img, &mut out)?; + Ok(out + .iter() + .flatten() + .map(|e| { + let p = e.expect("placed"); + (p.index(), (p.type_guid(), p.unique_guid(), p.first_lba(), p.last_lba())) + }) + .collect()) +} + +fn listing_of(t: &Table) -> Listing { + t.used().map(|(i, s)| (i, (s.type_guid, s.unique_guid, s.first, s.last))).collect() +} + +/// Every block size the crate parses, on devices of whole 4 KiB blocks and +/// of not whole: both copies read back as the table, and `list` finds it. +#[test] +fn a_written_table_reads_back_from_both_copies() { + let mut rng = Rng::new(SEED); + for (lba_bytes, lba_count) in [(512, 262_144), (512, 7_507_812 / 32), (1024, 70_001), (2048, 40_000), (4096, 9_000)] { + let geometry = Geometry::new(lba_bytes, lba_count).expect("room"); + for _ in 0..20 { + let t = some_table(&mut rng, &geometry); + let mut img = image(lba_bytes, lba_count); + write(&mut img, &owned(&t, &geometry, true)); + let copies = read_copies(&mut img).expect("a geometry"); + assert_eq!(copies.mbr, Mbr::Protective); + assert_eq!(copies.primary.as_ref(), Ok(&t), "{lba_bytes}x{lba_count}"); + assert!(copies.agree()); + assert_eq!(listed(&mut img), Ok(listing_of(&t))); + let backup = geometry.backup_header_lba() as usize * lba_bytes as usize; + assert_eq!(&img.bytes[backup..backup + 8], b"EFI PART", "the backup header is on the last LBA"); + } + } +} + +/// The ROWS a boot image lays, on 1 MiB boundaries, one of them a length the +/// alignment does not round. +const ROWS: [(&str, Guid, u64); 6] = [ + ("ToyOS data", Guid::TOYOS_DATA, 3 << 20), + ("ESP", Guid::EFI_SYSTEM, 34 << 20), + ("ToyOS slots", Guid::TOYOS_SLOTS, 1 << 20), + ("ToyOS log", Guid::MICROSOFT_BASIC, 34 << 20), + ("ToyOS slot A", Guid::TOYOS_BOOT, (40 << 20) + 4096), + ("ToyOS root A", Guid::TOYOS_ROOT, 7 << 20), +]; + +/// **The differential oracle**: the `gpt` crate, a third party's writer +/// sharing no code with this one, lays the same partitions on the same disk +/// under the same GUIDs into the same bytes, on a disk the alignment rounds +/// and on one it does not. +#[test] +fn the_gpt_crate_writes_the_same_table() { + const LBA: u64 = 512; + for total in [128u64 << 20, (160 << 20) + 3 * 4096] { + let mut theirs = vec![0u8; total as usize]; + let mut cursor = Cursor::new(&mut theirs); + gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(total / LBA - 1).unwrap_or(u32::MAX)) + .overwrite_lba0(&mut cursor) + .expect("the protective MBR"); + let mut disk = gpt::GptConfig::default() + .initialized(false) + .writable(true) + .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) + .create_from_device(Box::new(cursor), None) + .expect("a table"); + disk.update_partitions(BTreeMap::new()).expect("an empty table"); + for (name, kind, len) in ROWS { + let kind = gpt::partition_types::Type { guid: kind.to_string().leak(), os: gpt::partition_types::OperatingSystem::None }; + disk.add_partition(name, len, kind, 0, Some(2048)).expect("a partition"); + } + let mut device = disk.write().expect("the table"); + let mut theirs = vec![0u8; total as usize]; + device.seek(SeekFrom::Start(0)).and_then(|_| device.read_exact(&mut theirs)).expect("read back"); + let guid = |at: usize| Guid(theirs[at..at + 16].try_into().unwrap()); + + let geometry = Geometry::new(LBA as u32, total / LBA).expect("room"); + let mut t = Table::empty(&geometry, guid(LBA as usize + 56)); + let mut at = 2048; + for (i, (name, kind, len)) in ROWS.iter().enumerate() { + let first = at; + at = (first + len / LBA).next_multiple_of(2048); + t.slots[i] = Slot { + type_guid: *kind, + unique_guid: guid(2 * LBA as usize + i * 128 + 16), + first, + last: first + len / LBA - 1, + attributes: 0, + name: Slot::named(name).unwrap(), + }; + } + let mut ours = image(LBA as u32, total / LBA); + write(&mut ours, &owned(&t, &geometry, true)); + let differing = (0..total as usize).filter(|&i| ours.bytes[i] != theirs[i]).count(); + assert_eq!(differing, 0, "{total}: {differing} bytes differ from the gpt crate's"); + } +} + +/// Each thing §5.3 forbids is refused by name, before a byte is encoded. +#[test] +fn a_table_uefi_forbids_is_refused() { + let mut rng = Rng::new(SEED ^ 1); + let g = Geometry::new(512, 100_000).unwrap(); + let (first, last) = g.usable(); + assert_eq!((first, last), (34, 100_000 - 34), "§5.3.1's minimum at 512-byte blocks"); + assert_eq!(Geometry::new(4096, 100).unwrap().usable(), (6, 100 - 6), "and at 4096"); + let base = |rng: &mut Rng| { + let mut t = Table::empty(&g, rng.guid()); + t.slots[0] = slot(rng, Guid::TOYOS_DATA, 2048, 4095, "a"); + t.slots[5] = slot(rng, Guid::TOYOS_ROOT, 4096, 8191, "b"); + t + }; + assert_eq!(base(&mut rng).check(&g), Ok(())); + type Case = (&'static str, fn(&mut Table), Invalid); + let cases: [Case; 8] = [ + ("zero disk", |t| t.disk_guid = Guid::ZERO, Invalid::DiskGuid), + ("usable over the primary", |t| t.first_usable = 33, Invalid::UsableRange { first: 33, last: 100_000 - 34 }), + ("usable over the backup", |t| t.last_usable += 1, Invalid::UsableRange { first: 34, last: 100_000 - 33 }), + ("reversed", |t| t.slots[5].last = 4000, Invalid::OutsideUsable { index: 5 }), + ("past usable", |t| t.slots[5].last = 100_000 - 33, Invalid::OutsideUsable { index: 5 }), + ("overlap", |t| t.slots[5].first = 4095, Invalid::Overlap { first: 0, second: 5 }), + ("zero unique", |t| t.slots[0].unique_guid = Guid::ZERO, Invalid::ZeroUnique { index: 0 }), + ("unused not zero", |t| t.slots[9].first = 1, Invalid::UnusedNotZero { index: 9 }), + ]; + for (what, bend, want) in cases { + let mut t = base(&mut rng); + bend(&mut t); + assert_eq!(t.check(&g), Err(want), "{what}"); + assert!(t.encode(&g).is_err(), "{what} encoded"); + } + let mut t = base(&mut rng); + t.slots[5].unique_guid = t.slots[0].unique_guid; + assert_eq!(t.check(&g), Err(Invalid::DuplicateUnique { first: 0, second: 5 })); + assert_eq!(Slot::named(&"x".repeat(36)).map(|n| n[35]), Some(u16::from(b'x'))); + assert_eq!(Slot::named(&"x".repeat(37)), None, "37 units"); +} + +/// What each copy holds is reported alone: a torn primary leaves the backup +/// in force, a torn backup leaves the primary in force and the two not in +/// agreement, two valid copies that differ do not agree, an MBR disk and a +/// blank one are named, and an array of another shape is not read as one. +#[test] +fn each_copy_is_read_and_reported_alone() { + let mut rng = Rng::new(SEED ^ 2); + let g = Geometry::new(512, 50_000).unwrap(); + let t = some_table(&mut rng, &g); + let mut whole = image(512, 50_000); + write(&mut whole, &owned(&t, &g, true)); + let header = |which: Which| match which { + Which::Primary => 512, + Which::Backup => g.backup_header_lba() as usize * 512, + }; + + for which in [Which::Primary, Which::Backup] { + let mut torn = copy(&whole); + torn.bytes[header(which) + 40] ^= 1; + let copies = read_copies(&mut torn).unwrap(); + let other = if which == Which::Primary { Which::Backup } else { Which::Primary }; + assert_eq!(copies.in_force().map(|(w, t)| (w, t.clone())), Some((other, t.clone())), "{which:?} torn"); + assert!(!copies.agree()); + let bad = if which == Which::Primary { &copies.primary } else { &copies.backup }; + assert!(matches!(bad, Err(CopyError::Gpt(GptError::HeaderCrc { .. }))), "{bad:?}"); + } + + // A second valid table in the backup's place alone. + let mut changed = t; + changed.slots.iter_mut().find(|s| s.is_used()).unwrap().name = Slot::named("renamed").unwrap(); + let mut differing = copy(&whole); + let steps = owned(&changed, &g, false); + write(&mut differing, &steps[..2]); + let copies = read_copies(&mut differing).unwrap(); + assert_eq!(copies.in_force().map(|(w, _)| w), Some(Which::Primary)); + assert!(copies.primary.is_ok() && copies.backup.is_ok() && !copies.agree()); + + let mut mbr = image(512, 50_000); + mbr.bytes[446 + 4] = 0x83; + mbr.bytes[510..512].copy_from_slice(&[0x55, 0xAA]); + let copies = read_copies(&mut mbr).unwrap(); + assert_eq!((copies.mbr, copies.in_force()), (Mbr::Other, None)); + assert_eq!(read_copies(&mut image(512, 50_000)).unwrap().mbr, Mbr::Blank); + assert!(read_copies(&mut image(512, 66)).is_none(), "no room for both copies and a block"); + + // The same table with 256-byte entries, resealed: valid, and not this shape. + let mut wide = copy(&whole); + let h = 512; + wide.bytes[h + 80..h + 84].copy_from_slice(&64u32.to_le_bytes()); + wide.bytes[h + 84..h + 88].copy_from_slice(&256u32.to_le_bytes()); + wide.bytes[h + 16..h + 20].fill(0); + let crc = toyos_gpt::crc32(&wide.bytes[h..h + 92]); + wide.bytes[h + 16..h + 20].copy_from_slice(&crc.to_le_bytes()); + let copies = read_copies(&mut wide).unwrap(); + assert_eq!(copies.primary, Err(CopyError::Shape { entries: 64, entry_bytes: 256 })); +} + +/// What a crash may leave of one write: nothing, all of it, or any subset of +/// its blocks. +#[derive(Clone, Copy, Debug)] +enum Landed { + None, + Whole, + Torn(u64), +} + +/// Show `each` every image a crash during `steps` over `old` may leave, and +/// answer how many. A flush is a barrier: everything before it is on the +/// medium. Between two flushes each write landed whole, not at all, or torn +/// into an arbitrary subset of its blocks; `torn` draws that many subsets +/// per write. +fn crashes(old: &Image, steps: &[Owned], rng: &mut Rng, torn: usize, each: &mut dyn FnMut(&str, &mut Image)) -> usize { + let epochs: Vec<&[Owned]> = steps.split(|s| matches!(s, Owned::Flush)).collect(); + let mut seen = 0; + let mut base = copy(old); + for (e, epoch) in epochs.iter().enumerate() { + let writes: Vec<(u64, &Vec)> = + epoch.iter().filter_map(|s| if let Owned::Write { lba, bytes } = s { Some((*lba, bytes)) } else { None }).collect(); + let choices: Vec> = writes + .iter() + .map(|_| { + let mut c = vec![Landed::None, Landed::Whole]; + c.extend((0..torn).map(|_| Landed::Torn(rng.next()))); + c + }) + .collect(); + let mut pick = vec![0usize; writes.len()]; + loop { + let mut img = copy(&base); + for ((lba, bytes), (c, k)) in writes.iter().zip(choices.iter().zip(&pick)) { + let blocks = bytes.chunks(old.lba_bytes as usize).enumerate(); + for (i, b) in blocks { + let lands = match c[*k] { + Landed::None => false, + Landed::Whole => true, + Landed::Torn(mask) => mask.rotate_left(i as u32) & 1 == 1, + }; + if lands { + apply(&mut img, lba + i as u64, b); + } + } + } + let at = format!("epoch {e} {:?}", pick.iter().zip(&choices).map(|(k, c)| c[*k]).collect::>()); + each(&at, &mut img); + seen += 1; + let Some(next) = (0..pick.len()).find(|&i| pick[i] + 1 < choices[i].len()) else { break }; + pick[next] += 1; + pick[..next].fill(0); + } + for (lba, bytes) in writes { + apply(&mut base, lba, bytes); + } + } + seen +} + +/// Whether a crash left an image read as wholly the old table or wholly the +/// new one by the reader drivers serve from, and §5.3.2's order held: the +/// primary is never newer than the backup. +fn judge(img: &mut Image, old: &Table, new: &Table) -> Result<(), String> { + let seen = listed(img).map_err(|e| format!("no table: {e:?}"))?; + if seen != listing_of(old) && seen != listing_of(new) { + return Err(format!("neither table: {seen:?}")); + } + let copies = read_copies(img).ok_or("no geometry")?; + if copies.primary.as_ref() == Ok(new) && copies.backup.as_ref() != Ok(new) { + return Err("the primary is new and the backup is not".into()); + } + Ok(()) +} + +/// The write order's whole claim: a crash anywhere — between any two writes, +/// or inside one, any subset of its blocks landed — leaves the disk reading +/// as the old table or the new one, never neither and never a mixture, and +/// the primary never newer than the backup. +#[test] +fn a_crash_anywhere_in_the_write_reads_as_the_old_table_or_the_new() { + let mut rng = Rng::new(SEED ^ 3); + for (lba_bytes, lba_count) in [(512u32, 40_000u64), (4096, 5_000)] { + let g = Geometry::new(lba_bytes, lba_count).unwrap(); + for _ in 0..4 { + let old = some_table(&mut rng, &g); + let mut new = some_table(&mut rng, &g); + new.disk_guid = old.disk_guid; + let mut disk = image(lba_bytes, lba_count); + write(&mut disk, &owned(&old, &g, true)); + let steps = owned(&new, &g, false); + let states = crashes(&disk, &steps, &mut rng, 6, &mut |at, img| { + if let Err(why) = judge(img, &old, &new) { + panic!("{lba_bytes}x{lba_count}, a crash at {at}: {why}"); + } + }); + assert!(states > 100, "{states} crash states"); + } + } +} diff --git a/lib/gpt/tests/mutate.rs b/lib/gpt/tests/mutate.rs index e8f5baf106a..63b0a810f61 100644 --- a/lib/gpt/tests/mutate.rs +++ b/lib/gpt/tests/mutate.rs @@ -17,7 +17,6 @@ const SHAPE: Shape<'static> = Shape { types: &[Guid::EFI_SYSTEM, Guid::MICROSOFT_BASIC, Guid::TOYOS_DATA, Guid::TOYOS_ROOT], entry_sizes: &[128, 128, 128, 128, 256, 512], backup: true, - floored: true, }; /// A partition as the parser handed it back. diff --git a/lib/gpt/tests/oracle.rs b/lib/gpt/tests/oracle.rs index 26c56f9afcd..99385f44620 100644 --- a/lib/gpt/tests/oracle.rs +++ b/lib/gpt/tests/oracle.rs @@ -18,7 +18,6 @@ const SHAPE: Shape<'static> = Shape { // `gpt` reads 128-byte entries whatever the header says. entry_sizes: &[128], backup: false, - floored: false, }; const VALID: u64 = 2_000; diff --git a/lib/gpt/tests/parse.rs b/lib/gpt/tests/parse.rs index 9fe8565964a..05756208562 100644 --- a/lib/gpt/tests/parse.rs +++ b/lib/gpt/tests/parse.rs @@ -64,7 +64,6 @@ fn disk(edit: impl FnOnce(&mut Layout)) -> Image { }, backup: None, reported_lba_count: DISK_LBAS, - granularity: 1, mbr_type: 0xEE, mbr_signature: [0x55, 0xAA], }; @@ -513,22 +512,6 @@ fn a_usable_range_reaching_the_backup_gpt_is_refused() { ); } -/// The kernel's 4 KiB view floors a 512-byte disk's `lba_count` by up to 7 -/// LBAs while an honest table is laid out against the true end — a 2055-LBA -/// disk (2055 % 8 = 7) seen as 2048, its last_usable 2021 at the conceded -/// bound's edge, must parse. The unconceded bound refused every such disk. -#[test] -fn an_honest_table_on_a_floored_device_view_parses() { - let mut img = disk(|l| { - l.lba_count = 2055; - l.primary.last_usable = 2055 - 34; - mirrored(l); - }); - (img.lba_count, img.granularity) = (2048, 8); - let found = img.locate(guid(0xC3)).expect("an honest disk lost /boot"); - assert_eq!(found.partition().index(), 2); -} - /// UEFI gives every entry a `UniquePartitionGUID` that must be unique. Two /// entries claiming the searched-for GUID must refuse, never resolve /// first-wins — either one could be the partition the firmware meant. diff --git a/lib/gpt/tests/table/mod.rs b/lib/gpt/tests/table/mod.rs index 252caedf1af..92ee65ffb7c 100644 --- a/lib/gpt/tests/table/mod.rs +++ b/lib/gpt/tests/table/mod.rs @@ -98,9 +98,8 @@ pub struct Layout { pub lba_count: u64, pub primary: Table, pub backup: Option, - /// What the device answers for its block count, and the granularity it floors by. + /// What the device answers for its block count. pub reported_lba_count: u64, - pub granularity: u64, pub mbr_type: u8, pub mbr_signature: [u8; 2], } @@ -112,8 +111,6 @@ pub struct Shape<'a> { pub entry_sizes: &'a [u32], /// Some layouts carry no backup copy. pub backup: bool, - /// Some devices floor their block count, as the kernel's 4 KiB reader does. - pub floored: bool, } /// A table UEFI 2.11 §5.3 accepts, with up to eight disjoint partitions at random indices. @@ -164,14 +161,12 @@ pub fn valid(rng: &mut Rng, shape: &Shape<'_>) -> Layout { entries, }; let backup = (shape.backup && !rng.one_in(3)).then(|| primary.mirror(lba_bytes, lba_count)); - let granularity = if shape.floored && lba_bytes == 512 && rng.one_in(4) { 8 } else { 1 }; Layout { lba_bytes, lba_count, primary, backup, - reported_lba_count: lba_count / granularity * granularity, - granularity, + reported_lba_count: lba_count, mbr_type: 0xEE, mbr_signature: [0x55, 0xAA], } @@ -334,7 +329,6 @@ pub fn image(layout: &Layout) -> Image { Image { lba_bytes: layout.lba_bytes, lba_count: layout.reported_lba_count, - granularity: layout.granularity, bytes: disk, fail_at: None, } @@ -343,7 +337,6 @@ pub fn image(layout: &Layout) -> Image { pub struct Image { pub lba_bytes: u32, pub lba_count: u64, - pub granularity: u64, pub bytes: Vec, /// The one block whose read does not happen. pub fail_at: Option, @@ -356,9 +349,6 @@ impl Sectors for Image { fn lba_count(&self) -> u64 { self.lba_count } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(self.granularity).expect("1 or 8") - } fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { if self.fail_at == Some(lba) { return false; diff --git a/lib/manifest/src/authorize.rs b/lib/manifest/src/authorize.rs new file mode 100644 index 00000000000..a7968ae4be9 --- /dev/null +++ b/lib/manifest/src/authorize.rs @@ -0,0 +1,46 @@ +//! What a row may do to the machine's disks beyond its own partitions, which +//! only the signed image grants: `authorize = [...]` in its row. The +//! supervisor mints each at every start of the row, one action on one target +//! that the block service resolves each time it is used, and puts it in the +//! row's namespace under [`Authorization::name`]. The build refuses one that +//! changes a disk to a login authority or a row a login can start, and no +//! `[apps]` row can spell one. + +/// One authorization a row may declare. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Authorization { + /// Every disk's table, read: a listing and each disk's description. + Tables, + /// Designations of the machine's one DATA partition, resolved each time + /// one is asked for, for as long as the row runs. + DesignateData, +} + +impl Authorization { + pub const ALL: [Self; 2] = [Self::Tables, Self::DesignateData]; + + /// How a row spells it. + pub const fn word(self) -> &'static str { + match self { + Self::Tables => "tables", + Self::DesignateData => "designate:data", + } + } + + pub fn parse(word: &str) -> Option { + Self::ALL.into_iter().find(|a| a.word() == word) + } + + /// The name it has in the row's namespace. + pub const fn name(self) -> &'static str { + match self { + Self::Tables => "block-tables", + Self::DesignateData => "block-designate-data", + } + } + + /// Whether it changes a disk, rather than reading one. + pub const fn changes(self) -> bool { + matches!(self, Self::DesignateData) + } +} diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index d2ee73e9742..0f6d356e399 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -23,6 +23,7 @@ //! drive one process of it per PCI function the matcher names //! syscap a right on the SysCap dup the supervisor endows //! slots the idle slot's partitions and the slot table +//! authorize a disk authorization the supervisor mints at its start ([`authorize`]) //! service a system service: its `HOME` is `/state/`, not the session's //! state it keeps machine state of its own in `/state/` //! role a file server for ``: one process of it per role @@ -44,6 +45,7 @@ //! package is granted, [`consent`] how the person at the screen is asked, //! [`clipboard`] who watches the clipboard and [`display`] who sets the display. +pub mod authorize; pub mod clipboard; pub mod consent; pub mod device; @@ -316,6 +318,10 @@ pub struct Program { /// runs is never among them. One program holds it — `build/build.rs` gates /// which. pub slots: bool, + /// Disk authorizations the supervisor mints at its start, by + /// [`authorize::Authorization::word`]: the signed image's word alone, and + /// `build/build.rs` gates which rows. + pub authorize: Vec, /// A system service: its `HOME` is its own [`STATE`] directory rather /// than the session user's home, so what it keeps is machine state and no /// user's. It keeps state ([`Self::state_dir`]) whether or not @@ -486,6 +492,8 @@ pub enum RenderError { ServesWithoutService(String), /// A `roles` entry naming no file-server role. NoSuchRole { program: String, role: String }, + /// An `authorize` entry naming no [`authorize::Authorization`]. + NoSuchAuthorization { program: String, word: String }, /// A `devices` entry that names no device, or a `drives` entry that is /// no PCI matcher. NoSuchDevice { program: String, entry: String }, @@ -545,6 +553,12 @@ pub fn render(manifest: &Manifest) -> Result, RenderError> { if program.slots { out.push_str("slots\n"); } + for word in &program.authorize { + if authorize::Authorization::parse(word).is_none() { + return Err(RenderError::NoSuchAuthorization { program: program.name.clone(), word: word.clone() }); + } + out.push_str(&format!("authorize {word}\n")); + } if program.service { out.push_str("service\n"); } @@ -726,6 +740,7 @@ pub fn parse(text: &str) -> Manifest { "drive" => program.drives.push(rest.to_string()), "syscap" => program.syscap.push(rest.to_string()), "slots" if rest.is_empty() => program.slots = true, + "authorize" if authorize::Authorization::parse(rest).is_some() => program.authorize.push(rest.to_string()), "service" => program.service = true, "state" if rest.is_empty() => program.state = true, "role" => program.roles.push(rest.to_string()), @@ -773,6 +788,7 @@ mod tests { source: Source::Path("/system/bin/update".into()), slots: true, state: true, + authorize: vec!["tables".into(), "designate:data".into()], ..Program::default() }, Program { diff --git a/loader/src/rootimage.rs b/loader/src/rootimage.rs index 0d3faaaae1e..d39ba28e6e6 100644 --- a/loader/src/rootimage.rs +++ b/loader/src/rootimage.rs @@ -16,7 +16,6 @@ use alloc::alloc::Layout; use alloc::string::String; -use core::num::NonZeroU64; use crate::payload::Payload; @@ -236,10 +235,6 @@ impl Sectors for Disk<'_> { self.lba_count } - fn lba_count_granularity(&self) -> NonZeroU64 { - NonZeroU64::MIN - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { let n = self.lba_bytes as usize; if self.io.read_blocks(self.media_id, lba, &mut self.scratch[..n]).is_err() { diff --git a/system/disks/Cargo.toml b/system/disks/Cargo.toml new file mode 100644 index 00000000000..97529e1cd65 --- /dev/null +++ b/system/disks/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "disks" +description = "disks [list | designate data [--commit]]: the machine's disks and their tables, and the designation of its DATA partition, through the block service under the authorizations its row is minted." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos = { path = "../../sdk/toyos" } +toyos-blockring = { path = "../../lib/blockring" } +toyos-gpt = { path = "../../lib/gpt" } +toyos-manifest = { path = "../../lib/manifest" } +diskserver = { path = "../diskserver" } + +[package.metadata.toyos.host] +exempt.manages = "ToyOS's disks, through its block service's tables and designation badges" diff --git a/system/disks/src/main.rs b/system/disks/src/main.rs new file mode 100644 index 00000000000..37cbea3f25a --- /dev/null +++ b/system/disks/src/main.rs @@ -0,0 +1,210 @@ +//! `/system/bin/disks`: the machine's disks, through the block service. +//! +//! - `disks` or `disks list`: every disk the block service serves, its size, +//! each copy of its table, and every partition of the copy in force. +//! - `disks designate data`: judge the machine's one DATA partition for +//! designation and write nothing; `--commit` writes the stamp, which the +//! DATA file server formats at its next start. +//! +//! **It holds no authority of its own.** Each command needs the +//! authorization its row is minted under its name +//! (`toyos_manifest::authorize`): `tables` to list, `designate:data` to +//! designate. A run without one is refused, whoever runs it and from where; +//! a login grants none. Exit status 0 where it was done, 1 where it was +//! refused or failed, 2 for a command line it does not take. + +use toyos_blockring::wire::{Designated, DiskHead, SlotLine}; +use toyos_gpt::Guid; +use toyos_manifest::authorize::Authorization; + +const USAGE: &str = "usage: disks [list | designate data [--commit]]"; + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let args: Vec<&str> = args.iter().map(String::as_str).collect(); + let code = match args[..] { + [] | ["list"] => list(), + ["designate", "data"] => designate(false), + ["designate", "data", "--commit"] => designate(true), + _ => { + eprintln!("{USAGE}"); + 2 + } + }; + std::process::exit(code); +} + +/// The refusal of a run whose row was not minted `authorization`. +fn unauthorized(authorization: Authorization) -> i32 { + refuse(&format!( + "disks: this run holds no `{}` authorization, which only the signed image grants a row; nothing was done", + authorization.word() + )) +} + +fn list() -> i32 { + let Some(names) = toyos::endow::namespace() else { return unauthorized(Authorization::Tables) }; + let service = Authorization::Tables.name(); + let listed = match diskserver::list(names, service) { + Ok(listed) => listed, + Err(diskserver::Error::NotHeld) => return unauthorized(Authorization::Tables), + Err(why) => return refuse(&format!("disks: the block service would not list its partitions: {why:?}")), + }; + let mut disks: Vec = listed.iter().map(|l| l.disk).collect(); + disks.sort_unstable(); + disks.dedup(); + if disks.is_empty() { + println!("no disk is served"); + } + let mut code = 0; + for disk in disks { + match diskserver::tables::describe(names, service, disk) { + Ok((head, slots)) => print!("{}", describe(disk, &head, &slots)), + Err(why) => { + println!("disk {disk}: not described: {why:?}"); + code = 1; + } + } + } + code +} + +fn designate(commit: bool) -> i32 { + let Some(names) = toyos::endow::namespace() else { return unauthorized(Authorization::DesignateData) }; + match diskserver::tables::designate(names, Authorization::DesignateData.name(), commit) { + Err(diskserver::Error::NotHeld) => unauthorized(Authorization::DesignateData), + Ok(done) => { + println!("{}", designated(&done)); + i32::from(!matches!(done, Designated::Written | Designated::WouldWrite | Designated::Already)) + } + Err(why) => refuse(&format!("disks: the DATA partition was not designated: {why:?}")), + } +} + +fn refuse(why: &str) -> i32 { + eprintln!("{why}"); + 1 +} + +/// What a designation came to, as a line. +fn designated(done: &Designated) -> String { + match done { + Designated::Written => "the DATA partition is designated: the stamp is on the disk and read back; DATA is formatted at its next start".into(), + Designated::WouldWrite => "the DATA partition holds nothing recognised and would be designated; nothing was written (--commit writes)".into(), + Designated::Already => "the DATA partition is designated already; nothing was written".into(), + Designated::Volume => "the DATA partition holds a ToyOS volume; nothing was written".into(), + Designated::Foreign(what) => format!("the DATA partition carries {what}; nothing was written"), + } +} + +/// The name a partition type has here, or its GUID. +fn kind(guid: [u8; 16]) -> String { + let named = [ + (Guid::EFI_SYSTEM, "EFI system"), + (Guid::MICROSOFT_BASIC, "basic data"), + (Guid::TOYOS_DATA, "ToyOS DATA"), + (Guid::TOYOS_ROOT, "ToyOS ROOT"), + (Guid::TOYOS_BOOT, "ToyOS slot"), + (Guid::TOYOS_SLOTS, "ToyOS slot table"), + (Guid::TOYOS_SCRATCH, "ToyOS scratch"), + ]; + named.iter().find(|(g, _)| g.0 == guid).map_or_else(|| Guid(guid).to_string(), |(_, name)| (*name).to_string()) +} + +/// `bytes` in the largest binary unit that leaves at least 10 of it. +fn size(bytes: u64) -> String { + let units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let mut at = 0; + let mut v = bytes; + while v >= 1024 * 10 && at + 1 < units.len() { + v /= 1024; + at += 1; + } + format!("{v} {}", units[at]) +} + +/// One disk as `disks list` prints it. +fn describe(disk: u32, head: &DiskHead, slots: &[SlotLine]) -> String { + let bytes = head.sectors * u64::from(head.lba_bytes); + let copies = if head.agree { + "both copies agree".to_string() + } else { + format!("primary {}, backup {}", head.primary, head.backup) + }; + let mut out = format!( + "disk {disk}: {} ({} sectors of {} bytes), a GPT, {copies}\n", + size(bytes), + head.sectors, + head.lba_bytes + ); + for s in slots { + let name = String::from_utf16_lossy(s.name.split(|u| *u == 0).next().unwrap_or_default()); + let sectors = s.last.saturating_sub(s.first).saturating_add(1); + let state = match (s.served, s.held) { + (true, true) => "in use", + (true, false) => "free", + (false, _) => "not served", + }; + out.push_str(&format!( + " {:>3} {:<16} {:>10} {} {:?} {state}\n", + s.index + 1, + kind(s.kind), + size(sectors * u64::from(head.lba_bytes)), + Guid(s.unique), + name, + )); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A disk prints its size, its table's state and each partition by its + /// one-based number, type, size, unique GUID, name and use; copies that + /// disagree say what each is. + #[test] + fn a_disk_is_described_one_line_a_partition() { + let mut name = [0u16; 36]; + for (u, c) in name.iter_mut().zip("ToyOS data".encode_utf16()) { + *u = c; + } + let head = DiskHead { + lba_bytes: 512, + sectors: 500_118_192, + agree: true, + primary: "valid".into(), + backup: "valid".into(), + }; + let data = SlotLine { + index: 3, + kind: Guid::TOYOS_DATA.0, + unique: [0x11; 16], + first: 2048, + last: 2048 + (170u64 << 21) - 1, + attributes: 0, + name, + held: false, + served: true, + }; + let text = describe(1, &head, &[data]); + let lines: Vec<&str> = text.lines().collect(); + assert_eq!(lines[0], "disk 1: 238 GiB (500118192 sectors of 512 bytes), a GPT, both copies agree"); + assert_eq!( + lines[1].split_whitespace().collect::>(), + ["4", "ToyOS", "DATA", "170", "GiB", "11111111-1111-1111-1111-111111111111", "\"ToyOS", "data\"", "free"] + ); + assert_eq!(lines.len(), 2); + let torn = DiskHead { agree: false, backup: "Gpt(HeaderCrc { stored: 1, computed: 2 })".into(), ..head }; + assert!(describe(0, &torn, &[]).contains("primary valid, backup Gpt(HeaderCrc"), "{}", describe(0, &torn, &[])); + assert_eq!(size(4096), "4096 B"); + assert_eq!(size(10 << 30), "10 GiB"); + } + + #[test] + fn a_designation_is_said_as_what_it_came_to() { + assert!(designated(&Designated::Written).contains("read back")); + assert!(designated(&Designated::Foreign("an NTFS file system".into())).contains("carries an NTFS file system")); + } +} diff --git a/system/diskserver/Cargo.toml b/system/diskserver/Cargo.toml index 1bcd64f4b7e..8e7d38a1aa5 100644 --- a/system/diskserver/Cargo.toml +++ b/system/diskserver/Cargo.toml @@ -23,6 +23,8 @@ toyos-blockring = { path = "../../lib/blockring" } toyos-blockserve = { path = "../../lib/blockserve" } # The device a served partition is. toyos-blockdev = { path = "../../lib/blockdev" } +# The `--active` partitions, named as GUID text. +toyos-gpt = { path = "../../lib/gpt" } [package.metadata.toyos.host] exempt.owns = "the block service between ToyOS disk drivers and their clients" diff --git a/system/diskserver/src/lib.rs b/system/diskserver/src/lib.rs index 99d18360bba..de343129040 100644 --- a/system/diskserver/src/lib.rs +++ b/system/diskserver/src/lib.rs @@ -1,9 +1,11 @@ //! What a program that opens a partition through the block service links: //! the session itself ([`session`]), and the partition as a block device over -//! it ([`disk`]). The service is `src/main.rs` beside this. +//! it ([`disk`]); and a disk's description and a designation ([`tables`]). +//! The service is `src/main.rs` beside this. pub mod disk; pub mod session; +pub mod tables; pub use session::{list, Answer, Error, Session, Unsent, Waited}; pub use toyos_blockring::client::{Outcome, Ticket}; diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index fbe3b654769..7500dad4302 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -9,13 +9,16 @@ //! region it sent — and the driver answers it and serves the session, so a //! block never passes through here. //! -//! **A connection reaches what its grant says** ([`wire::Grant`]): the badge -//! the supervisor minted its connector with, which the kernel stamps on it and -//! answers this acceptor alone. A listing names the partitions the grant -//! admits, an open of any other is refused `NotGranted` whether or not a disk -//! carries it, a connection with no grant reaches nothing, and a session whose -//! grant does not write is handed over as one, whose driver answers every -//! write `ReadOnly`. +//! **A connection reaches what its badge says** ([`wire::Badge`]), minted by +//! the supervisor, which the kernel stamps on it and answers this acceptor +//! alone. A partitions grant lists and opens what it admits, and an open of +//! any other is refused `NotGranted` whether or not a disk carries it; a +//! session whose grant does not write is handed over as one, whose driver +//! answers every write `ReadOnly`. A tables badge lists every partition and +//! has a disk described by its driver, and opens nothing. A designation badge +//! does one thing: the machine's one DATA partition, resolved when it is +//! asked, is designated by the driver of its disk, told whether the running +//! system is on it (`--active`). A connection with no badge reaches nothing. //! //! **A driver is who the supervisor says it is**: the badge it minted the //! driver's `disk` connector with. It starts the disk drivers before this @@ -44,7 +47,7 @@ use toyos::poller::{Poller, READABLE}; use toyos::{AsHandle, OwnedHandle}; use toyos_abi::syscall::{SyscallError, MAX_BADGE}; use toyos_blockring::disk::{self, Offer, Part, Settled}; -use toyos_blockring::wire::{self, Grant, Listed, Refusal}; +use toyos_blockring::wire::{self, Badge, Grant, Listed, Refusal}; use toyos_blockring::PORT; use toyos_blockserve::guid_text; @@ -67,6 +70,11 @@ const MAX_DRIVERS: usize = 16; /// on. const DRIVER: &str = "--driver"; const UNDRIVEN: &str = "--undriven"; +/// Argv: a partition the running system is on, by unique GUID, which no +/// designation changes; or that which they are is unknown, and every +/// partition is taken for one. +const ACTIVE: &str = "--active"; +const ACTIVE_UNKNOWN: &str = "--active-unknown"; const TOKEN_ACCEPT: u64 = 0; const TOKEN_DRIVERS: u64 = 1; @@ -92,16 +100,19 @@ enum Fate { /// A disk driver as the supervisor named it: the badge it minted the /// driver's `disk` connector with, which the kernel stamps on the connection /// and the driver cannot choose. What a driver calls itself is for the lines. -type Badge = u32; +type DriverBadge = u32; /// Everything the drivers said, and what the supervisor said of them. #[derive(Default)] struct Known { /// The disk drivers the supervisor started before this service. - expected: Vec, + expected: Vec, undriven: Vec, - fates: BTreeMap, + fates: BTreeMap, offered: Vec, + /// The partitions the running system is on, or `None` where the + /// supervisor could not say. + active: Option>, } impl Known { @@ -116,7 +127,7 @@ impl Known { /// it settles again (`true`, so the bound counts from now). One that never /// settled is unusable at once: its end is its last word, and every /// question waiting on it is answered now rather than at the bound. - fn ended(&mut self, badge: Badge) -> bool { + fn ended(&mut self, badge: DriverBadge) -> bool { match self.fates.insert(badge, Fate::Ended) { Some(_) => true, None => { @@ -128,7 +139,7 @@ impl Known { /// The drivers whose controllers' partitions cannot be said absent: those /// expected and never heard, and those unusable or ended. - fn unsettled(&self) -> Vec { + fn unsettled(&self) -> Vec { let silent = self.expected.iter().filter(|d| !self.fates.contains_key(d)).copied(); silent.chain(self.fates.iter().filter(|(_, f)| **f != Fate::Offered).map(|(d, _)| *d)).collect() } @@ -144,18 +155,20 @@ impl Known { } } - /// What a listing through `grant` answers, or `None` while it waits: - /// every partition offered that it admits. **The grant is asked first**, + /// What a listing through `badge` answers, or `None` while it waits: + /// every partition offered that it lists. **The badge is asked first**, /// so a connection with none learns nothing. - fn listing(&self, grant: Option, expired: bool) -> Option, Refusal>> { - let Some(grant) = grant else { return Some(Err(Refusal::NotGranted)) }; + fn listing(&self, badge: Option, expired: bool) -> Option, Refusal>> { + let Some(badge) = badge.filter(|b| *b != Badge::DesignateData) else { + return Some(Err(Refusal::NotGranted)); + }; if !self.settled() && !expired { return None; } let listed: Vec = self .offered .iter() - .filter(|o| grant.admits(o.part.unique, o.part.kind)) + .filter(|o| badge.lists(o.part.unique, o.part.kind)) .map(|o| Listed { disk: o.disk, unique: o.part.unique, kind: o.part.kind }) .collect(); Some(match self.absent() { @@ -193,6 +206,45 @@ impl Known { } } +impl Known { + /// Where a description of disk `disk` through `badge` is answered, or its + /// refusal, or `None` while it waits: only a [`Badge::Tables`] reaches + /// one. + fn describe(&self, disk: u32, badge: Option, expired: bool) -> Option> { + if badge != Some(Badge::Tables) { + return Some(Err(Refusal::NotGranted)); + } + if !self.settled() && !expired { + return None; + } + Some(if self.offered.iter().any(|o| o.disk == disk) { Ok(disk) } else { Err(self.absent()) }) + } + + /// Where the designation `badge` asks for is carried out — the one DATA + /// partition offered, resolved now — and whether the running system is on + /// it; or its refusal, or `None` while it waits. Two DATA partitions are + /// never guessed between, and a GUID two disks carry is refused on both, + /// as an open of it is. + fn designation(&self, badge: Option, expired: bool) -> Option> { + if badge != Some(Badge::DesignateData) { + return Some(Err(Refusal::NotGranted)); + } + if !self.settled() && !expired { + return None; + } + let data: Vec<&Offered> = self.offered.iter().filter(|o| o.part.kind == toyos_gpt::Guid::TOYOS_DATA.0).collect(); + let Some(one) = data.first().filter(|_| data.len() == 1) else { + return Some(Err(if data.is_empty() { self.absent() } else { Refusal::Unusable })); + }; + let unique = one.part.unique; + let carrying = self.offered.iter().filter(|o| o.part.unique == unique).count(); + Some(match (carrying, one.part.blocks) { + (1, Some(_)) => Ok((**one, self.active.as_ref().is_none_or(|a| a.contains(&unique)))), + _ => Err(Refusal::Unusable), + }) + } +} + /// A connection that has not asked anything yet. struct Pending { conn: Connection, @@ -204,18 +256,20 @@ struct Pending { enum Ask { List, Open { guid: [u8; 16], region: OwnedHandle }, + Describe { disk: u32 }, + Designate { commit: bool }, } /// A client whose question waits for the drivers. struct Waiting { conn: Connection, - grant: Option, + badge: Option, ask: Ask, } /// One disk driver's connection. struct Driver { - badge: Badge, + badge: DriverBadge, conn: Connection, rx: ipc::FrameRx<{ 4 + disk::MAX_PARTS * Part::BYTES }>, label: Option, @@ -240,10 +294,23 @@ fn main() { let args: Vec = std::env::args().collect(); let mut known = Known::default(); let mut at = 1; + known.active = Some(Vec::new()); while at < args.len() { + if args[at] == ACTIVE_UNKNOWN { + println!("diskserver: which partitions the running system is on is unknown; every designation is refused"); + known.active = None; + at += 1; + continue; + } let value = args.get(at + 1).unwrap_or_else(|| panic!("diskserver: {} takes a value", args[at])); match args[at].as_str() { DRIVER => known.expected.push(value.parse().unwrap_or_else(|_| panic!("diskserver: {DRIVER} {value}"))), + ACTIVE => { + let guid = toyos_gpt::Guid::parse(value).unwrap_or_else(|| panic!("diskserver: {ACTIVE} {value}")); + if let Some(active) = &mut known.active { + active.push(guid.0); + } + } UNDRIVEN => { println!("diskserver: {value} is on this machine and no driver of it started; what it carries is refused"); known.undriven.push(value.clone()); @@ -457,11 +524,20 @@ impl Service { } } - /// One client's first frame: a listing, or an open. + /// One client's first frame: a listing, an open, a description or a + /// designation. fn asked(&mut self, clients: &toyos::port::Acceptor, mut p: Pending, msg_type: u32, payload_len: usize) { - let grant = grant(clients, &p.conn); - if msg_type == wire::MSG_LIST && payload_len == 0 { - return self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::List }); + let badge = badge(clients, &p.conn); + let payload = p.rx.payload(payload_len); + let ask = match (msg_type, payload) { + (wire::MSG_LIST, []) => Some(Ask::List), + (wire::MSG_DESCRIBE, _) => disk::disk(payload).map(|disk| Ask::Describe { disk }), + (wire::MSG_DESIGNATE, [0]) => Some(Ask::Designate { commit: false }), + (wire::MSG_DESIGNATE, [1]) => Some(Ask::Designate { commit: true }), + _ => None, + }; + if let Some(ask) = ask { + return self.waiting.push(Waiting { conn: p.conn, badge, ask }); } let guid = wire::guid(p.rx.payload(payload_len)); let handles = p.rx.take_handles::<1>(); @@ -474,7 +550,7 @@ impl Service { // SAFETY: the handle arrived with this frame and nothing else in this // process names it. let region = unsafe { OwnedHandle::from_raw(region) }; - self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::Open { guid, region } }); + self.waiting.push(Waiting { conn: p.conn, badge, ask: Ask::Open { guid, region } }); } /// Answer every waiting question the drivers have now answered, or whose @@ -484,14 +560,14 @@ impl Service { for w in waiting { let expired = now >= self.awaited_since + DRIVER_BOUND; match w.ask { - Ask::List => match self.known.listing(w.grant, expired) { + Ask::List => match self.known.listing(w.badge, expired) { None => self.waiting.push(w), Some(answer) => { self.waited(expired, "a listing"); list(&w.conn, answer); } }, - Ask::Open { guid, region } => match self.known.open(guid, w.grant, expired) { + Ask::Open { guid, region } => match self.known.open(guid, partitions(w.badge), expired) { None => self.waiting.push(Waiting { ask: Ask::Open { guid, region }, ..w }), Some(Err(why)) => { self.waited(expired, "an open"); @@ -499,10 +575,28 @@ impl Service { refuse(&w.conn, why); } Some(Ok(offered)) => { - let writes = w.grant.expect("admitted through a grant").writes; + let writes = partitions(w.badge).expect("admitted through a grant").writes; self.hand_over(offered, guid, writes, w.conn, region); } }, + Ask::Describe { disk } => match self.known.describe(disk, w.badge, expired) { + None => self.waiting.push(Waiting { ask: Ask::Describe { disk }, ..w }), + Some(Err(why)) => refuse(&w.conn, why), + Some(Ok(ours)) => self.hand_work(ours, disk::MSG_DESCRIBE, |theirs| theirs.to_le_bytes().to_vec(), w.conn), + }, + Ask::Designate { commit } => match self.known.designation(w.badge, expired) { + None => self.waiting.push(Waiting { ask: Ask::Designate { commit }, ..w }), + Some(Err(why)) => { + println!("diskserver: a designation refused: {why:?}"); + refuse(&w.conn, why); + } + Some(Ok((offered, active))) => { + let unique = offered.part.unique; + let order = |disk| disk::Designation { disk, unique, active, commit }.encode().to_vec(); + println!("diskserver: designation of {} handed to disk {}'s driver", guid_text(unique), offered.disk); + self.hand_work(offered.disk, disk::MSG_DESIGNATE, order, w.conn); + } + }, } } } @@ -536,6 +630,30 @@ impl Service { } } +impl Service { + /// Hand `conn` to the driver of disk `ours`, with the frame `payload` + /// makes of that driver's own number for the disk: the driver answers it. + fn hand_work(&mut self, ours: u32, msg_type: u32, payload: impl Fn(u32) -> Vec, conn: Connection) { + let Some((&id, d)) = self.drivers.iter().find(|(_, d)| d.disks.values().any(|&o| o == ours)) else { + unreachable!("diskserver: disk {ours} is offered and no driver has it") + }; + let theirs = *d.disks.iter().find(|(_, o)| **o == ours).expect("just found").0; + match d.conn.try_send_bytes_with(msg_type, &payload(theirs), [conn.into_owned()]) { + Ok(()) => {} + Err(ipc::TrySendError::Full) => self.drop_driver(id, "it would not take a client's work"), + Err(why) => println!("diskserver: the kernel would not move a client to disk driver {}: {why:?}", d.badge), + } + } +} + +/// The partitions grant a badge carries, if it is one. +fn partitions(badge: Option) -> Option { + match badge { + Some(Badge::Partitions(grant)) => Some(grant), + _ => None, + } +} + fn refuse(conn: &Connection, why: Refusal) { let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); } @@ -556,7 +674,7 @@ fn list(conn: &Connection, answer: Result, Refusal>) { /// Who the driver on `conn`, accepted from `acceptor`, is: the badge the /// supervisor minted its connector with. -fn driver_badge(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { +fn driver_badge(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { let mut badge = [0u8; MAX_BADGE]; match acceptor.badge(conn, &mut badge) { Ok(bytes) => disk::identity(bytes), @@ -565,12 +683,12 @@ fn driver_badge(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option Option { +fn badge(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { let mut badge = [0u8; MAX_BADGE]; match acceptor.badge(conn, &mut badge) { - Ok(bytes) => Grant::decode(bytes), + Ok(bytes) => Badge::decode(bytes), Err(SyscallError::NotFound) => None, Err(why) => panic!("diskserver: its own acceptor would not say a connection's badge: {why:?}"), } @@ -587,11 +705,11 @@ mod tests { const ROOT: [u8; 16] = [4; 16]; const LOG_KIND: [u8; 16] = [0x10; 16]; const BOOT_KIND: [u8; 16] = [0x20; 16]; - const DATA_KIND: [u8; 16] = [0x30; 16]; + const DATA_KIND: [u8; 16] = toyos_gpt::Guid::TOYOS_DATA.0; const ROOT_KIND: [u8; 16] = [0x40; 16]; /// Two drivers, by the badges the supervisor minted them. - const STICK: Badge = 7; - const NVME: Badge = 9; + const STICK: DriverBadge = 7; + const NVME: DriverBadge = 9; fn unique(guid: [u8; 16]) -> Option { Some(Grant { scope: Scope::Unique(guid), writes: true }) @@ -605,7 +723,7 @@ mod tests { Part { unique, kind, blocks: Some(100) } } - fn known(expected: &[Badge]) -> Known { + fn known(expected: &[DriverBadge]) -> Known { Known { expected: expected.to_vec(), ..Known::default() } } @@ -647,9 +765,9 @@ mod tests { let uniques = |answer: Option, Refusal>>| { answer.expect("answered").map(|all| all.iter().map(|l| (l.disk, l.unique)).collect::>()) }; - assert_eq!(uniques(known.listing(unique(BOOT), false)), Ok(vec![(0, BOOT)])); - assert_eq!(uniques(known.listing(data(), false)), Ok(vec![(1, DATA)])); - assert_eq!(uniques(known.listing(unique([9; 16]), false)), Ok(vec![])); + assert_eq!(uniques(known.listing(unique(BOOT).map(Badge::Partitions), false)), Ok(vec![(0, BOOT)])); + assert_eq!(uniques(known.listing(data().map(Badge::Partitions), false)), Ok(vec![(1, DATA)])); + assert_eq!(uniques(known.listing(unique([9; 16]).map(Badge::Partitions), false)), Ok(vec![])); assert_eq!(known.listing(None, false), Some(Err(Refusal::NotGranted))); } @@ -662,19 +780,19 @@ mod tests { let mut known = known(&[STICK, NVME]); known.fates.insert(NVME, Fate::Offered); known.offered.push(Offered { disk: 0, part: part(DATA, DATA_KIND) }); - assert_eq!(known.listing(data(), false), None, "the stick's driver has not settled"); + assert_eq!(known.listing(data().map(Badge::Partitions), false), None, "the stick's driver has not settled"); assert_eq!(known.open(LOG, unique(LOG), false), None); assert_eq!(known.open(DATA, data(), false), None, "offered, and another disk may carry it too"); assert_eq!(known.open(DATA, None, false), Some(Err(Refusal::NotGranted)), "no grant waits for nothing"); assert_eq!(known.open(LOG, unique(LOG), true), Some(Err(Refusal::Unusable)), "the bound passed, a driver silent"); assert!(known.open(DATA, data(), true).is_some_and(|r| r.is_ok())); - assert!(known.listing(data(), true).is_some_and(|r| r.is_ok_and(|l| l.len() == 1))); + assert!(known.listing(data().map(Badge::Partitions), true).is_some_and(|r| r.is_ok_and(|l| l.len() == 1))); let log = Some(Grant { scope: Scope::Kind(LOG_KIND), writes: true }); - assert_eq!(known.listing(log, true), Some(Err(Refusal::Unusable)), "nothing listed, a driver silent"); + assert_eq!(known.listing(log.map(Badge::Partitions), true), Some(Err(Refusal::Unusable)), "nothing listed, a driver silent"); known.fates.insert(STICK, Fate::Offered); assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::NotFound)), "every driver said"); - assert_eq!(known.listing(log, false), Some(Ok(vec![]))); + assert_eq!(known.listing(log.map(Badge::Partitions), false), Some(Ok(vec![]))); } /// A GUID the first driver offers is not routed while another may offer @@ -702,7 +820,7 @@ mod tests { known.offered.retain(|o| o.disk != 1); assert_eq!(known.open(DATA, data(), false), None, "a type grant waits for the offer"); assert_eq!(known.open(DATA, unique(DATA), false), None); - assert_eq!(known.listing(data(), false), None); + assert_eq!(known.listing(data().map(Badge::Partitions), false), None); known.fates.insert(NVME, Fate::Offered); known.offered.push(Offered { disk: 2, part: part(DATA, DATA_KIND) }); assert_eq!(known.open(DATA, data(), false), Some(Ok(Offered { disk: 2, part: part(DATA, DATA_KIND) }))); @@ -746,13 +864,13 @@ mod tests { fn an_unusable_or_undriven_controller_is_refused_and_an_absent_one_lists_nothing() { let mut unusable = known(&[NVME]); unusable.fates.insert(NVME, Fate::Unusable); - assert_eq!(unusable.listing(data(), false), Some(Err(Refusal::Unusable))); + assert_eq!(unusable.listing(data().map(Badge::Partitions), false), Some(Err(Refusal::Unusable))); assert_eq!(unusable.open(DATA, unique(DATA), false), Some(Err(Refusal::Unusable))); let undriven = Known { undriven: vec!["pci:01:08:02 at PCI 00:02.0".into()], ..Known::default() }; - assert_eq!(undriven.listing(data(), false), Some(Err(Refusal::ClaimRefused))); + assert_eq!(undriven.listing(data().map(Badge::Partitions), false), Some(Err(Refusal::ClaimRefused))); assert_eq!(undriven.open(DATA, unique(DATA), false), Some(Err(Refusal::ClaimRefused))); let absent = Known::default(); - assert_eq!(absent.listing(data(), false), Some(Ok(vec![]))); + assert_eq!(absent.listing(data().map(Badge::Partitions), false), Some(Ok(vec![]))); assert_eq!(absent.open(DATA, unique(DATA), false), Some(Err(Refusal::NotFound))); } @@ -766,4 +884,46 @@ mod tests { assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::Unusable))); assert_eq!(known.open([7; 16], unique([7; 16]), false), Some(Err(Refusal::Unusable))); } + + /// A description reaches a disk only through a tables badge, and names + /// one the service has. A designation reaches only the machine's one DATA + /// partition, resolved when it is asked, said active when the running + /// system is on it, and is refused when two DATA partitions or two disks + /// carrying its GUID leave it no one partition; nothing a partitions grant + /// or a tables badge carries designates anything. + #[test] + fn only_its_own_badge_describes_or_designates() { + let mut known = two_disks(); + known.active = Some(vec![LOG]); + assert_eq!(known.describe(1, Some(Badge::Tables), false), Some(Ok(1))); + assert_eq!(known.describe(7, Some(Badge::Tables), false), Some(Err(Refusal::NotFound))); + for other in [None, data().map(Badge::Partitions), Some(Badge::DesignateData)] { + assert_eq!(known.describe(1, other, false), Some(Err(Refusal::NotGranted)), "{other:?}"); + } + let designate = Some(Badge::DesignateData); + let data_disk = Offered { disk: 1, part: part(DATA, DATA_KIND) }; + assert_eq!(known.designation(designate, false), Some(Ok((data_disk, false)))); + for other in [None, data().map(Badge::Partitions), Some(Badge::Tables)] { + assert_eq!(known.designation(other, false), Some(Err(Refusal::NotGranted)), "{other:?}"); + } + assert_eq!(known.listing(designate, false), Some(Err(Refusal::NotGranted)), "a designation lists nothing"); + assert_eq!(known.listing(Some(Badge::Tables), false).map(|r| r.map(|l| l.len())), Some(Ok(4))); + + known.active = Some(vec![DATA]); + assert_eq!(known.designation(designate, false), Some(Ok((data_disk, true))), "the running system's DATA"); + known.active = None; + assert_eq!(known.designation(designate, false), Some(Ok((data_disk, true))), "nothing known is active"); + + let mut cloned = two_disks(); + cloned.active = Some(Vec::new()); + cloned.offered.push(Offered { disk: 0, part: part([7; 16], LOG_KIND) }); + cloned.offered[0].part.unique = DATA; + assert_eq!(cloned.designation(designate, false), Some(Err(Refusal::Unusable)), "its GUID on two disks"); + let mut two = two_disks(); + two.offered.push(Offered { disk: 0, part: part([8; 16], DATA_KIND) }); + assert_eq!(two.designation(designate, false), Some(Err(Refusal::Unusable)), "two DATA partitions"); + let mut none = two_disks(); + none.offered.retain(|o| o.part.kind != DATA_KIND); + assert_eq!(none.designation(designate, false), Some(Err(Refusal::NotFound))); + } } diff --git a/system/diskserver/src/session.rs b/system/diskserver/src/session.rs index edc6123da30..587715fd4a2 100644 --- a/system/diskserver/src/session.rs +++ b/system/diskserver/src/session.rs @@ -376,7 +376,7 @@ fn whole_blocks(len: usize) -> u32 { blocks } -fn connect(names: &Namespace, service: &str) -> Result { +pub(crate) fn connect(names: &Namespace, service: &str) -> Result { names.open(service).map_err(|e| match e { SyscallError::NotFound => Error::NotHeld, e => Error::Kernel(e), @@ -385,7 +385,7 @@ fn connect(names: &Namespace, service: &str) -> Result { /// The header of the service's answer on `conn`, once it is there, within /// [`wire::ANSWER_BOUND_NS`]. -fn answer(conn: &Connection) -> Result { +pub(crate) fn answer(conn: &Connection) -> Result { let poller = Poller::new(1); poller.watch(conn, READABLE, 0); let mut ready = false; diff --git a/system/diskserver/src/tables.rs b/system/diskserver/src/tables.rs new file mode 100644 index 00000000000..48da7223035 --- /dev/null +++ b/system/diskserver/src/tables.rs @@ -0,0 +1,57 @@ +//! A client's side of a disk's description and of a designation, asked of +//! the block service through a tables or a designation badge +//! (`toyos_blockring::wire::Badge`). + +use toyos::namespace::Namespace; +use toyos_blockring::wire::{self, Designated, DiskHead, Refusal, SlotLine}; + +use crate::session::{answer, connect}; +use crate::Error; + +/// The longest frame either answer carries. +const FRAME: usize = 4 + 2 * (2 + wire::MAX_STATE) + 64; + +/// One frame's type and payload on `conn`, within the answer bound. +fn frame(conn: &toyos::ipc::Connection) -> Result<(u32, Vec), Error> { + let header = answer(conn)?; + let mut payload = vec![0u8; (header.len() as usize).min(FRAME)]; + let len = conn.recv_bytes(&header, &mut payload).map_err(|_| Error::Ended)?; + payload.truncate(len); + Ok((header.msg_type, payload)) +} + +fn refused(payload: &[u8]) -> Error { + Refusal::decode(payload).map_or(Error::Protocol, Error::Refused) +} + +/// What disk `disk` (the service's number, from a listing) holds: its head +/// and every used entry of its table in force. +pub fn describe(names: &Namespace, service: &str, disk: u32) -> Result<(DiskHead, Vec), Error> { + let conn = connect(names, service)?; + conn.send_bytes(wire::MSG_DESCRIBE, &disk.to_le_bytes()).map_err(|_| Error::Ended)?; + let head = match frame(&conn)? { + (wire::MSG_DISK, payload) => DiskHead::decode(&payload).ok_or(Error::Protocol)?, + (wire::MSG_REFUSED, payload) => return Err(refused(&payload)), + _ => return Err(Error::Protocol), + }; + let mut slots = Vec::new(); + loop { + match frame(&conn)? { + (wire::MSG_SLOT, payload) => slots.push(SlotLine::decode(&payload).ok_or(Error::Protocol)?), + (wire::MSG_DESCRIBED, payload) if payload.is_empty() => return Ok((head, slots)), + _ => return Err(Error::Protocol), + } + } +} + +/// Carry out the designation the connector `service` names was minted for: +/// write the stamp when `commit`, judge only otherwise. +pub fn designate(names: &Namespace, service: &str, commit: bool) -> Result { + let conn = connect(names, service)?; + conn.send_bytes(wire::MSG_DESIGNATE, &[u8::from(commit)]).map_err(|_| Error::Ended)?; + match frame(&conn)? { + (wire::MSG_DESIGNATED, payload) => Designated::decode(&payload).ok_or(Error::Protocol), + (wire::MSG_REFUSED, payload) => Err(refused(&payload)), + _ => Err(Error::Protocol), + } +} diff --git a/system/fileserver/src/data.rs b/system/fileserver/src/data.rs index 15739097bba..a19b3eb3c8b 100644 --- a/system/fileserver/src/data.rs +++ b/system/fileserver/src/data.rs @@ -197,12 +197,7 @@ fn designated(cache: &Cache) -> bool { return false; } let [block0] = block0; - let magic = bcachefs::DESIGNATION_MAGIC; - let at = bcachefs::DESIGNATION_BLOCKS_OFFSET; - if block0[..magic.len()] != magic { - return false; - } - let stamped = u64::from_le_bytes(block0[at..at + 8].try_into().expect("eight bytes")); + let Some(stamped) = bcachefs::designated(&block0) else { return false }; if stamped != cache.blocks() { println!( "fileserver: a designation stamp at block 0 names {stamped} blocks, and this partition has {}; ignoring it", diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index 37e9022db56..7abacd93ae0 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -345,7 +345,10 @@ fn data_on(disk: D, roots: &[&str]) -> Box { println!("fileserver: the DATA volume is ours and does not mount ({why}); it is absent this boot"); Box::new(Absent::new(roots, why)) } - Probed::Foreign => ram(roots, "the DATA partition is not ours"), + // A disk that is there and holds somebody's bytes is no machine without + // one: nothing stands in for it, and a designation is how it becomes + // ToyOS's. + Probed::Foreign => absent(roots, "the DATA partition holds no volume of ours and no designation stamp".into()), } } diff --git a/system/nvme/src/main.rs b/system/nvme/src/main.rs index 38d86ee4e18..b8a9c286fb4 100644 --- a/system/nvme/src/main.rs +++ b/system/nvme/src/main.rs @@ -22,7 +22,7 @@ use toyos::PciDev; use toyos_abi::syscall::{SyscallError, DEV_PREFIX}; use toyos_blockring::disk::Settled; use toyos_blockring::BLOCK_BYTES; -use toyos_blockserve::link::Link; +use toyos_blockserve::link::{Handed, Link}; use toyos_blockserve::sessions::MAX_SESSIONS; use toyos_blockserve::{Device, Sessions}; @@ -47,8 +47,10 @@ const TOKEN_LINK: u64 = 1; const TOKEN_STOP: u64 = 2; const TOKEN_SESSION: u64 = 0x1_0000; -/// The controller as `toyos_gpt` reads it: its own sectors, served a 4 KiB -/// block at a time, and floored to whole blocks. +/// The controller as `toyos_gpt` reads it: every sector of the namespace, +/// a 4 KiB block at a time where the block is whole and one sector at a time +/// past the last whole block, where the backup table is on a namespace whose +/// count is not whole blocks. struct Sectors<'a> { ctrl: &'a mut Controller, block: Vec, @@ -59,20 +61,19 @@ impl toyos_gpt::Sectors for Sectors<'_> { self.ctrl.lba_bytes } fn lba_count(&self) -> u64 { - let per = BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64; - self.ctrl.sectors / per * per - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64).expect("at least one") + self.ctrl.sectors } fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - let lba_bytes = self.ctrl.lba_bytes as u64; - let at = lba * lba_bytes; - if !self.ctrl.read_block(at / BLOCK_BYTES as u64, &mut self.block) { + let per = BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64; + let first = lba / per * per; + if first + per > self.ctrl.sectors { + return self.ctrl.read_sectors(lba, buf); + } + if !self.ctrl.read_sectors(first, &mut self.block) { return false; } - let off = (at % BLOCK_BYTES as u64) as usize; - buf.copy_from_slice(&self.block[off..off + lba_bytes as usize]); + let off = ((lba - first) * self.ctrl.lba_bytes as u64) as usize; + buf.copy_from_slice(&self.block[off..off + self.ctrl.lba_bytes as usize]); true } } @@ -133,8 +134,7 @@ fn main() { ctrl.sectors ); let read = toyos_blockserve::table::read(&mut Sectors { ctrl: &mut ctrl, block: vec![0u8; BLOCK_BYTES] }); - let parts = toyos_blockserve::table::say(&name, read); - let mut disk = Sessions::new(name.clone(), parts); + let mut disk = Sessions::new(name.clone(), read); link.offer(DISK, disk.offered()); link.settle(Settled::Offered); serve(&name, &mut ctrl, &mut disk, &mut link, stop.as_ref()) @@ -155,8 +155,9 @@ fn idle(link: &mut Link, stop: Option<&toyos::Pipe>) -> ! { std::process::exit(0); } for handed in link.take() { - toyos_abi::syscall::close(handed.region); - drop(handed.conn); + if let Handed::Session { region, .. } = handed { + toyos_abi::syscall::close(region); + } } } } @@ -199,12 +200,23 @@ fn serve(name: &str, ctrl: &mut Controller, disk: &mut Sessions, link: &mut Link } for handed in link.take() { - if handed.session.disk != DISK { - println!("{name}: the block service handed a session on disk {}, which this driver never offered", handed.session.disk); - toyos_abi::syscall::close(handed.region); + let on = match &handed { + Handed::Session { session, .. } => session.disk, + Handed::Describe { disk, .. } => *disk, + Handed::Designate { order, .. } => order.disk, + }; + if on != DISK { + println!("{name}: the block service handed work on disk {on}, which this driver never offered"); + if let Handed::Session { region, .. } = handed { + toyos_abi::syscall::close(region); + } continue; } - disk.open(ctrl, handed.conn, handed.region, handed.session.unique, handed.session.writes); + match handed { + Handed::Session { session, conn, region } => disk.open(ctrl, conn, region, session.unique, session.writes), + Handed::Describe { conn, .. } => disk.describe(conn), + Handed::Designate { order, conn } => disk.designate(ctrl, conn, order), + } } for id in disk.ids() { if ready.contains(&(TOKEN_SESSION + id)) { diff --git a/system/nvme/src/nvme.rs b/system/nvme/src/nvme.rs index f3ba02cb588..954e8d50c71 100644 --- a/system/nvme/src/nvme.rs +++ b/system/nvme/src/nvme.rs @@ -580,10 +580,16 @@ impl Controller { /// # Panics /// With no room: the caller asks [`Self::has_room`] first. pub fn submit_io(&mut self, write: bool, block: u64, blocks: u32, at: u64, owner: Owner) { - let queue = self.roomiest().expect("nvme: an I/O submission with no queue room"); let per = (toyos_blockring::BLOCK_BYTES as u32 / self.lba_bytes) as u64; - let sectors = blocks as u64 * per; - let pages = blocks as usize; + self.submit_sectors(write, block * per, blocks as u64 * per, at, owner); + } + + /// [`Self::submit_io`] in the namespace's own sectors: `sectors` of them + /// from `lba`, which a table at the device's end needs when its sector + /// count is not whole blocks. + fn submit_sectors(&mut self, write: bool, lba: u64, sectors: u64, at: u64, owner: Owner) { + let queue = self.roomiest().expect("nvme: an I/O submission with no queue room"); + let pages = (sectors * self.lba_bytes as u64).div_ceil(PAGE as u64) as usize; let (dma, dma_addr) = (self.dma, self.dma_addr); let list_base = OFF_PRP + queue * IO_DEPTH as usize * PAGE; let cmd = Command { @@ -595,8 +601,8 @@ impl Controller { 2 => at + PAGE as u64, _ => u64::MAX, }, - cdw10: (block * per) as u32, - cdw11: ((block * per) >> 32) as u32, + cdw10: lba as u32, + cdw11: (lba >> 32) as u32, cdw12: (sectors - 1) as u32, }; // Every page after the first, in this slot's list page (§4.1.1). @@ -619,12 +625,14 @@ impl Controller { self.peak = self.peak.max(self.busy()); } - /// Read one 4 KiB block synchronously into `out`, through this driver's - /// own scratch page: before any session exists, for the partition table. - pub fn read_block(&mut self, block: u64, out: &mut [u8]) -> bool { - assert_eq!(out.len(), toyos_blockring::BLOCK_BYTES); + /// Read `out.len()` bytes of whole sectors from `lba`, at most a page, + /// synchronously through this driver's own scratch page: while no + /// session's command is on the device, for the partition table. + pub fn read_sectors(&mut self, lba: u64, out: &mut [u8]) -> bool { + assert!(out.len() <= PAGE && out.len().is_multiple_of(self.lba_bytes as usize) && !out.is_empty()); assert_eq!(self.busy(), 0, "nvme: a waited read beside other commands"); - self.submit_io(false, block, 1, self.dma_addr + OFF_SCRATCH as u64, Owner::Driver); + let sectors = (out.len() / self.lba_bytes as usize) as u64; + self.submit_sectors(false, lba, sectors, self.dma_addr + OFF_SCRATCH as u64, Owner::Driver); let start = Instant::now(); let mut done = Vec::new(); let ok = loop { diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 09b12cb3d12..9300f4e8389 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -1163,8 +1163,9 @@ impl<'a> Supervisor<'a> { } /// What the block service is started with: each disk driver running, by - /// its badge, which it waits for, and every disk driver's function the - /// boot started none on. + /// its badge, which it waits for; every disk driver's function the boot + /// started none on; and every partition the loader named, which the + /// running system is on and no designation changes. fn block_args(&self) -> Vec { let mut args = Vec::new(); for instance in self.instances.iter().filter(|i| is_disk_driver(i.row)) { @@ -1173,6 +1174,21 @@ impl<'a> Supervisor<'a> { for undriven in &self.undriven { args.extend(["--undriven".to_string(), undriven.clone()]); } + match inventory(self.syscap) { + Ok(records) => { + let named = records.iter().filter_map(|r| match r { + Record::Loaded(l) => Some(l.unique_guid), + _ => None, + }); + for guid in named { + args.extend(["--active".to_string(), guid_text(guid)]); + } + } + Err(why) => { + say!("supervisor: the inventory was not read ({why}), so the block service designates nothing"); + args.push("--active-unknown".to_string()); + } + } args } @@ -3003,6 +3019,7 @@ fn start<'a>( } let mut view = grants.view(program, minting.session); view.extend(ports); + view.extend(grants.authorizations(program).map_err(StartError::Refused)?); view.extend(grants.netstack(program, minting.session)); if let Some(ns) = build_namespace(program, system, connectors, view, extras)? { let raw = ns.into_raw(); @@ -3370,6 +3387,32 @@ impl Grants<'_> { .map_err(|e| format!("no connector to the block service could be minted: {e:?}")) } + /// What `program`'s row is authorized for on the machine's disks + /// (`toyos_manifest::authorize`), each a connector to the block service + /// minted with that one authorization's badge, under its name. The block + /// service resolves and judges each when it is used, so nothing here + /// waits for a disk. + fn authorizations(&self, program: &Program) -> Result, String> { + use toyos_manifest::authorize::Authorization; + let mut minted = Vec::new(); + for word in &program.authorize { + let authorization = Authorization::parse(word).unwrap_or_else(|| panic!("supervisor: `{word}` is no authorization the build admits")); + let badge = match authorization { + Authorization::Tables => toyos_blockring::wire::Badge::Tables, + Authorization::DesignateData => toyos_blockring::wire::Badge::DesignateData, + }; + let Some(ports) = &self.block else { return Err(format!("`{word}` is authorized and no block service runs")) }; + let ports = ports.borrow(); + let Some((_, acceptor)) = ports.iter().find(|(name, _)| name == toyos_blockring::PORT) else { + return Err("the block service's port has closed for good".into()); + }; + let connector = acceptor.mint(&badge.encode()).map_err(|e| format!("no `{word}` connector could be minted: {e:?}"))?; + say!("supervisor: {} is authorized for `{word}`", program.name); + minted.push((authorization.name().to_string(), connector)); + } + Ok(minted) + } + /// The block service's `disk` port minted for the disk driver of the /// function that arrived as `arrival`. fn disk(&self, arrival: Arrival) -> Result { diff --git a/system/usbd/src/main.rs b/system/usbd/src/main.rs index d4e3f7ff104..15f032883f7 100644 --- a/system/usbd/src/main.rs +++ b/system/usbd/src/main.rs @@ -29,9 +29,9 @@ use toyos::poller::{Poller, READABLE, WRITABLE}; use toyos::{AsHandle, PciDev}; use toyos_abi::syscall::{SyscallError, DEV_PREFIX}; use toyos_blockring::disk::Settled; -use toyos_blockserve::link::Link; +use toyos_blockserve::link::{Handed, Link}; use toyos_blockserve::sessions::{Owner as Request, MAX_SESSIONS}; -use toyos_blockserve::{table, Device, Partition, Sessions}; +use toyos_blockserve::{table, Device, Sessions}; use toyos_input::wire::{self, Record, MAX_PAYLOAD}; use toyos_inspect::{Snapshot, MSG_INSPECT, MSG_SNAPSHOT}; use toyos_pci_claim::{Bar, Grant}; @@ -70,7 +70,7 @@ impl Host for Process { } } -type Driver = Usb, String>>; +type Driver = Usb; /// The owner of input, over the `input` port. #[derive(Default)] @@ -223,7 +223,9 @@ fn idle(mut link: Option, stop: Option) -> ! { std::process::exit(0); } for handed in link.iter_mut().flat_map(Link::take) { - toyos_abi::syscall::close(handed.region); + if let Handed::Session { region, .. } = handed { + toyos_abi::syscall::close(region); + } } } } @@ -268,7 +270,7 @@ fn serve( match event { Event::Arrived { disk, name: disk_name, table } => { let label = format!("{name} {disk_name}"); - let sessions = Sessions::new(label.clone(), table::say(&label, table)); + let sessions = Sessions::new(label.clone(), table); if let Some(link) = &mut link { link.offer(disk, sessions.offered()); } @@ -299,15 +301,23 @@ fn serve( } } for handed in link.iter_mut().flat_map(Link::take) { - match disks.get_mut(&handed.session.disk) { - Some(sessions) => { - let mut port = Disk { usb, disk: handed.session.disk, claim: dev }; - sessions.open(&mut port, handed.conn, handed.region, handed.session.unique, handed.session.writes); - } - None => { - println!("{name}: the block service handed a session on disk {}, which is not served", handed.session.disk); - toyos_abi::syscall::close(handed.region); + let on = match &handed { + Handed::Session { session, .. } => session.disk, + Handed::Describe { disk, .. } => *disk, + Handed::Designate { order, .. } => order.disk, + }; + let Some(sessions) = disks.get_mut(&on) else { + println!("{name}: the block service handed work on disk {on}, which is not served"); + if let Handed::Session { region, .. } = handed { + toyos_abi::syscall::close(region); } + continue; + }; + let mut port = Disk { usb, disk: on, claim: dev }; + match handed { + Handed::Session { session, conn, region } => sessions.open(&mut port, conn, region, session.unique, session.writes), + Handed::Describe { conn, .. } => sessions.describe(conn), + Handed::Designate { order, conn } => sessions.designate(&mut port, conn, order), } } let submitted = usb.disks.submitted(); diff --git a/system/usbd/xhci/src/msc.rs b/system/usbd/xhci/src/msc.rs index 4c51710cd2f..02aa2ec36e2 100644 --- a/system/usbd/xhci/src/msc.rs +++ b/system/usbd/xhci/src/msc.rs @@ -19,7 +19,7 @@ use alloc::format; use alloc::string::String; use alloc::vec::Vec; use core::cell::{Cell, RefCell}; -use core::num::{NonZeroU16, NonZeroU64}; +use core::num::NonZeroU16; use toyos_device_memory::{DmaBuffers, Registers}; use toyos_scsi::{self as scsi, Cdb, Geometry, Reply, Sense, HOST_BLOCK, SENSE_BYTES}; @@ -441,13 +441,7 @@ impl<'d, 'u, R: Registers, D: DmaBuffers, C: Host, T> Msc<'d, 'u, R, D, C, T> { /// the read until it asks for none it has not been given. async fn table(&mut self, geometry: &Geometry) -> Result { let per = HOST_BLOCK / geometry.sector_bytes(); - let mut cache = Cache { - lba_bytes: geometry.sector_bytes(), - lbas: geometry.blocks() * u64::from(per), - per: NonZeroU64::new(u64::from(per)).expect("at least one sector a block"), - have: BTreeMap::new(), - missed: Vec::new(), - }; + let mut cache = Cache { lba_bytes: geometry.sector_bytes(), lbas: geometry.sectors(), have: BTreeMap::new(), missed: Vec::new() }; let mut reads = 0; loop { cache.missed.clear(); @@ -463,12 +457,17 @@ impl<'d, 'u, R: Registers, D: DmaBuffers, C: Host, T> Msc<'d, 'u, R, D, C, T> { } reads += 1; let at = self.addr(self.block); - let cdb = Cdb::transfer(false, (block * u64::from(per)) as u32, per as u16); - let read = self.command(&cdb, Data::In { at, len: HOST_BLOCK }).await?; - for s in 0..u64::from(per) { + // The last block of a disk whose count is not whole blocks is + // read as far as the disk goes: the backup table is there. + let start = block * u64::from(per); + let sectors = u64::from(per).min(geometry.sectors() - start) as u32; + let len = sectors * geometry.sector_bytes(); + let cdb = Cdb::transfer(false, start as u32, sectors as u16); + let read = self.command(&cdb, Data::In { at, len }).await?; + for s in 0..u64::from(sectors) { let first = (s as usize) * geometry.sector_bytes() as usize; let sector = match read { - Reply::Ok { delivered } if delivered == HOST_BLOCK => Some( + Reply::Ok { delivered } if delivered == len => Some( (0..geometry.sector_bytes() as usize).map(|i| self.dev.read8(self.block + first + i)).collect(), ), _ => None, @@ -762,7 +761,6 @@ fn status_of(sense: Sense) -> u32 { struct Cache { lba_bytes: u32, lbas: u64, - per: NonZeroU64, /// `None` for a sector whose read failed. have: BTreeMap>>, missed: Vec, @@ -775,9 +773,6 @@ impl toyos_gpt::Sectors for Cache { fn lba_count(&self) -> u64 { self.lbas } - fn lba_count_granularity(&self) -> NonZeroU64 { - self.per - } fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { match self.have.get(&lba) { Some(Some(sector)) => { diff --git a/system/usbd/xhci/src/tests.rs b/system/usbd/xhci/src/tests.rs index f7f8c3d4f46..52a93e80f5b 100644 --- a/system/usbd/xhci/src/tests.rs +++ b/system/usbd/xhci/src/tests.rs @@ -493,6 +493,32 @@ fn a_stick_is_offered_once_its_table_is_read_and_the_bus_settles_only_then() { assert_eq!(arrived, Some((0, Some((2048, 8192))))); } +/// A stick whose sector count is not whole 4 KiB blocks keeps its backup +/// table on its true last sector (UEFI 2.10 §5.3.1), past the last whole +/// block: with the primary header torn, the table is still read, from there. +#[test] +fn a_stick_s_backup_past_its_last_whole_block_is_read() { + let sectors = 16384 + 4; + let mut bytes = gpt_disk(sectors, UNIQUE, 2048, 8192); + bytes[512 + 40] ^= 1; + let stick = Stick::new(bytes, "SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + let mut arrived = None; + for step in 1..=60 { + w.run(&usb, &mut tasks, &mut owner, step * 10 * MS).unwrap(); + for event in usb.disks.take_events() { + if let Event::Arrived { disk, table, .. } = event { + arrived = Some((disk, table)); + } + } + } + assert_eq!(arrived, Some((0, Some((2048, 8192)))), "the backup on sector {} was not read", sectors - 1); +} + /// A session's read and write move straight between the stick and the /// client's lent region, and a flush is SYNCHRONIZE CACHE. #[test] diff --git a/tests/common/hostdisk.rs b/tests/common/hostdisk.rs index 5523b9fb8d3..e82da3eb56a 100644 --- a/tests/common/hostdisk.rs +++ b/tests/common/hostdisk.rs @@ -33,19 +33,30 @@ pub fn bytes() -> Vec { let mut entries = Vec::new(); for (i, (name, kind, len)) in PARTS.iter().enumerate() { let unique = Guid::from_fields(0x7057_0000 + i as u32, 0, 0x4000, [0x80, 0, 0, 0, 0, 0, 0, i as u8]); - entries.push(toyos_build::gptwrite::Entry { kind: *kind, unique, first_lba: at / LBA, last_lba: (at + len) / LBA - 1, name }); + entries.push(toyos_gpt::edit::Slot::new(*kind, unique, at / LBA, (at + len) / LBA - 1, name).expect("a short name")); for (sector, block) in disk[at as usize..(at + len) as usize].chunks_mut(LBA as usize).enumerate() { block.fill(((i as u8 + 1).wrapping_mul(0x25) ^ sector as u8) | 1); } at += len; } - let table = toyos_build::gptwrite::table(total / LBA, Guid::from_fields(0x7057_00ff, 0, 0x4000, [0x80; 8]), &entries); - disk[..table.primary.len()].copy_from_slice(&table.primary); - let backup = disk.len() - table.backup.len(); - disk[backup..].copy_from_slice(&table.backup); + let disk_guid = Guid::from_fields(0x7057_00ff, 0, 0x4000, [0x80; 8]); + toyos_build::image::write_gpt(&mut std::io::Cursor::new(&mut disk), total, disk_guid, &entries); disk } +/// Where the disk's ToyOS DATA partition is, in bytes: its first and its +/// length. +pub fn data_span() -> (u64, u64) { + let mut at = EDGE; + for (_, kind, len) in PARTS { + if kind == Guid::TOYOS_DATA { + return (at, len); + } + at += len; + } + unreachable!("the disk carries a ToyOS DATA partition") +} + /// What the nvme driver of `bb:dd.f` said, past its name: `nvme 04:00.0: `. fn said_by_nvme(line: &str) -> Option<&str> { let (_, rest) = line.split_once("nvme ")?; @@ -54,12 +65,26 @@ fn said_by_nvme(line: &str) -> Option<&str> { (at.len() == 7 && at[2] == b':' && at[5] == b'.').then_some(said) } -/// A boot off a stick drove an NVMe disk that is another system's and left -/// it alone: a driver brought a controller up and offered its partitions, it -/// opened at least one session and each on a ToyOS DATA partition, and the file server -/// found that DATA neither ours nor designated, and so neither mounted nor -/// formatted it. Answers how many partitions the disks' tables named. -pub fn left_alone(log: &Serial) -> Result { +/// What a boot's DATA file server made of the bench's ToyOS DATA partition. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Data { + /// No volume of ours and no designation stamp: neither mounted nor + /// formatted, and DATA served absent. + Foreign, + /// A designated partition, formatted off its stamp and served. + Formatted, + /// A volume of ours, mounted and served. + Mounted, +} + +/// A boot off a stick drove an NVMe disk that is another system's and +/// touched nothing on it but its ToyOS DATA partition: a driver brought a +/// controller up and offered its partitions, it opened at least one session +/// and each on a ToyOS DATA partition, and the file server either left that +/// DATA alone, finding it neither ours nor designated, or — once it is +/// designated — formatted or mounted it; never DATA from memory. Answers how +/// many partitions the disks' tables named, and which of those DATA was. +pub fn left_alone(log: &Serial) -> Result<(usize, Data), String> { log.must_say("diskserver: every disk driver has settled")?; let said: Vec<&str> = log.text().lines().filter_map(said_by_nvme).collect(); if !said.iter().any(|s| s.starts_with("up: ")) { @@ -90,8 +115,24 @@ pub fn left_alone(log: &Serial) -> Result { other => return Err(format!("an nvme driver opened a session on {guid}, of type {other:?}, which is no ToyOS DATA")), } } - log.must_say("fileserver: no volume of ours and no designation stamp; nothing will be written to this partition")?; - log.must_not_say("fileserver: mounted the DATA volume")?; - log.must_not_say("designates this partition for ToyOS; formatting it")?; - Ok(kinds.len()) + log.must_not_say("are in memory and will not survive a reboot")?; + let said = |line: &str| log.text().lines().any(|l| l.contains(line)); + let fates = [ + (Data::Foreign, said("fileserver: the DATA partition holds no volume of ours and no designation stamp; DATA is absent this boot")), + (Data::Formatted, said("designates this partition for ToyOS; formatting it")), + (Data::Mounted, said("fileserver: mounted the DATA volume")), + ]; + let fate = match fates.iter().filter(|(_, said)| *said).map(|(fate, _)| *fate).collect::>()[..] { + [one] => one, + ref many => return Err(format!("DATA's file server said {many:?} of foreign, formatted and mounted, and one is its answer")), + }; + if fate == Data::Foreign { + log.must_say("fileserver: no volume of ours and no designation stamp; nothing will be written to this partition")?; + } else { + let serving = log.must_say("fileserver: Data serving ")?; + if !serving.contains(" — bcachefs") { + return Err(format!("DATA is {fate:?} and served by no bcachefs volume: {serving:?}")); + } + } + Ok((kinds.len(), fate)) } diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 5cf6c34fed0..a975d929519 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -44,9 +44,9 @@ pub struct Arm { /// What the image is armed with. Every name here is judged by the /// pre-flash gate (`toyos_metal::FLASHABLE`) before it is written. pub params: &'static [&'static str], - /// Binary names and runner builtins this test needs in the job list, in the - /// order it needs them. `reboot` is appended to every list by the - /// derivation and is never written here. + /// The jobs this test needs in the job list, in the order it needs them: + /// each a binary name or runner builtin and its arguments. `reboot` is + /// appended to every list by the derivation and is never written here. pub jobs: &'static [&'static str], /// The kernel build this boot needs, empty for the one an image ships. /// diff --git a/tests/guest/test-runner/src/main.rs b/tests/guest/test-runner/src/main.rs index 4491020c2d7..c20111e3ec9 100644 --- a/tests/guest/test-runner/src/main.rs +++ b/tests/guest/test-runner/src/main.rs @@ -96,7 +96,8 @@ fn main() { let _ = io::stdout().flush(); // **A machine with no host on the other end runs the jobs its manifest - // names**, one binary name per argument through the stdin path's own [`run_one`]. + // names**, one job per argument — a binary name and its arguments — + // through the stdin path's own [`run_one`]. let mut jobs: Vec = std::env::args().skip(1).collect(); // The one argument that is not a job: what the whole list gets. let mut bound_ms = toyos_tco::JOB_BOUND_MS; @@ -108,13 +109,14 @@ fn main() { if !jobs.is_empty() { deadline(bound_ms); for job in &jobs { - // Fatal by name: nobody is reading this console, so a job that did not run must end the boot. - if job.split_whitespace().count() != 1 { - give_the_machine_back(&format!("job {job:?} is not one binary name")); - } + // A binary name and its arguments. Fatal by name: nobody is + // reading this console, so a job that did not run must end the boot. + let mut words = job.split_whitespace(); + let Some(name) = words.next() else { give_the_machine_back("an empty job") }; + let args: Vec<&str> = words.collect(); *RUNNING.lock().expect("the deadline thread does not panic holding this") = job.clone(); - match run_one(job, &[], cap.as_ref()) { + match run_one(name, &args, cap.as_ref()) { Ran::No => { give_the_machine_back(&format!("job {job:?} did not run")) } diff --git a/tests/images/designatecase.toml b/tests/images/designatecase.toml new file mode 100644 index 00000000000..80298bccd1f --- /dev/null +++ b/tests/images/designatecase.toml @@ -0,0 +1,49 @@ +# The boot that designates a bench's DATA partition from inside ToyOS: its +# runner, the one row authorized to (`authorize = ["designate:data"]`), runs +# `disks designate data --commit` once (`tests/toyos.rs`'s +# `bench_data_designated` row). Its own image, so the authorization reaches no +# boot but the one that is for it; no row here starts another, so no login +# holds it. +[boot] +start = ["logkeeper", "diskserver", "fileserver", "acpiserver", "test-runner"] + +[programs.logkeeper] +service = true +syscap = ["logread"] +serves = ["log"] + +[programs.acpiserver] +service = true +devices = ["acpi"] +receives = ["power"] + +# `power` for the reboot that ends the list. Its jobs inherit its namespace, +# the authorization among it. +[programs.test-runner] +receives = ["power"] +authorize = ["designate:data"] + +[programs.disks] + +[programs.toybox] + +[symlinks] +"bin/reboot" = "/system/bin/toybox" + +[programs.diskserver] +service = true +restart = true +serves = ["block", "disk"] + +[programs.nvme] +drives = ["pci:01:08:02"] +receives = ["disk"] + +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30@boot"] +receives = ["disk"] + +[programs.fileserver] +restart = true +roles = ["data", "log", "boot"] +receives = ["block"] diff --git a/tests/metal/src/image.rs b/tests/metal/src/image.rs index 04ce4992866..ef4c451e195 100644 --- a/tests/metal/src/image.rs +++ b/tests/metal/src/image.rs @@ -5,8 +5,8 @@ //! no serial port has nothing at the other end to type a job — so the //! stdin path every config in `tests/images/` but `jobcase.toml` leaves the runner on parks //! forever, and the loop refuses after `metal::return_secs`. What ends a boot -//! is `[programs.test-runner] args`: the list's bound, one binary name per -//! job, then `reboot`. +//! is `[programs.test-runner] args`: the list's bound, one job per argument +//! — a binary name and its arguments — then `reboot`. //! //! So a metal image is a committed boot config with that one field derived onto //! it. The derivation is here, pure, because what it changes about the config @@ -145,6 +145,7 @@ mod tests { "tests/images/metalcase.toml", "tests/images/latencycase.toml", "tests/images/acpicase.toml", + "tests/images/designatecase.toml", ]; fn derived(config: &str, jobs: &[&str]) -> String { diff --git a/tests/metal/src/internal.rs b/tests/metal/src/internal.rs index d701148e5a2..6a993f42d0e 100644 --- a/tests/metal/src/internal.rs +++ b/tests/metal/src/internal.rs @@ -574,21 +574,12 @@ mod tests { fn image(dir: &Path, name: &str, parts: &[(Guid, Guid)], slots: [Option<(Guid, Guid)>; 2]) -> PathBuf { const ALIGN: u64 = 2048; const TOTAL: u64 = 32 << 20; - let entries: Vec> = (1..) + let entries: Vec = (1..) .zip(parts) - .map(|(i, (kind, unique))| toyos_build::gptwrite::Entry { - kind: *kind, - unique: *unique, - first_lba: i * ALIGN, - last_lba: i * ALIGN + 511, - name: "x", - }) + .map(|(i, (kind, unique))| toyos_gpt::edit::Slot::new(*kind, *unique, i * ALIGN, i * ALIGN + 511, "x").unwrap()) .collect(); - let table = toyos_build::gptwrite::table(TOTAL / u64::from(LBA), toyos_build::gptwrite::random_guid(), &entries); let mut out = vec![0u8; TOTAL as usize]; - out[..table.primary.len()].copy_from_slice(&table.primary); - let back = out.len() - table.backup.len(); - out[back..].copy_from_slice(&table.backup); + toyos_build::image::write_gpt(&mut std::io::Cursor::new(&mut out), TOTAL, toyos_build::image::random_guid(), &entries); let slot = |s: Option<(Guid, Guid)>| s.map(|(b, r)| toyos_update::slots::Slot { boot: b.0, root: r.0, version: 1 }); let slots = toyos_update::slots::Table { sequence: 1, marked: toyos_update::slots::Which::A, slots: slots.map(slot) }; let at = parts.iter().position(|(k, _)| *k == Guid::TOYOS_SLOTS).expect("a slot table") as u64 + 1; @@ -602,7 +593,7 @@ mod tests { #[test] fn an_image_is_admitted_piece_by_piece_by_its_slot_table() { let dir = toyos_tmpdir::TempDir::new("internal"); - let g = |_| toyos_build::gptwrite::random_guid(); + let g = |_| toyos_build::image::random_guid(); let [esp, slots, log, ba, ra, bb, rb, other]: [Guid; 8] = std::array::from_fn(g); let built = [ (Guid::EFI_SYSTEM, esp), (Guid::TOYOS_SLOTS, slots), (Guid::MICROSOFT_BASIC, log), diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index be53c64a004..2926fca80c8 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -1559,7 +1559,7 @@ impl Driver { } let own = |o: Owned| parts[Owned::ALL.iter().position(|x| *x == o).expect("listed")].clone(); let kept = |o: Owned| own(o).unique.expect("recognise refuses a partition with no GUID"); - let named = image.guids(toyos_build::gptwrite::random_guid); + let named = image.guids(toyos_build::image::random_guid); let mut every: Vec = vec![kept(Owned::Esp), kept(Owned::Slots)]; every.extend(named.iter().map(|(_, guid)| *guid)); let holders = self.ssh("listing every partition's GUID", internal::HOLDERS)?; @@ -3586,21 +3586,14 @@ mod tests { fn disk(parts: &[(&str, toyos_gpt::Guid)]) -> Vec { const TOTAL: u64 = 16 * 1024 * 1024; const ALIGN: u64 = 2048; - let entries: Vec> = (1..) + let entries: Vec = (1..) .zip(parts) - .map(|(i, (name, kind))| toyos_build::gptwrite::Entry { - kind: *kind, - unique: toyos_build::gptwrite::random_guid(), - first_lba: i * ALIGN, - last_lba: i * ALIGN + 511, - name, + .map(|(i, (name, kind))| { + toyos_gpt::edit::Slot::new(*kind, toyos_build::image::random_guid(), i * ALIGN, i * ALIGN + 511, name).unwrap() }) .collect(); - let table = toyos_build::gptwrite::table(TOTAL / u64::from(LBA), toyos_build::gptwrite::random_guid(), &entries); let mut out = vec![0u8; TOTAL as usize]; - out[..table.primary.len()].copy_from_slice(&table.primary); - let back = out.len() - table.backup.len(); - out[back..].copy_from_slice(&table.backup); + toyos_build::image::write_gpt(&mut std::io::Cursor::new(&mut out), TOTAL, toyos_build::image::random_guid(), &entries); out } diff --git a/tests/toyos.rs b/tests/toyos.rs index d5173fa98a1..67457a149a2 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -588,6 +588,14 @@ const MACHINE_TESTS: &[&str] = &[ // host test runs together, and the disk is read back whole on the host // after the stop. QEMU x86-64; on a bench it is `nvme_leaves_the_benchs_disk_alone`. "a_host_disk_beside_the_stick_is_left_alone", + // The same disk's DATA designated from inside the guest: the supervisor + // minting the designation image's one authorization, the block service + // resolving and routing it, and the NVMe driver judging, stamping, + // flushing and reading back are four processes and a claim no host build + // runs together; the host reads the disk image after the stop for the + // stamp and for every other byte. QEMU x86-64; on a bench it is + // `bench_data_designated`. + "the_data_partition_beside_the_stick_is_designated", // clang and LLD built for ToyOS, installed by `pkg` and compiling and // linking inside it: the claim is that they install and run on ToyOS, // which no host test can make, and the T14 would read nothing a guest does @@ -1406,12 +1414,26 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: TESTCASES, judge: |b| usbd_serves_the_boot_stick(&b[0].kernel(), &b[0].log()) }, ), ( - // The bench's own NVMe disk, its host's partitions and the owner's - // unformatted ToyOS DATA, driven beside the stick by the row's class - // and left alone (`hostdisk::left_alone`). The T14 and the AMD bench; - // the metal loop's next boot of the host off that disk is the other half. + // The bench's own NVMe disk, driven beside the stick by the row's + // class: nothing on it is touched but its ToyOS DATA partition, which + // is left alone while it is foreign and formatted or mounted once it + // is designated (`hostdisk::left_alone`), and never stood in for by + // memory. The T14 and the AMD bench; the metal loop's next boot of the + // host off that disk is the other half. "nvme_leaves_the_benchs_disk_alone", - metal::Metal { arms: TESTCASES, judge: |b| common::hostdisk::left_alone(&b[0].log()).map(|_| ()) }, + metal::Metal { arms: TESTCASES, judge: |b| common::hostdisk::left_alone(&b[0].log()).map(drop) }, + ), + ( + // The bench's DATA partition designated from inside ToyOS, and the + // next boot's DATA file server formatting it and mounting it: the + // driver of the NVMe disk judges the partition, writes the stamp, + // flushes and reads it back, under the one authorization the + // designation image grants its runner. Destructive and once per + // bench, so named only: the T14 and the AMD bench, whose p4s are the + // owner's empty ToyOS DATA. On the T14 the same boot reads its stick's + // last sector, past its last whole block. + "bench_data_designated", + metal::Metal { arms: DESIGNATE, judge: |b| bench_data_designated(&b[0].log(), &b[1].log()) }, ), ( // The stick written through usbd while the boot deadline resets the @@ -1723,6 +1745,17 @@ const TESTCASES_HELD: &[metal::Arm] = &[metal::Arm { ..metal::once("testcases", "tests/images/testcases.toml", &[], &[]) }]; +/// Two boots of the designation image (`tests/images/designatecase.toml`): +/// the first designates the machine's DATA partition, the second only +/// reboots, which is where the file server formats what the first stamped. +const DESIGNATE: &[metal::Arm] = &[ + metal::Arm { + named_only: true, + ..metal::once("designate", "tests/images/designatecase.toml", &[], &["disks designate data --commit"]) + }, + metal::Arm { named_only: true, ..metal::once("designated", "tests/images/designatecase.toml", &[], &[]) }, +]; + /// Its own boot: `kernel_objects_refused` holds every frame a process may, so /// every other process's allocation is refused while it runs. const TESTCASES_REFUSED: &[metal::Arm] = @@ -4456,6 +4489,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "desktop_without_x2apic" => desktop_without_x2apic(), "nvme_disk_boots" => nvme_disk_boots(test_config), "a_host_disk_beside_the_stick_is_left_alone" => a_host_disk_beside_the_stick_is_left_alone(test_config), + "the_data_partition_beside_the_stick_is_designated" => the_data_partition_beside_the_stick_is_designated(), "hosted_clang_hello" => common::hostedclang::hosted_clang_hello(), "libc_children" => common::libcchildren::libc_children(), "stream_holders" => stream_holders(test_config), @@ -4675,6 +4709,82 @@ fn nvme_disk_boots(test_config: &Path) -> Result<(), String> { Ok(()) } +/// The NVMe driver's own account of the designation, which must be that it +/// wrote it: the line, or why there is none. +fn nvme_wrote_the_designation(text: &str) -> Result<&str, String> { + let said: Vec<&str> = text.lines().filter(|l| l.contains("nvme ") && l.contains(": designation of ")).collect(); + match said[..] { + [one] if one.ends_with(": Written") => Ok(one), + _ => Err(format!("the NVMe driver did not say it wrote one designation: {said:?}")), + } +} + +/// The first boot designated the machine's DATA partition, judged and +/// written by the NVMe driver and read back, through the runner's +/// authorization; the second formatted it and serves it, and neither served +/// DATA from memory. +fn bench_data_designated(first: &serial::Serial, second: &serial::Serial) -> Result<(), String> { + first.must_say("supervisor: test-runner is authorized for `designate:data`")?; + nvme_wrote_the_designation(first.text())?; + first.must_say("the DATA partition is designated: the stamp is on the disk and read back")?; + // The T14's stick carries 7507812 sectors, four past its last whole 4 KiB + // block, and a smaller image than it, so its last sector holds no backup: + // usbd reads it with a short READ(10) and says the copies disagree. A + // read that failed would name the sector. + let usbd = |l: &&str| l.contains("usbd ") && l.contains("two copies do not agree"); + if let Some(failed) = first.text().lines().filter(usbd).find(|l| l.contains("ReadFailed(")) { + return Err(format!("a stick's last sector would not read: {failed:?}")); + } + first.must_not_say("are in memory and will not survive a reboot")?; + second.must_say("fileserver: block 0 designates this partition for ToyOS; formatting it")?; + let serving = second.must_say("fileserver: Data serving ")?; + if !serving.contains(" — bcachefs") { + return Err(format!("DATA is served by no bcachefs volume: {serving:?}")); + } + second.must_not_say("are in memory and will not survive a reboot")?; + second.must_not_say("DATA is absent this boot")?; + Ok(()) +} + +/// A boot of the designation image beside [`common::hostdisk`]'s disk: a +/// judge-only `disks designate data` writes nothing and says it would, the +/// committed one is written by the NVMe driver and read back, and once the +/// machine has stopped the disk's DATA partition begins with the stamp for its +/// size and every other byte of the disk is as it was. +fn the_data_partition_beside_the_stick_is_designated() -> Result<(), String> { + let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/images/designatecase.toml"); + let options = BootOptions { profile: qemu::Profile::MetalBesideAHostDisk, qmp: true, ..Default::default() }; + let mut qemu = QemuInstance::boot_with_options(&config, &[], &[], options); + let mut console = qemu.boot_log().to_string(); + qemu::await_marker(&mut qemu, &mut console, "fileserver: Data serving", "DATA's file server to serve")?; + let judged = qemu.run_test("disks designate data", Duration::from_secs(60)); + if judged.exit_code != Some(0) || !judged.stdout.contains("would be designated; nothing was written") { + return Err(format!("the judge-only designation: {:?}\n{}", judged.exit_code, judged.serial)); + } + let done = qemu.run_test("disks designate data --commit", Duration::from_secs(60)); + if done.exit_code != Some(0) || !done.stdout.contains("the stamp is on the disk and read back") { + return Err(format!("the designation: {:?}\n{}", done.exit_code, done.serial)); + } + eprintln!(" [designate] {}", nvme_wrote_the_designation(&done.serial)?); + qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)).power_button(); + let by = qemu.budget(Duration::from_secs(60)); + console.push_str(&qemu.await_exit(by)?); + let log = serial::Serial::named("the boot", console); + log.must_say("supervisor: test-runner is authorized for `designate:data`")?; + log.must_say("fileserver: the DATA partition holds no volume of ours and no designation stamp; DATA is absent this boot")?; + + let after = std::fs::read(qemu.nvme_image()).map_err(|e| e.to_string())?; + let mut want = common::hostdisk::bytes(); + let (at, len) = common::hostdisk::data_span(); + let at = at as usize; + want[at..at + 4096].copy_from_slice(&bcachefs::designation(len / 4096)); + match want.iter().zip(&after).position(|(w, a)| w != a) { + None if want.len() == after.len() => Ok(()), + None => Err(format!("the disk was {} bytes and is {}", want.len(), after.len())), + Some(byte) => Err(format!("byte {byte} (sector {}) is not what the designation alone leaves", byte / 512)), + } +} + /// `hostdisk::left_alone` of a stick boot beside [`common::hostdisk`]'s disk, /// whose four partitions the driver of QEMU's controller under Intel's id /// named, and every byte of the disk as it was once the machine has stopped. @@ -4689,9 +4799,11 @@ fn a_host_disk_beside_the_stick_is_left_alone(test_config: &Path) -> Result<(), console.push_str(&qemu.await_exit(by)?); let log = serial::Serial::named("the boot", console); log.must_say("vendor=8086 device=5845 prog_if=02")?; + // This disk's DATA is never designated: what it is judged on is that + // nothing writes to a disk that is not ours. match common::hostdisk::left_alone(&log)? { - 4 => {} - named => return Err(format!("the driver named {named} partitions of a disk that has 4")), + (4, common::hostdisk::Data::Foreign) => {} + (named, data) => return Err(format!("the driver named {named} partitions of a disk that has 4, and DATA was {data:?}")), } let after = std::fs::read(qemu.nvme_image()).map_err(|e| e.to_string())?; let before = common::hostdisk::bytes();