From e0d1976c7d1ff8bf6544cb29cfc7ef5f2c60923e Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 16:54:33 +0200 Subject: [PATCH 1/6] toyos-gpt: a Partition is only what parsing proved, and a bent table is refused A USB stick whose CRC-valid GPT states an entry with first_lba > last_lba panicked the kernel on the next SYS_DEVICE_INVENTORY: `list` handed the entry back as a `Partition` with public fields, and `lba_count` underflowed (audit B1). - `Partition` has private fields and one constructor, `place`, which proves first_usable <= first <= last <= last_usable (< the device's block count) and computes `lba_count` once, as a `NonZeroU64`. `Located` is private too, so the overlap proof travels with it. - `list` and `locate_type` hand back each entry of the type as `Result`: every consumer sees what the table states and has to handle the entry that is no partition. The kernel logs it and lists it nowhere; blockd refuses it as Unusable, as before. - The domain lint line forbids arithmetic side effects, indexing, unwrap, expect, panic and `as` in the crate; the CRC goes bit at a time, since a table lookup is an index. - tests/mutate.rs bends header and entry values of seeded layouts and reseals both CRCs; tests/oracle.rs reads every table against the `gpt` crate. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 3 + bootloader/src/rootimage.rs | 29 +- kernel/src/gpt.rs | 101 ++++--- kernel/src/inventory.rs | 10 +- src/image.rs | 113 +++---- src/metal.rs | 27 +- tests/common/partclaim.rs | 18 +- tests/common/volumes.rs | 49 +-- toyos-gpt/Cargo.toml | 4 + toyos-gpt/src/crc32.rs | 28 +- toyos-gpt/src/guid.rs | 9 +- toyos-gpt/src/lib.rs | 567 +++++++++++++++++++++++------------ toyos-gpt/tests/mutate.rs | 232 ++++++++++++++ toyos-gpt/tests/oracle.rs | 199 ++++++++++++ toyos-gpt/tests/parse.rs | 76 ++--- toyos-gpt/tests/table/mod.rs | 400 ++++++++++++++++++++++++ userland/blockd/src/main.rs | 45 ++- 17 files changed, 1422 insertions(+), 488 deletions(-) create mode 100644 toyos-gpt/tests/mutate.rs create mode 100644 toyos-gpt/tests/oracle.rs create mode 100644 toyos-gpt/tests/table/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 802affd915..600ac99693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,6 +1290,9 @@ version = "0.1.0" [[package]] name = "toyos-gpt" version = "0.1.0" +dependencies = [ + "gpt", +] [[package]] name = "toyos-hda" diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index d5d0be69ff..efd5725f79 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -180,27 +180,30 @@ impl<'a> Disk<'a> { /// `toyos_gpt::locate` checks every one. pub fn locate(&mut self, guid: [u8; 16]) -> Result { toyos_gpt::locate(self, Guid(guid)) - .map(|located| located.partition) + .map(|located| located.partition()) .map_err(|e| alloc::format!("partition {} on the boot disk: {e:?}", Guid(guid))) } /// The slot table on this disk's one TOYOS-SLOTS partition. pub fn slot_table(&mut self) -> Result { - let blank = Partition { index: 0, type_guid: Guid::ZERO, unique_guid: Guid::ZERO, first_lba: 0, last_lba: 0 }; - let mut found = [blank; 2]; + let mut found = [None; 2]; let scan = toyos_gpt::locate_type(self, Guid::TOYOS_SLOTS, &mut found) .map_err(|e| alloc::format!("the boot disk's partition table: {e:?}"))?; - if scan.matched != 1 { - return Err(alloc::format!("the boot disk carries {} slot tables, and a machine has one", scan.matched)); - } - let part = self.locate(found[0].unique_guid.0)?; + let listed = match (scan.matched, found[0]) { + (1, Some(Ok(listed))) => listed, + (1, Some(Err(unplaced))) => { + return Err(alloc::format!("the boot disk's slot table entry is no partition on it: {unplaced:?}")); + } + (n, _) => return Err(alloc::format!("the boot disk carries {n} slot tables, and a machine has one")), + }; + let part = self.locate(listed.unique_guid().0)?; let lbas = BLOCK as u64 / u64::from(self.lba_bytes); - if part.lba_count() < slots::COPIES * lbas { + if part.lba_count().get() < slots::COPIES * lbas { return Err(alloc::format!("the slot table's partition is {} blocks, short of its two copies", part.lba_count())); } let mut copies = [[0u8; BLOCK]; 2]; for (i, copy) in copies.iter_mut().enumerate() { - let at = part.first_lba + i as u64 * lbas; + let at = part.first_lba() + i as u64 * lbas; self.io .read_blocks(self.media_id, at, self.scratch) .map_err(|e| alloc::format!("the slot table's copy {i} would not read: {:?}", e.status()))?; @@ -221,7 +224,7 @@ impl<'a> Disk<'a> { /// stopped is the slot's line before the read and the attempt count the /// next pass reads. pub fn read_root(&mut self, bs: &BootServices, part: &Partition, len: u64) -> Result { - let capacity = part.lba_count().saturating_mul(u64::from(self.lba_bytes)); + let capacity = part.lba_count().get().saturating_mul(u64::from(self.lba_bytes)); if len > capacity || !len.is_multiple_of(BLOCK as u64) { return Err(alloc::format!("{len} bytes of ROOT do not fit whole in its {capacity}-byte partition")); } @@ -241,8 +244,8 @@ impl<'a> Disk<'a> { let chunk = chunk::chunk_bytes(CHUNK_BOUND, BLOCK, self.lba_bytes, granularity.unwrap_or(0)); let began = crate::tsc(); let mut device = Firmware { io: &self.io, media_id: self.media_id }; - let read = chunk::read(&mut device, part.first_lba, self.lba_bytes, chunk, into); - let image = RootImage { at, len, partition: part.unique_guid.0, cycles: crate::tsc().wrapping_sub(began) }; + let read = chunk::read(&mut device, part.first_lba(), self.lba_bytes, chunk, into); + let image = RootImage { at, len, partition: part.unique_guid().0, cycles: crate::tsc().wrapping_sub(began) }; if let Err(failed) = read { let why = alloc::format!( "the read of {} blocks at LBA {} failed: {:?}, after {} of {len} bytes read", @@ -256,7 +259,7 @@ impl<'a> Disk<'a> { } println!( "{READ_AT} {at:#x}+{len:#x} from LBA {}+{}, {chunk} bytes a request (optimal granularity: {}), in {} TSC cycles", - part.first_lba, + part.first_lba(), len / u64::from(self.lba_bytes), match granularity { Some(lbas) => alloc::format!("{lbas} block(s)"), diff --git a/kernel/src/gpt.rs b/kernel/src/gpt.rs index a83625d0b2..f564a676eb 100644 --- a/kernel/src/gpt.rs +++ b/kernel/src/gpt.rs @@ -95,7 +95,7 @@ pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { let mut out = Vec::new(); for (handle, lba_bytes, parts) in listed { for part in parts { - let holder = match crate::block::span_blocks(part.first_lba, part.lba_count(), lba_bytes) { + let holder = match crate::block::span_blocks(part.first_lba(), part.lba_count().get(), lba_bytes) { Ok((first_block, blocks)) => handle.holder(first_block, first_block + blocks), Err(_) => None, }; @@ -108,7 +108,7 @@ pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { /// List `handle`'s table into [`LISTED`], once per disk. fn list(sectors: &mut DeviceSectors<'_>, handle: &Handle, lba_bytes: u32) { let id = handle.device_id(); - let mut found = alloc::vec![BLANK; MAX_LISTED]; + let mut found = alloc::vec![None; MAX_LISTED]; // A disk with no table this kernel parses carries no partition, and // `collect` says so, naming the refusal. let Ok(scan) = toyos_gpt::list(sectors, &mut found) else { return }; @@ -119,8 +119,21 @@ fn list(sectors: &mut DeviceSectors<'_>, handle: &Handle, lba_bytes: u32) { scan.listed ); } - found.truncate(scan.listed); - LISTED.lock().push(Listed { handle: handle.clone(), lba_bytes, parts: found }); + let mut parts = Vec::new(); + for entry in found.into_iter().flatten() { + match entry { + Ok(part) => parts.push(part), + Err(unplaced) => log!( + "gpt: device {id} states entry {} ({}) at LBA {}..={}, whose blocks are no \ + partition on it, and the inventory does not list it", + unplaced.index, + unplaced.unique_guid, + unplaced.first, + unplaced.last + ), + } + } + LISTED.lock().push(Listed { handle: handle.clone(), lba_bytes, parts }); } /// How many partitions of one ToyOS type one device may offer this kernel. @@ -216,13 +229,13 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { }; // Firmware's and the table's accounts must agree; a mismatch refuses, never repairs. - let part = found.partition; - if part.first_lba != firmware.start_lba || part.lba_count() != firmware.blocks { + let part = found.partition(); + if part.first_lba() != firmware.start_lba || part.lba_count().get() != firmware.blocks { log!( "gpt: device {id} puts {} at LBA {}+{} but firmware said {}+{} — not treating it as \ the boot volume", - part.unique_guid, - part.first_lba, + part.unique_guid(), + part.first_lba(), part.lba_count(), firmware.start_lba, firmware.blocks @@ -233,8 +246,8 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { let volume = Volume { device: id, lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }; let mut resolved = RESOLVED.lock(); @@ -248,9 +261,9 @@ pub fn probe(handle: &Handle, lba_bytes: u32) { volume.start_lba, volume.blocks, lba_bytes, - part.index, - found.used_entries, - found.disk_guid, + part.index(), + found.used_entries(), + found.disk_guid(), if part.is_efi_system() { "" } else { " — and its type is not ESP" } ); *resolved = Resolution::Found { boot: volume, log }; @@ -283,7 +296,7 @@ fn collect( ty: Guid, into: &Lock>, ) { - let mut found = [BLANK; MAX_PER_DEVICE]; + let mut found = [None; MAX_PER_DEVICE]; let scan = match toyos_gpt::locate_type(sectors, ty, &mut found) { Ok(scan) => scan, Err(e) => { @@ -298,31 +311,44 @@ fn collect( scan.listed ); } - for candidate in &found[..scan.listed] { - let checked = match toyos_gpt::locate(sectors, candidate.unique_guid) { - Ok(located) => located.partition, + for entry in found.iter().flatten() { + let candidate = match entry { + Ok(candidate) => candidate, + Err(unplaced) => { + log!( + "gpt: device {id} names a {what} {} at LBA {}..={}, whose blocks are no \ + partition on it", + unplaced.unique_guid, + unplaced.first, + unplaced.last + ); + continue; + } + }; + let checked = match toyos_gpt::locate(sectors, candidate.unique_guid()) { + Ok(located) => located.partition(), Err(e) => { log!( "gpt: device {id} names a {what} {} its own table then refuses: {e:?}", - candidate.unique_guid + candidate.unique_guid() ); continue; } }; log!( "gpt: device {id} carries the {what} candidate {} at LBA {}+{}", - checked.unique_guid, - checked.first_lba, + checked.unique_guid(), + checked.first_lba(), checked.lba_count() ); into.lock().push(Candidate { volume: Volume { device: id, lba_bytes, - start_lba: checked.first_lba, - blocks: checked.lba_count(), + start_lba: checked.first_lba(), + blocks: checked.lba_count().get(), }, - guid: checked.unique_guid, + guid: checked.unique_guid(), }); } } @@ -410,7 +436,7 @@ pub fn seek(guid: PartGuid) -> Sought { for (handle, lba_bytes) in &disks { let id = handle.device_id(); let part = match toyos_gpt::locate(&mut DeviceSectors::new(handle, *lba_bytes), target) { - Ok(located) => located.partition, + Ok(located) => located.partition(), Err(e) => { match table_refused(id, target, e) { Ok(Unread::Lacks) => {} @@ -432,10 +458,10 @@ pub fn seek(guid: PartGuid) -> Sought { volume: Volume { device: id, lba_bytes: *lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }, - unique: part.unique_guid, + unique: part.unique_guid(), }); } Sought { found: Ok(found), silent } @@ -486,33 +512,24 @@ fn table_refused(id: DeviceId, target: Guid, e: GptError) -> Result, id: DeviceId, lba_bytes: u32) -> Option { let target = LOG_GUID.lock().expect("gpt::init runs before any device is probed"); match toyos_gpt::locate(sectors, target) { Ok(found) => { - let part = found.partition; + let part = found.partition(); log!( "gpt: device {id} carries the log partition {target} at LBA {}+{}, entry {} of {}", - part.first_lba, + part.first_lba(), part.lba_count(), - part.index, - found.used_entries + part.index(), + found.used_entries() ); Some(Volume { device: id, lba_bytes, - start_lba: part.first_lba, - blocks: part.lba_count(), + start_lba: part.first_lba(), + blocks: part.lba_count().get(), }) } Err(e) => { diff --git a/kernel/src/inventory.rs b/kernel/src/inventory.rs index 00d023f72f..a7f8ed5c8c 100644 --- a/kernel/src/inventory.rs +++ b/kernel/src/inventory.rs @@ -33,11 +33,11 @@ pub fn collect() -> Vec { for (device, part, holder) in crate::gpt::inventory() { out.push(Record::Partition(Partition { device, - index: part.index, - type_guid: part.type_guid.0, - unique_guid: part.unique_guid.0, - first_lba: part.first_lba, - lbas: part.lba_count(), + index: part.index(), + type_guid: part.type_guid().0, + unique_guid: part.unique_guid().0, + first_lba: part.first_lba(), + lbas: part.lba_count().get(), state: match holder { None => PartState::Free, Some(crate::block::Holder::Kernel(_)) => PartState::Kernel, diff --git a/src/image.rs b/src/image.rs index e2471bc1e8..0c5cc8b6ef 100644 --- a/src/image.rs +++ b/src/image.rs @@ -341,18 +341,23 @@ pub fn slot_table_of(file: &mut std::fs::File) -> Result Result<(u64, u64), String> { - let mut out = [BLANK_PARTITION; 16]; + let mut out = [None; 16]; let scan = toyos_gpt::list(&mut FileSectors(file), &mut out) .map_err(|e| format!("no readable partition table: {e:?}"))?; - let found: Vec<&toyos_gpt::Partition> = - out[..scan.listed].iter().filter(|p| p.unique_guid == toyos_gpt::Guid(guid)).collect(); + let unique = |entry: &toyos_gpt::Entry| match entry { + Ok(p) => p.unique_guid(), + Err(unplaced) => unplaced.unique_guid, + }; + let found: Vec<&toyos_gpt::Entry> = + out.iter().flatten().filter(|entry| unique(entry) == toyos_gpt::Guid(guid)).collect(); match found[..] { - [p] if scan.listed == scan.matched as usize => { - Ok((p.first_lba * u64::from(LBA), p.lba_count() * u64::from(LBA))) + [Ok(p)] if scan.listed == scan.matched as usize => { + Ok((p.first_lba() * u64::from(LBA), p.lba_count().get() * u64::from(LBA))) + } + [Err(unplaced)] => { + Err(format!("partition {}: its blocks are no partition: {unplaced:?}", toyos_gpt::Guid(guid))) } _ => Err(format!( "partition {}: the table states it {} time(s) among {} entries", @@ -473,12 +478,18 @@ pub fn restage_table(path: &Path, edit: impl FnOnce(&mut toyos_update::slots::Ta /// The unique GUID of the one partition of type `kind` on the disk image /// `file`, as a GPT entry stores it. pub fn unique_guid_of(file: &mut std::fs::File, kind: toyos_gpt::Guid) -> Result<[u8; 16], String> { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut FileSectors(file), kind, &mut out) + only_partition(&mut FileSectors(file), kind).map(|part| part.unique_guid().0) +} + +/// The one partition of type `kind` on `disk`. +pub fn only_partition(disk: &mut dyn toyos_gpt::Sectors, kind: toyos_gpt::Guid) -> Result { + let mut out = [None; 2]; + let scan = toyos_gpt::locate_type(disk, kind, &mut out) .map_err(|e| format!("no readable partition table: {e:?}"))?; - match scan.matched { - 1 => Ok(out[0].unique_guid.0), - n => Err(format!("{n} partitions of type {kind}, and this asks for one")), + match (scan.matched, out[0]) { + (1, Some(Ok(part))) => Ok(part), + (1, Some(Err(unplaced))) => Err(format!("the one entry of type {kind} is no partition: {unplaced:?}")), + (n, _) => Err(format!("{n} partitions of type {kind}, where one is owed")), } } @@ -520,14 +531,9 @@ pub fn overwrite_file_on(path: &Path, guid: [u8; 16], name: &str, bytes: &[u8]) /// The slot table on the disk image `file`, which copy is current, and where /// its partition starts. fn table_on(file: &mut std::fs::File) -> Result<(toyos_update::slots::Table, usize, u64), String> { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut FileSectors(file), toyos_gpt::Guid::TOYOS_SLOTS, &mut out) - .map_err(|e| format!("no readable partition table: {e:?}"))?; - if scan.matched != 1 { - return Err(format!("{} slot tables, and a boot image has one", scan.matched)); - } + let part = only_partition(&mut FileSectors(file), toyos_gpt::Guid::TOYOS_SLOTS)?; let mut copies = [[0u8; toyos_update::slots::BLOCK]; 2]; - let at = out[0].first_lba * u64::from(LBA); + let at = part.first_lba() * u64::from(LBA); for (i, copy) in copies.iter_mut().enumerate() { file.seek(SeekFrom::Start(at + (i * toyos_update::slots::BLOCK) as u64)) .and_then(|_| file.read_exact(copy)) @@ -903,25 +909,11 @@ fn designation(blocks: u64) -> [u8; SECTOR] { pub fn data_partition_of(path: &Path) -> Result<(u64, u64), String> { let mut file = std::fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?; - let mut out = [BLANK_PARTITION; 2]; - let scan = - toyos_gpt::locate_type(&mut FileSectors(&mut file), toyos_gpt::Guid::TOYOS_DATA, &mut out) - .map_err(|e| format!("{} has no readable partition table: {e:?}", path.display()))?; - if scan.matched != 1 { - return Err(format!("{} carries {} TOYOS-DATA partitions", path.display(), scan.matched)); - } - Ok((out[0].first_lba * u64::from(LBA), out[0].lba_count() * u64::from(LBA))) + let part = only_partition(&mut FileSectors(&mut file), toyos_gpt::Guid::TOYOS_DATA) + .map_err(|why| format!("{}: {why}", path.display()))?; + Ok((part.first_lba() * u64::from(LBA), part.lba_count().get() * u64::from(LBA))) } -/// A slot [`toyos_gpt::locate_type`] has not filled in. -const BLANK_PARTITION: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, -}; - /// A disk file as logical blocks, for a reader that may not hold the image. pub(crate) struct FileSectors<'a>(pub(crate) &'a mut std::fs::File); @@ -1122,8 +1114,8 @@ fn certify(disk: &[u8], parts: &[(&str, toyos_gpt::Guid, Volume)]) -> Result<(), for (what, guid, kind) in parts { let located = toyos_gpt::locate(&mut ImageSectors(disk), *guid) .map_err(|e| format!("toyos-gpt cannot find the {what} ({guid}) on this image: {e:?}"))?; - let at = located.partition.first_lba as usize * LBA as usize; - let bytes = located.partition.lba_count() as usize * LBA as usize; + let at = located.partition().first_lba() as usize * LBA as usize; + let bytes = located.partition().lba_count().get() as usize * LBA as usize; let volume = disk .get(at..at + bytes) .ok_or_else(|| format!("the {what} runs to byte {} of a {}-byte image", at + bytes, disk.len()))?; @@ -1232,11 +1224,7 @@ mod tests { /// The one partition of `kind` on `disk`. fn only(disk: &[u8], kind: toyos_gpt::Guid) -> toyos_gpt::Guid { - let mut out = [BLANK_PARTITION; 2]; - let scan = toyos_gpt::locate_type(&mut ImageSectors(disk), kind, &mut out) - .expect("the image has a partition table"); - assert_eq!(scan.matched, 1, "the image carries {} partitions of type {kind}", scan.matched); - out[0].unique_guid + only_partition(&mut ImageSectors(disk), kind).expect("the image carries one").unique_guid() } /// Publishing a flash target runs every reader over the assembled image, and @@ -1262,8 +1250,8 @@ mod tests { let start_of = |guid| { toyos_gpt::locate(&mut ImageSectors(&disk), guid) .expect("the partition is on the image") - .partition - .first_lba as usize + .partition() + .first_lba() as usize * LBA as usize }; @@ -1495,24 +1483,10 @@ mod tests { // Located by *type*, through the parser the kernel uses, at the offset // the table gives — never at the one the writer computed. - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT, &mut out) - .expect("the image this build wrote has a partition table"); - assert_eq!( - (scan.matched, scan.listed), - (1, 1), - "a boot image carries exactly one TOYOS-ROOT partition; this one has {}", - scan.matched - ); - let at = out[0].first_lba as usize * LBA as usize; - let bytes = out[0].lba_count() as usize * LBA as usize; + let part = only_partition(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT) + .expect("a boot image carries exactly one TOYOS-ROOT partition"); + let at = part.first_lba() as usize * LBA as usize; + let bytes = part.lba_count().get() as usize * LBA as usize; let volume = disk[at..at + bytes].to_vec(); let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(VecBlockIO::from_vec(volume)) @@ -1599,13 +1573,10 @@ mod tests { None, ); - let mut out = [BLANK_PARTITION; 2]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT, &mut out) - .expect("the image this build wrote has a partition table"); - assert_eq!(scan.matched, 1, "a boot image carries exactly one TOYOS-ROOT partition"); - let at = out[0].first_lba as usize * LBA as usize; - let bytes = out[0].lba_count() as usize * LBA as usize; + let part = only_partition(&mut ImageSectors(&disk), toyos_gpt::Guid::TOYOS_ROOT) + .expect("a boot image carries exactly one TOYOS-ROOT partition"); + let at = part.first_lba() as usize * LBA as usize; + let bytes = part.lba_count().get() as usize * LBA as usize; let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(VecBlockIO::from_vec( disk[at..at + bytes].to_vec(), )) diff --git a/src/metal.rs b/src/metal.rs index b6c5e539df..c2c21c9214 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -954,24 +954,21 @@ fn one_partition( guid: toyos_gpt::Guid, what: &'static str, ) -> Result { - const BLANK: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; - let mut out = [BLANK; 2]; + let mut out = [None; 2]; let scan = toyos_gpt::locate_type(&mut crate::image::FileSectors(file), guid, &mut out) .map_err(|e| Refusal::Table(format!("{e:?}")))?; - if scan.matched != 1 { - return Err(Refusal::Partitions { what, matched: scan.matched }); - } + let part = match (scan.matched, out[0]) { + (1, Some(Ok(part))) => part, + (1, Some(Err(unplaced))) => { + return Err(Refusal::Table(format!("the {what} entry is no partition: {unplaced:?}"))) + } + (matched, _) => return Err(Refusal::Partitions { what, matched }), + }; Ok(Part { - index: out[0].index + 1, - start: out[0].first_lba, - sectors: out[0].lba_count(), - guid: out[0].unique_guid, + index: part.index() + 1, + start: part.first_lba(), + sectors: part.lba_count().get(), + guid: part.unique_guid(), }) } diff --git a/tests/common/partclaim.rs b/tests/common/partclaim.rs index e613a46a8f..6ffcd9f1f3 100644 --- a/tests/common/partclaim.rs +++ b/tests/common/partclaim.rs @@ -541,27 +541,15 @@ fn boot_stick_guids(image: &Path) -> Result<[String; 3], String> { // ROOT's type is read with the kernel's parser: the `gpt` crate answers the // all-zero GUID for a type its own table does not name. let bytes = std::fs::read(image).map_err(|e| format!("read the boot image: {e}"))?; - let blank = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; - let mut found = [blank; 2]; - let scan = toyos_gpt::locate_type( + let root = toyos_build::image::only_partition( &mut super::volumes::ImageSectors { bytes: &bytes }, toyos_gpt::Guid::TOYOS_ROOT, - &mut found, ) - .map_err(|e| format!("the boot image's table: {e:?}"))?; - if scan.matched != 1 { - return Err(format!("the boot image has {} of ROOT, expected one", scan.matched)); - } + .map_err(|why| format!("the boot image's ROOT: {why}"))?; Ok([ one(gpt::partition_types::EFI.guid, "ESP")?, one(gpt::partition_types::BASIC.guid, "log partition")?, - found[0].unique_guid.to_string(), + root.unique_guid().to_string(), ]) } diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index c07eafeaa7..8bc6ae01ce 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -2461,22 +2461,15 @@ pub fn log_partition_layout( (toyos_gpt::Guid::TOYOS_SLOTS, toyos_gpt::Guid::TOYOS_SLOTS_TEXT, "the slot table", slots), (toyos_gpt::Guid::TOYOS_BOOT, toyos_gpt::Guid::TOYOS_BOOT_TEXT, "slot A's volume", volume), ] { - let mut found = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = toyos_gpt::locate_type(&mut ImageSectors { bytes: &image }, kind, &mut found) - .map_err(|e| format!("the kernel's own GPT parser cannot read this table: {e:?}"))?; - if (scan.matched, scan.listed) != (1, 1) { - return Err(format!("the built image carries {} partitions typed {text}, wanted one", scan.matched)); - } - if found[0].first_lba != entry.first_lba || found[0].last_lba != entry.last_lba { + let found = toyos_build::image::only_partition(&mut ImageSectors { bytes: &image }, kind) + .map_err(|why| format!("the kernel's own GPT parser, on the partitions typed {text}: {why}"))?; + if found.first_lba() != entry.first_lba || found.last_lba() != entry.last_lba { return Err(format!( "the kernel's parser puts {what} at LBA {}..{} and the table says {}..{}", - found[0].first_lba, found[0].last_lba, entry.first_lba, entry.last_lba + found.first_lba(), + found.last_lba(), + entry.first_lba, + entry.last_lba )); } } @@ -2555,12 +2548,14 @@ pub fn log_partition_layout( // given the GUID exactly as the file carries it. let located = toyos_gpt::locate(&mut ImageSectors { bytes: &image }, toyos_gpt::Guid(named)) .map_err(|e| format!("the kernel's own GPT parser cannot find the log partition: {e:?}"))?; - if located.partition.first_lba != log.first_lba - || located.partition.last_lba != log.last_lba - { + let found = located.partition(); + if found.first_lba() != log.first_lba || found.last_lba() != log.last_lba { return Err(format!( "the kernel's parser puts the log partition at LBA {}..{} and the table says {}..{}", - located.partition.first_lba, located.partition.last_lba, log.first_lba, log.last_lba + found.first_lba(), + found.last_lba(), + log.first_lba, + log.last_lba )); } @@ -3092,21 +3087,9 @@ pub fn log_flush_retry( /// Where ROOT is on `image`, found by the parser the kernel uses. fn root_extent(image: &[u8]) -> Result<(usize, usize), String> { - let mut found = [toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid::ZERO, - unique_guid: toyos_gpt::Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; - let scan = - toyos_gpt::locate_type(&mut ImageSectors { bytes: image }, toyos_gpt::Guid::TOYOS_ROOT, &mut found) - .map_err(|e| format!("this image has no readable partition table: {e:?}"))?; - if (scan.matched, scan.listed) != (1, 1) { - return Err(format!("this image carries {} ROOT partitions, wanted one", scan.matched)); - } - let at = found[0].first_lba as usize * 512; - Ok((at, found[0].lba_count() as usize * 512)) + let root = toyos_build::image::only_partition(&mut ImageSectors { bytes: image }, toyos_gpt::Guid::TOYOS_ROOT) + .map_err(|why| format!("this image's ROOT: {why}"))?; + Ok((root.first_lba() as usize * 512, root.lba_count().get() as usize * 512)) } /// A second USB disk carrying a copy of `image`, `mutate`d after the copy. diff --git a/toyos-gpt/Cargo.toml b/toyos-gpt/Cargo.toml index 342a3f8586..e2b9d2199d 100644 --- a/toyos-gpt/Cargo.toml +++ b/toyos-gpt/Cargo.toml @@ -9,3 +9,7 @@ name = "toyos-gpt" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" + +[dev-dependencies] +# The differential oracle of `tests/oracle.rs`. +gpt = "3.1.0" diff --git a/toyos-gpt/src/crc32.rs b/toyos-gpt/src/crc32.rs index 1bbf20b21d..ddbcff43c0 100644 --- a/toyos-gpt/src/crc32.rs +++ b/toyos-gpt/src/crc32.rs @@ -6,25 +6,7 @@ //! every table on earth. The two implementations look identical apart from one //! constant, which is exactly why this file says so. -const TABLE: [u32; 256] = { - let mut table = [0u32; 256]; - let mut i = 0u32; - while i < 256 { - let mut crc = i; - let mut j = 0; - while j < 8 { - if crc & 1 != 0 { - crc = (crc >> 1) ^ 0xEDB8_8320; - } else { - crc >>= 1; - } - j += 1; - } - table[i as usize] = crc; - i += 1; - } - table -}; +const POLY: u32 = 0xEDB8_8320; /// A CRC-32 taken over pieces. The partition entry array is read a block at a /// time and never held whole, and the header's own CRC is taken over the @@ -39,8 +21,12 @@ impl Crc32 { pub fn update(&mut self, data: &[u8]) { for &byte in data { - let idx = ((self.0 ^ byte as u32) & 0xFF) as usize; - self.0 = (self.0 >> 8) ^ TABLE[idx]; + let mut crc = self.0 ^ u32::from(byte); + for _ in 0..8 { + // `POLY` where the bit shifted out is set, zero where it is not. + crc = crc.wrapping_shr(1) ^ (POLY & (crc & 1).wrapping_neg()); + } + self.0 = crc; } } diff --git a/toyos-gpt/src/guid.rs b/toyos-gpt/src/guid.rs index 7c1ec1bd36..0564ef80de 100644 --- a/toyos-gpt/src/guid.rs +++ b/toyos-gpt/src/guid.rs @@ -100,14 +100,7 @@ impl Guid { } pub const fn is_zero(&self) -> bool { - let mut i = 0; - while i < 16 { - if self.0[i] != 0 { - return false; - } - i += 1; - } - true + u128::from_ne_bytes(self.0) == 0 } } diff --git a/toyos-gpt/src/lib.rs b/toyos-gpt/src/lib.rs index 7e06472739..36df637a8f 100644 --- a/toyos-gpt/src/lib.rs +++ b/toyos-gpt/src/lib.rs @@ -21,10 +21,23 @@ #![no_std] #![forbid(unsafe_code)] +#![cfg_attr( + not(test), + forbid( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::as_conversions + ) +)] mod crc32; mod guid; +use core::num::NonZeroU64; + pub use crc32::{crc32, Crc32}; pub use guid::Guid; @@ -69,7 +82,7 @@ pub trait Sectors { 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) -> core::num::NonZeroU64; + 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; @@ -157,18 +170,67 @@ impl GptError { /// One partition entry, after it has been checked against the disk it is on. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Partition { - /// Position in the entry array, from 0. - pub index: u32, - pub type_guid: Guid, - pub unique_guid: Guid, - pub first_lba: u64, - /// Inclusive, as GPT stores it. - pub last_lba: u64, + index: u32, + type_guid: Guid, + unique_guid: Guid, + first_lba: u64, + last_lba: u64, + lba_count: NonZeroU64, } impl Partition { - pub const fn lba_count(&self) -> u64 { - self.last_lba - self.first_lba + 1 + /// `stated`, if its blocks are a partition inside `header`'s usable range. + fn place(stated: &Stated, header: &Header) -> Result { + let unplaced = Unplaced { + index: stated.index, + type_guid: stated.type_guid, + unique_guid: stated.unique_guid, + first: stated.first, + last: stated.last, + }; + if stated.first < header.first_usable_lba || stated.last > header.last_usable_lba { + return Err(unplaced); + } + let lba_count = stated + .last + .checked_sub(stated.first) + .and_then(|span| span.checked_add(1)) + .and_then(NonZeroU64::new) + .ok_or(unplaced)?; + Ok(Self { + index: stated.index, + type_guid: stated.type_guid, + unique_guid: stated.unique_guid, + first_lba: stated.first, + last_lba: stated.last, + lba_count, + }) + } + + /// Position in the entry array, from 0. + pub const fn index(&self) -> u32 { + self.index + } + + pub const fn type_guid(&self) -> Guid { + self.type_guid + } + + pub const fn unique_guid(&self) -> Guid { + self.unique_guid + } + + pub const fn first_lba(&self) -> u64 { + self.first_lba + } + + /// Inclusive, as GPT stores it. + pub const fn last_lba(&self) -> u64 { + self.last_lba + } + + pub const fn lba_count(&self) -> NonZeroU64 { + self.lba_count } /// Whether this is an ESP *by type*. A sanity check for a log line and @@ -178,18 +240,47 @@ impl Partition { } } +/// An entry that exists and whose blocks are no partition on its disk. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Unplaced { + pub index: u32, + pub type_guid: Guid, + pub unique_guid: Guid, + pub first: u64, + pub last: u64, +} + +/// One used entry a scan hands back. +pub type Entry = Result; + /// A located partition plus what the table around it looked like. /// +/// Made only by [`locate`]: no other entry of its table claims its blocks. +/// /// The extra fields are not decoration: a log line saying "this disk has three /// partitions and none of them is ours" is a different diagnostic from "this /// disk has no partition table", and on a machine with no serial port the /// difference is the whole debugging session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Located { - pub partition: Partition, - pub disk_guid: Guid, + partition: Partition, + disk_guid: Guid, + used_entries: u32, +} + +impl Located { + pub const fn partition(&self) -> Partition { + self.partition + } + + pub const fn disk_guid(&self) -> Guid { + self.disk_guid + } + /// Entries with a non-zero type GUID, i.e. partitions that exist. - pub used_entries: u32, + pub const fn used_entries(&self) -> u32 { + self.used_entries + } } /// The primary GPT header, once every field in it has been checked. @@ -199,11 +290,28 @@ struct Header { first_usable_lba: u64, last_usable_lba: u64, entry_array_lba: u64, + /// One past the array's last block, which the header check proved + /// representable. + entry_array_end: u64, entry_count: u32, entry_bytes: u32, + /// `entry_bytes`, which the header check proved at least 128 and a + /// divisor of the block. + entry_len: usize, entry_array_crc: u32, } +/// One used entry as the table states it, before anything is proven of its +/// blocks. +#[derive(Debug, Clone, Copy)] +struct Stated { + index: u32, + type_guid: Guid, + unique_guid: Guid, + first: u64, + last: u64, +} + /// Find the partition carrying `target` on `dev`. /// /// Reads only. The order is not negotiable: the protective MBR, then the @@ -229,43 +337,38 @@ pub fn locate(dev: &mut dyn Sectors, target: Guid) -> Result match locate_at(dev, 1, target, &disk) { Ok(located) => Ok(located), Err(primary_err) if primary_err.primary_never_checked_out() => { - locate_at(dev, disk.lba_count - 1, target, &disk).or(Err(primary_err)) + locate_at(dev, disk.backup_header_lba, target, &disk).or(Err(primary_err)) } Err(primary_err) => Err(primary_err), } } /// Every partition on `dev` whose *type* GUID is `target`, in entry order. -/// -/// The same walk [`locate`] makes, up to and including the entry array's CRC -/// and no further: a match here is a **candidate**, not a partition anything -/// may read. `locate` on a candidate's own [`Partition::unique_guid`] is what -/// applies the range and overlap checks a set cannot carry. +/// `out` is cleared, then filled from the front. pub fn locate_type( dev: &mut dyn Sectors, target: Guid, - out: &mut [Partition], + out: &mut [Option], ) -> Result { - scan(dev, &|part| part.type_guid == target, out) + scan(dev, &|type_guid| type_guid == target, out) } /// Every partition on `dev`, in entry order: [`locate_type`] with no type -/// asked for, so a match here is what the table *states* — checked up to the -/// entry array's CRC and not for range or overlap. -pub fn list(dev: &mut dyn Sectors, out: &mut [Partition]) -> Result { +/// asked for. +pub fn list(dev: &mut dyn Sectors, out: &mut [Option]) -> Result { scan(dev, &|_| true, out) } fn scan( dev: &mut dyn Sectors, - keep: &dyn Fn(&Partition) -> bool, - out: &mut [Partition], + keep: &dyn Fn(Guid) -> bool, + out: &mut [Option], ) -> Result { let disk = open_disk(dev)?; match scan_type_at(dev, 1, keep, &disk, out) { Ok(scan) => Ok(scan), Err(primary_err) if primary_err.primary_never_checked_out() => { - scan_type_at(dev, disk.lba_count - 1, keep, &disk, out).or(Err(primary_err)) + scan_type_at(dev, disk.backup_header_lba, keep, &disk, out).or(Err(primary_err)) } Err(primary_err) => Err(primary_err), } @@ -283,61 +386,109 @@ pub struct TypeScan { } struct Disk { - lba_bytes: u32, + lba: LbaSize, 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 +/// a [`Block`]. +#[derive(Clone, Copy)] +enum LbaSize { + B512, + B1024, + B2048, + B4096, +} + +/// One logical block of the largest size this crate parses. +type Block = [u8; 4096]; + +impl LbaSize { + fn of(bytes: u32) -> Option { + match bytes { + 512 => Some(Self::B512), + 1024 => Some(Self::B1024), + 2048 => Some(Self::B2048), + 4096 => Some(Self::B4096), + _ => None, + } + } + + const fn bytes(self) -> u32 { + 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], + Self::B1024 => &mut block[..1024], + Self::B2048 => &mut block[..2048], + Self::B4096 => &mut block[..], + } + } +} + /// The preamble both walks share: a block size this crate parses, a device big /// enough to hold a table, and a protective MBR at LBA 0. fn open_disk(dev: &mut dyn Sectors) -> Result { let lba_bytes = dev.lba_bytes(); - if !(MIN_LBA_BYTES..=MAX_LBA_BYTES).contains(&lba_bytes) || !lba_bytes.is_power_of_two() { - return Err(GptError::UnsupportedLbaSize(lba_bytes)); - } + let lba = LbaSize::of(lba_bytes).ok_or(GptError::UnsupportedLbaSize(lba_bytes))?; let lba_count = dev.lba_count(); // LBA 0 protective MBR, LBA 1 header, at least one block of entries. - if lba_count < 3 { - return Err(GptError::DeviceTooSmall(lba_count)); - } - let lba_count_slack = dev.lba_count_granularity().get() - 1; + let backup_header_lba = match lba_count.checked_sub(1) { + 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 = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = lba.of_block(&mut block); read(dev, 0, block)?; check_protective_mbr(block)?; - Ok(Disk { lba_bytes, lba_count, lba_count_slack }) + Ok(Disk { lba, lba_count, backup_header_lba, lba_count_slack }) } /// [`locate_type`]'s work against one header, primary or backup. fn scan_type_at( dev: &mut dyn Sectors, header_lba: u64, - keep: &dyn Fn(&Partition) -> bool, + keep: &dyn Fn(Guid) -> bool, disk: &Disk, - out: &mut [Partition], + out: &mut [Option], ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..disk.lba_bytes as usize]; + out.fill(None); + let capacity = out.len(); + let mut block: Block = [0; 4096]; + let block = disk.lba.of_block(&mut block); read(dev, header_lba, block)?; let header = parse_header(block, disk, header_lba)?; + // At most `entry_count`, a `u32`: never saturates. let mut matched = 0u32; - let mut listed = 0usize; + let mut slots = out.iter_mut(); // Meaningless unless `walk_entries` returns `Ok`: the array's CRC is // checked at the end of the walk, and an `Err` hands the caller nothing. - let used_entries = walk_entries(dev, &header, disk.lba_bytes, &mut |part| { - if keep(&part) { - matched += 1; - if let Some(slot) = out.get_mut(listed) { - *slot = part; - listed += 1; - } + let used_entries = walk_entries(dev, &header, disk.lba, &mut |stated| { + if !keep(stated.type_guid) { + return; + } + matched = matched.saturating_add(1); + if let Some(slot) = slots.next() { + *slot = Some(Partition::place(&stated, &header)); } })?; + let listed = usize::try_from(matched).map_or(capacity, |matched| matched.min(capacity)); Ok(TypeScan { matched, listed, disk_guid: header.disk_guid, used_entries }) } @@ -350,27 +501,19 @@ fn locate_at( target: Guid, disk: &Disk, ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..disk.lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = disk.lba.of_block(&mut block); read(dev, header_lba, block)?; let header = parse_header(block, disk, header_lba)?; - let (found, used_entries) = scan_entries(dev, &header, target, disk.lba_bytes)?; - let Some(partition) = found else { + let (found, used_entries) = scan_entries(dev, &header, target, disk.lba)?; + let Some(stated) = found else { return Err(GptError::NotFound { used_entries }); }; - - if partition.first_lba > partition.last_lba - || partition.first_lba < header.first_usable_lba - || partition.last_lba > header.last_usable_lba - { - return Err(GptError::PartitionRange { - first: partition.first_lba, - last: partition.last_lba, - }); - } - check_no_overlap(dev, &header, &partition, disk.lba_bytes)?; + let partition = Partition::place(&stated, &header) + .map_err(|unplaced| GptError::PartitionRange { first: unplaced.first, last: unplaced.last })?; + check_no_overlap(dev, &header, &partition, disk.lba)?; Ok(Located { partition, disk_guid: header.disk_guid, used_entries }) } @@ -391,62 +534,97 @@ fn read(dev: &mut dyn Sectors, lba: u64, buf: &mut [u8]) -> Result<(), GptError> /// one of them is guessing. fn check_protective_mbr(lba0: &[u8]) -> Result<(), GptError> { // The MBR is 512 bytes at the front of LBA 0 whatever the block size is. - let Some(mbr) = lba0.get(..512) else { + let Some(mbr) = lba0.first_chunk::<512>() else { return Err(GptError::NoProtectiveMbr); }; if mbr[510] != 0x55 || mbr[511] != 0xAA { return Err(GptError::NoProtectiveMbr); } - let mut protective = 0; - for record in 0..4 { - let ty = mbr[446 + record * 16 + 4]; - match ty { - 0 => {} - MBR_TYPE_PROTECTIVE => protective += 1, - _ => return Err(GptError::NoProtectiveMbr), - } - } - if protective != 1 { + // The type byte of each of the four 16-byte records from byte 446. + let types = [mbr[450], mbr[466], mbr[482], mbr[498]]; + let protective = types.iter().filter(|&&ty| ty == MBR_TYPE_PROTECTIVE).count(); + if protective != 1 || types.iter().any(|&ty| ty != 0 && ty != MBR_TYPE_PROTECTIVE) { return Err(GptError::NoProtectiveMbr); } Ok(()) } +/// Every header field this crate reads, as the disk states it. +struct StatedHeader { + signature: [u8; 8], + revision: u32, + header_bytes: u32, + crc: u32, + reserved: u32, + my_lba: u64, + first_usable_lba: u64, + last_usable_lba: u64, + disk_guid: Guid, + entry_array_lba: u64, + entry_count: u32, + entry_bytes: u32, + entry_array_crc: u32, +} + +impl StatedHeader { + /// `None` only where `block` is shorter than a header. + fn decode(block: &[u8]) -> Option { + Some(Self { + signature: bytes(block, 0)?, + revision: le_u32(block, 8)?, + header_bytes: le_u32(block, 12)?, + crc: le_u32(block, 16)?, + reserved: le_u32(block, 20)?, + my_lba: le_u64(block, 24)?, + first_usable_lba: le_u64(block, 40)?, + last_usable_lba: le_u64(block, 48)?, + disk_guid: bytes(block, 56).map(Guid)?, + entry_array_lba: le_u64(block, 72)?, + entry_count: le_u32(block, 80)?, + entry_bytes: le_u32(block, 84)?, + entry_array_crc: le_u32(block, 88)?, + }) + } +} + fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result { - let Disk { lba_bytes, lba_count, lba_count_slack } = *disk; - if lba1.get(..8) != Some(&HEADER_SIGNATURE[..]) { + let Disk { lba, lba_count, backup_header_lba, lba_count_slack } = *disk; + let lba_bytes = lba.bytes(); + let Some(stated) = StatedHeader::decode(lba1) else { + return Err(GptError::NoHeader); + }; + if &stated.signature != HEADER_SIGNATURE { return Err(GptError::NoHeader); } - let revision = le_u32(lba1, 8); - if revision != HEADER_REVISION_1_0 { - return Err(GptError::UnsupportedRevision(revision)); + if stated.revision != HEADER_REVISION_1_0 { + return Err(GptError::UnsupportedRevision(stated.revision)); } - let header_bytes = le_u32(lba1, 12); + let header_bytes = stated.header_bytes; if header_bytes < MIN_HEADER_BYTES || header_bytes > lba_bytes { return Err(GptError::HeaderSize(header_bytes)); } - let reserved = le_u32(lba1, 20); - if reserved != 0 { - return Err(GptError::HeaderReserved(reserved)); + if stated.reserved != 0 { + return Err(GptError::HeaderReserved(stated.reserved)); } - let my_lba = le_u64(lba1, 24); - if my_lba != header_lba { - return Err(GptError::HeaderMisplaced(my_lba)); + if stated.my_lba != header_lba { + return Err(GptError::HeaderMisplaced(stated.my_lba)); } - let stored_crc = le_u32(lba1, 16); - let computed = header_crc(lba1, header_bytes as usize); - if stored_crc != computed { - return Err(GptError::HeaderCrc { stored: stored_crc, computed }); + let covered = usize::try_from(header_bytes).ok().and_then(|len| lba1.get(..len)); + let Some(computed) = covered.and_then(header_crc) else { + return Err(GptError::HeaderSize(header_bytes)); + }; + if stated.crc != computed { + return Err(GptError::HeaderCrc { stored: stated.crc, computed }); } - let first_usable_lba = le_u64(lba1, 40); - let last_usable_lba = le_u64(lba1, 48); + let (first_usable_lba, last_usable_lba) = (stated.first_usable_lba, stated.last_usable_lba); if first_usable_lba < 2 || last_usable_lba < first_usable_lba || last_usable_lba >= lba_count { return Err(GptError::UsableRange { first: first_usable_lba, last: last_usable_lba }); } - let entry_bytes = le_u32(lba1, 84); + let entry_bytes = stated.entry_bytes; + let entry_len = usize::try_from(entry_bytes).map_err(|_| GptError::EntrySize(entry_bytes))?; if entry_bytes < MIN_ENTRY_BYTES || !entry_bytes.is_power_of_two() || entry_bytes > lba_bytes @@ -454,15 +632,16 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result MAX_ENTRY_ARRAY_BYTES { - return Err(GptError::EntryArrayTooBig { entries: entry_count, entry_size: entry_bytes }); + return Err(too_big); } - let entry_array_lba = le_u64(lba1, 72); - let array_lbas = array_bytes.div_ceil(lba_bytes as u64); - let array_end = entry_array_lba + let entry_array_lba = stated.entry_array_lba; + let array_lbas = array_bytes.div_ceil(u64::from(lba_bytes)); + let entry_array_end = entry_array_lba .checked_add(array_lbas) .ok_or(GptError::EntryArrayMisplaced { lba: entry_array_lba, lbas: array_lbas })?; // The primary's array sits between the header block and the first usable @@ -472,9 +651,9 @@ fn parse_header(lba1: &[u8], disk: &Disk, header_lba: u64) -> Result first_usable_lba + entry_array_lba < 2 || entry_array_end > first_usable_lba } else { - entry_array_lba <= last_usable_lba || array_end > header_lba + entry_array_lba <= last_usable_lba || entry_array_end > header_lba }; if misplaced { return Err(GptError::EntryArrayMisplaced { lba: entry_array_lba, lbas: array_lbas }); @@ -485,31 +664,36 @@ 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 }); } Ok(Header { - disk_guid: read_guid(lba1, 56), + disk_guid: stated.disk_guid, first_usable_lba, last_usable_lba, entry_array_lba, + entry_array_end, entry_count, entry_bytes, - entry_array_crc: le_u32(lba1, 88), + entry_len, + entry_array_crc: stated.entry_array_crc, }) } /// The header's CRC is taken over itself with its own CRC field zeroed, so it /// is computed in three pieces rather than by copying the block to patch it. -fn header_crc(lba1: &[u8], header_bytes: usize) -> u32 { +/// `None` only where `covered` is shorter than the CRC field's end. +fn header_crc(covered: &[u8]) -> Option { + let (before, rest) = covered.split_first_chunk::<16>()?; + let (_stored, after) = rest.split_first_chunk::<4>()?; let mut crc = Crc32::new(); - crc.update(&lba1[..16]); + crc.update(before); crc.update(&[0; 4]); - crc.update(&lba1[20..header_bytes]); - crc.finish() + crc.update(after); + Some(crc.finish()) } /// Walk the entry array once, checking its CRC as we go, and show `visit` @@ -522,46 +706,32 @@ fn header_crc(lba1: &[u8], header_bytes: usize) -> u32 { fn walk_entries( dev: &mut dyn Sectors, header: &Header, - lba_bytes: u32, - visit: &mut dyn FnMut(Partition), + lba: LbaSize, + visit: &mut dyn FnMut(Stated), ) -> Result { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; + let mut block: Block = [0; 4096]; + let block = lba.of_block(&mut block); - let entries_per_lba = lba_bytes / header.entry_bytes; let mut crc = Crc32::new(); - let mut remaining = header.entry_count as u64 * header.entry_bytes as u64; + let mut indices = 0..header.entry_count; + // At most `entry_count`, a `u32`: never saturates. let mut used = 0u32; - let mut index = 0u32; - let mut lba = header.entry_array_lba; - while remaining > 0 { - read(dev, lba, block)?; - let take = remaining.min(lba_bytes as u64) as usize; - crc.update(&block[..take]); - - for slot in 0..entries_per_lba { - if index >= header.entry_count { - break; - } - let at = slot as usize * header.entry_bytes as usize; - let entry = &block[at..at + header.entry_bytes as usize]; - let type_guid = read_guid(entry, 0); - if !type_guid.is_zero() { - used += 1; - visit(Partition { - index, - type_guid, - unique_guid: read_guid(entry, 16), - first_lba: le_u64(entry, 32), - last_lba: le_u64(entry, 40), - }); + for at in header.entry_array_lba..header.entry_array_end { + read(dev, at, block)?; + // The array is `entry_count` whole entries and nothing after them, so + // the entries walked are exactly the bytes its CRC covers. + for entry in block.chunks_exact(header.entry_len) { + let Some(index) = indices.next() else { break }; + crc.update(entry); + let Some(stated) = Stated::decode(index, entry) else { + return Err(GptError::EntrySize(header.entry_bytes)); + }; + if !stated.type_guid.is_zero() { + used = used.saturating_add(1); + visit(stated); } - index += 1; } - - remaining -= take as u64; - lba += 1; } let computed = crc.finish(); @@ -571,23 +741,36 @@ fn walk_entries( Ok(used) } +impl Stated { + /// `None` only where `entry` is shorter than the fields read. + fn decode(index: u32, entry: &[u8]) -> Option { + Some(Self { + index, + type_guid: bytes(entry, 0).map(Guid)?, + unique_guid: bytes(entry, 16).map(Guid)?, + first: le_u64(entry, 32)?, + last: le_u64(entry, 40)?, + }) + } +} + /// The entry carrying the unique GUID `target`, out of a walk whose CRC held. fn scan_entries( dev: &mut dyn Sectors, header: &Header, target: Guid, - lba_bytes: u32, -) -> Result<(Option, u32), GptError> { - let mut found: Option = None; + lba: LbaSize, +) -> Result<(Option, u32), GptError> { + let mut found: Option = None; let mut duplicate: Option<(u32, u32)> = None; - let used = walk_entries(dev, header, lba_bytes, &mut |part| { - if part.unique_guid != target { + let used = walk_entries(dev, header, lba, &mut |stated| { + if stated.unique_guid != target { return; } match &found { - None => found = Some(part), - Some(first) if duplicate.is_none() => duplicate = Some((first.index, part.index)), + None => found = Some(stated), + Some(first) if duplicate.is_none() => duplicate = Some((first.index, stated.index)), Some(_) => {} } })?; @@ -608,53 +791,55 @@ fn check_no_overlap( dev: &mut dyn Sectors, header: &Header, matched: &Partition, - lba_bytes: u32, + lba: LbaSize, ) -> Result<(), GptError> { - let mut block = [0u8; MAX_LBA_BYTES as usize]; - let block = &mut block[..lba_bytes as usize]; - - let entries_per_lba = lba_bytes / header.entry_bytes; - let mut remaining = header.entry_count as u64 * header.entry_bytes as u64; - let mut index = 0u32; - let mut lba = header.entry_array_lba; - - while remaining > 0 { - read(dev, lba, block)?; - for slot in 0..entries_per_lba { - if index >= header.entry_count { - break; - } - let at = slot as usize * header.entry_bytes as usize; - let entry = &block[at..at + header.entry_bytes as usize]; - if index != matched.index && !read_guid(entry, 0).is_zero() { - let first = le_u64(entry, 32); - let last = le_u64(entry, 40); - if first <= last && first <= matched.last_lba && matched.first_lba <= last { - return Err(GptError::PartitionOverlap { index }); - } - } - index += 1; + let mut overlap = None; + walk_entries(dev, header, lba, &mut |other| { + if overlap.is_none() + && other.index != matched.index + && other.first <= other.last + && other.first <= matched.last_lba + && matched.first_lba <= other.last + { + overlap = Some(other.index); } - remaining -= remaining.min(lba_bytes as u64); - lba += 1; + })?; + match overlap { + Some(index) => Err(GptError::PartitionOverlap { index }), + None => Ok(()), } - Ok(()) } -fn read_guid(buf: &[u8], at: usize) -> Guid { - let mut out = [0u8; 16]; - out.copy_from_slice(&buf[at..at + 16]); - Guid(out) +/// `N` bytes of `buf` from `at`, or `None` where `buf` ends first. +fn bytes(buf: &[u8], at: usize) -> Option<[u8; N]> { + buf.get(at..)?.first_chunk::().copied() +} + +fn le_u32(buf: &[u8], at: usize) -> Option { + bytes(buf, at).map(u32::from_le_bytes) } -fn le_u32(buf: &[u8], at: usize) -> u32 { - let mut b = [0u8; 4]; - b.copy_from_slice(&buf[at..at + 4]); - u32::from_le_bytes(b) +fn le_u64(buf: &[u8], at: usize) -> Option { + bytes(buf, at).map(u64::from_le_bytes) } -fn le_u64(buf: &[u8], at: usize) -> u64 { - let mut b = [0u8; 8]; - b.copy_from_slice(&buf[at..at + 8]); - u64::from_le_bytes(b) +#[cfg(test)] +mod tests { + use super::*; + + /// The public bounds and the sizes [`LbaSize`] admits are one range. + #[test] + fn the_block_sizes_parsed_are_the_declared_range() { + let parsed: [u32; 4] = [512, 1024, 2048, 4096]; + for bytes in 0..=2 * MAX_LBA_BYTES { + let admitted = LbaSize::of(bytes).map(LbaSize::bytes); + assert_eq!(admitted.is_some(), parsed.contains(&bytes), "{bytes}"); + assert_eq!(admitted.unwrap_or(bytes), bytes); + } + assert_eq!((parsed[0], parsed[3]), (MIN_LBA_BYTES, MAX_LBA_BYTES)); + let mut block: Block = [0; 4096]; + for lba in [LbaSize::B512, LbaSize::B1024, LbaSize::B2048, LbaSize::B4096] { + assert_eq!(lba.of_block(&mut block).len() as u32, lba.bytes()); + } + } } diff --git a/toyos-gpt/tests/mutate.rs b/toyos-gpt/tests/mutate.rs new file mode 100644 index 0000000000..84d68f1816 --- /dev/null +++ b/toyos-gpt/tests/mutate.rs @@ -0,0 +1,232 @@ +//! Value-mutated tables with both CRCs resealed: no parse panics, and every answer is true of a copy of the table. + +mod table; + +use std::collections::BTreeMap; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use table::{OnDisk, Image, RawEntry, Rng, Shape}; +use toyos_gpt::{GptError, Guid}; + +/// Iterations of the normal host run; each draws its own seed from its number. +const ITERATIONS: u64 = 5_000; +const SEED: u64 = 0x6770_745F_6D75_7461; + +const SHAPE: Shape<'static> = Shape { + lba_sizes: &[512, 512, 512, 1024, 2048, 4096], + 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. +#[derive(Debug)] +struct Seen { + index: u32, + type_guid: Guid, + unique: Guid, + first: u64, + last: u64, + count: u64, +} + +/// An entry the parser handed back as no partition. +#[derive(Debug)] +struct Refused { + index: u32, + unique: Guid, + first: u64, + last: u64, +} + +/// What `list` answered: every entry in order, and its two counts. +struct Listed { + entries: Vec>, + matched: u32, + used_entries: u32, +} + +/// What `locate` answered for each GUID asked. +type Answers = Vec<(Guid, Result)>; + +/// What the parser answered for one image; the one place this file names its API. +fn observe(img: &mut Image, targets: &[Guid]) -> (Result, Answers) { + let seen = |p: &toyos_gpt::Partition| Seen { + index: p.index(), + type_guid: p.type_guid(), + unique: p.unique_guid(), + first: p.first_lba(), + last: p.last_lba(), + count: p.lba_count().get(), + }; + let mut out = [None; 64]; + let listed = toyos_gpt::list(img, &mut out).map(|scan| Listed { + entries: out + .iter() + .flatten() + .map(|entry| match entry { + Ok(p) => Ok(seen(p)), + Err(u) => Err(Refused { index: u.index, unique: u.unique_guid, first: u.first, last: u.last }), + }) + .collect(), + matched: scan.matched, + used_entries: scan.used_entries, + }); + let located = targets.iter().map(|&t| (t, toyos_gpt::locate(img, t).map(|l| seen(&l.partition())))).collect(); + (listed, located) +} + +fn is(e: &RawEntry, s: &Seen) -> bool { + (e.index, e.type_guid, e.unique, e.first, e.last) == (s.index, s.type_guid, s.unique, s.first, s.last) +} + +/// A partition's own invariant, against the copy it came from and the device. +fn sound(c: &OnDisk, s: &Seen, lba_count: u64) -> bool { + c.entry(s.index).is_some_and(|e| is(e, s)) + && c.first_usable <= s.first + && s.first <= s.last + && s.last <= c.last_usable + && s.last < lba_count + && u128::from(s.count) == u128::from(s.last) - u128::from(s.first) + 1 +} + +fn overlaps(a: &RawEntry, b: &RawEntry) -> bool { + a.first <= a.last && b.first <= b.last && a.first <= b.last && b.first <= a.last +} + +/// Every property this file states, for one image; `Err` names the one broken. +fn judge( + copies: &[OnDisk], + lba_count: u64, + listed: &Result, + located: &[(Guid, Result)], + tally: &mut BTreeMap, +) -> Result<(), String> { + let mut count = |what: String| *tally.entry(what).or_default() += 1; + match listed { + Ok(l) => { + let faithful = |c: &OnDisk| { + l.used_entries as usize == c.entries.len() + && l.matched as usize == c.entries.len() + && l.entries.len() == c.entries.len().min(64) + && l.entries.iter().all(|entry| match entry { + Ok(s) => sound(c, s, lba_count), + Err(r) => c.entry(r.index).is_some_and(|e| { + (e.index, e.unique, e.first, e.last) == (r.index, r.unique, r.first, r.last) && !c.places(e) + }), + }) + }; + if !copies.iter().any(faithful) { + return Err(format!("list answered what no copy of the table says: {:?}", l.entries)); + } + for entry in &l.entries { + count(if entry.is_ok() { "list: partition" } else { "list: no partition" }.into()); + } + } + Err(e) => count(format!("list: {}", variant(e))), + } + for (target, answer) in located { + let carrying = |c: &OnDisk| c.entries.iter().filter(|e| e.unique == *target).copied().collect::>(); + let true_of_a_copy = |c: &OnDisk| match answer { + Ok(s) => { + sound(c, s, lba_count) + && s.unique == *target + && carrying(c).len() == 1 + && c.entry(s.index).is_some_and(|me| !c.entries.iter().any(|o| o.index != me.index && overlaps(o, me))) + } + Err(GptError::PartitionRange { first, last }) => { + matches!(carrying(c)[..], [e] if (e.first, e.last) == (*first, *last) && !c.places(&e)) + } + Err(GptError::PartitionOverlap { index }) => match (&carrying(c)[..], c.entry(*index)) { + ([me], Some(other)) => other.index != me.index && overlaps(other, me) && c.places(me), + _ => false, + }, + Err(GptError::DuplicateUniqueGuid { first, second }) => { + carrying(c).iter().map(|e| e.index).take(2).eq([*first, *second]) + } + Err(GptError::NotFound { used_entries }) => { + carrying(c).is_empty() && *used_entries as usize == c.entries.len() + } + // Refusals of the table itself, before any entry means anything. + Err(_) => true, + }; + let of_the_table = matches!( + answer, + Err(GptError::PartitionRange { .. } + | GptError::PartitionOverlap { .. } + | GptError::DuplicateUniqueGuid { .. } + | GptError::NotFound { .. }) + | Ok(_) + ); + if of_the_table && !copies.iter().any(true_of_a_copy) { + return Err(format!("locate({target}) answered {answer:?}, which no copy of the table says")); + } + count(match answer { + Ok(_) => "locate: located".into(), + Err(e) => format!("locate: {}", variant(e)), + }); + } + Ok(()) +} + +fn variant(e: &GptError) -> String { + let debug = format!("{e:?}"); + debug.split(['(', ' ']).next().unwrap_or_default().to_string() +} + +#[test] +fn a_resealed_table_with_any_value_bent_never_panics_and_hands_back_only_partitions() { + let mut tally = BTreeMap::new(); + for i in 0..ITERATIONS { + let seed = SEED ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut rng = Rng::new(seed); + let mut layout = table::valid(&mut rng, &SHAPE); + // One in ten stays as UEFI laid it out. + if !rng.one_in(10) { + table::mutate(&mut rng, &mut layout); + } + let mut img = table::image(&layout); + let mut targets: Vec = layout.primary.entries.iter().map(|e| e.unique).collect(); + targets.truncate(3); + targets.push(rng.guid()); + + let run = catch_unwind(AssertUnwindSafe(|| observe(&mut img, &targets))); + let Ok((listed, located)) = run else { + panic!("the parser panicked on seed {seed:#x} (iteration {i}): {layout:#?}"); + }; + let copies = OnDisk::both(&img); + if let Err(why) = judge(&copies, img.lba_count, &listed, &located, &mut tally) { + panic!("seed {seed:#x} (iteration {i}): {why}\n{layout:#?}"); + } + } + + for (what, n) in &tally { + eprintln!("{n:>8} {what}"); + } + // Every refusal past both CRCs, and acceptance, has to have happened. + for reached in [ + "list: partition", + "list: no partition", + "locate: located", + "locate: PartitionRange", + "locate: PartitionOverlap", + "locate: DuplicateUniqueGuid", + "locate: NotFound", + "locate: UsableRange", + "locate: UsableRangeCoversBackup", + "locate: EntryArrayMisplaced", + "locate: EntryArrayTooBig", + "locate: EntrySize", + "locate: HeaderSize", + "locate: HeaderMisplaced", + "locate: NoProtectiveMbr", + ] { + assert!(tally.get(reached).copied().unwrap_or(0) > 0, "{ITERATIONS} tables never reached {reached:?}"); + } + let located = |crc: bool| -> u64 { + tally.iter().filter(|(k, _)| k.starts_with("locate: ") && (!crc || k.ends_with("Crc"))).map(|(_, n)| n).sum() + }; + let (crc, parses) = (located(true), located(false)); + assert!(crc * 10 < parses, "{crc} of {parses} locates stopped at a checksum: the reseal is not sealing"); +} diff --git a/toyos-gpt/tests/oracle.rs b/toyos-gpt/tests/oracle.rs new file mode 100644 index 0000000000..69e1df3ec1 --- /dev/null +++ b/toyos-gpt/tests/oracle.rs @@ -0,0 +1,199 @@ +//! toyos-gpt against the `gpt` crate: agreement on UEFI tables, and only named differences on bent ones. + +mod table; + +use std::collections::BTreeMap; +use std::io::Cursor; + +use table::{Image, Layout, Rng, Shape}; +use toyos_gpt::{GptError, Guid}; + +/// Linux filesystem data, a type both readers name. +const LINUX_FS: Guid = + Guid::from_fields(0x0FC6_3DAF, 0x8483, 0x4772, [0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4]); + +const SHAPE: Shape<'static> = Shape { + lba_sizes: &[512, 4096], + types: &[Guid::EFI_SYSTEM, Guid::MICROSOFT_BASIC, LINUX_FS], + // `gpt` reads 128-byte entries whatever the header says. + entry_sizes: &[128], + backup: false, + floored: false, +}; + +const VALID: u64 = 2_000; +const BENT: u64 = 4_000; +const SEED: u64 = 0x6770_745F_6F72_636C; + +/// One entry: index, type as text, unique GUID, first and last block. +type Row = (u32, String, Guid, u64, u64); + +/// toyos-gpt's reading. +struct Ours { + disk: Guid, + placed: BTreeMap, + unplaced: BTreeMap, +} + +fn ours(img: &mut Image) -> Result { + let mut out = [None; 64]; + let scan = toyos_gpt::list(img, &mut out)?; + assert_eq!(scan.listed, scan.matched as usize, "a table with more entries than this file's slice"); + let mut read = Ours { disk: scan.disk_guid, placed: BTreeMap::new(), unplaced: BTreeMap::new() }; + for entry in out.iter().flatten() { + match entry { + Ok(p) => { + let row = (p.index(), p.type_guid().to_string(), p.unique_guid(), p.first_lba(), p.last_lba()); + read.placed.insert(p.index(), row); + } + Err(u) => { + read.unplaced.insert(u.index, (u.index, u.type_guid.to_string(), u.unique_guid, u.first, u.last)); + } + } + } + Ok(read) +} + +/// The `gpt` crate's reading. +enum Theirs { + Refused(String), + /// Not asked: it allocates the array its header claims before checking it. + NotAsked, + Read { disk: Guid, rows: BTreeMap }, +} + +fn theirs(img: &Image) -> Theirs { + let lb = gpt::disk::LogicalBlockSize::try_from(u64::from(img.lba_bytes)).expect("512 or 4096"); + let mut dev = Cursor::new(img.bytes.as_slice()); + let header = match gpt::header::read_header_from_arbitrary_device(&mut dev, lb) { + Ok(header) => header, + Err(e) => return Theirs::Refused(e.to_string()), + }; + if u64::from(header.num_parts) * u64::from(header.part_size) > 1 << 20 { + return Theirs::NotAsked; + } + match gpt::partition::file_read_partitions(&mut dev, &header, lb) { + Err(e) => Theirs::Refused(e.to_string()), + Ok(parts) => Theirs::Read { + disk: Guid(header.disk_guid.to_bytes_le()), + // `gpt` keys its entries from 1. + rows: parts + .iter() + .map(|(&key, p)| { + let row = (key - 1, p.part_type_guid.guid.to_string(), Guid(p.part_guid.to_bytes_le()), p.first_lba, p.last_lba); + (key - 1, row) + }) + .collect(), + }, + } +} + +/// The name of the difference between the two readings, or why it has none. +fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: &Theirs) -> Result<&'static str, String> { + let t = &layout.primary; + let unnamed = gpt::partition_types::Type::default().guid; + match (ours, theirs) { + (Err(_), Theirs::Refused(_)) => Ok("both refuse"), + (Err(_), Theirs::NotAsked) => Ok("an array over 1 MiB: gpt would allocate it, toyos-gpt refuses"), + (Ok(_), Theirs::NotAsked) => Err("gpt was not asked, and toyos-gpt read an array over 1 MiB".into()), + (Err(e), Theirs::Read { .. }) => match e { + GptError::NoProtectiveMbr => Ok("gpt reads no protective MBR"), + GptError::UnsupportedRevision(_) | GptError::HeaderReserved(_) | GptError::HeaderMisplaced(_) => { + Ok("gpt checks no revision, reserved word or header position") + } + GptError::HeaderSize(_) | GptError::EntrySize(_) => Ok("gpt checks no header or entry size"), + GptError::UsableRange { .. } + | GptError::UsableRangeCoversBackup { .. } + | GptError::EntryArrayMisplaced { .. } + | GptError::EntryArrayTooBig { .. } => Ok("gpt checks no layout"), + GptError::DeviceTooSmall(_) | GptError::ReadFailed(_) if img.lba_count != layout.lba_count => { + Ok("the device answers a size the image is not") + } + other => Err(format!("toyos-gpt refused with {other:?}, and gpt read the table")), + }, + (Ok(_), Theirs::Refused(why)) if t.header_bytes != 92 => { + let _ = why; + Ok("gpt takes the header CRC over 92 bytes whatever header_size says") + } + (Ok(_), Theirs::Refused(why)) => Err(format!("toyos-gpt read the table, and gpt refused it: {why}")), + (Ok(o), Theirs::Read { disk, rows }) => { + if t.entry_bytes != 128 { + return Ok("gpt strides 128 bytes whatever the entry size"); + } + if *disk != o.disk { + return Err(format!("disk GUID {} against gpt's {disk}", o.disk)); + } + let mut named = "agree"; + for (index, row) in rows { + match (o.placed.get(index), o.unplaced.get(index)) { + (Some(mine), None) if mine == row => {} + // A type only toyos-gpt names: gpt answers the zero GUID. + (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4) == (&row.0, &row.2, row.3, row.4) => { + named = "a type gpt's own table does not name"; + } + (None, Some(stated)) if (stated.2, stated.3, stated.4) == (row.2, row.3, row.4) => { + named = "an entry toyos-gpt places as no partition: gpt checks no range"; + } + // gpt counts an entry by any non-zero byte, toyos-gpt by its type GUID. + (None, None) if row.1 == unnamed => named = "a zero type over non-zero bytes: gpt lists it", + (mine, stated) => { + return Err(format!("entry {index}: gpt reads {row:?}, toyos-gpt {mine:?} / {stated:?}")); + } + } + } + if let Some(index) = o.placed.keys().chain(o.unplaced.keys()).find(|i| !rows.contains_key(i)) { + return Err(format!("toyos-gpt reads entry {index} and gpt does not")); + } + Ok(named) + } + } +} + +/// The generator's types are ones both readers spell alike. +#[test] +fn both_readers_spell_the_types_alike() { + for (ours, theirs) in [ + (Guid::EFI_SYSTEM, gpt::partition_types::EFI), + (Guid::MICROSOFT_BASIC, gpt::partition_types::BASIC), + (LINUX_FS, gpt::partition_types::LINUX_FS), + ] { + assert_eq!(ours.to_string(), theirs.guid); + } +} + +#[test] +fn on_tables_uefi_lays_out_both_readers_agree_entry_for_entry() { + for i in 0..VALID { + let seed = SEED ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let layout = table::valid(&mut Rng::new(seed), &SHAPE); + let mut img = table::image(&layout); + let mine = ours(&mut img); + let named = differ(&layout, &img, &mine, &theirs(&img)); + assert_eq!(named, Ok("agree"), "seed {seed:#x}: {layout:#?}"); + let placed = mine.map(|o| o.placed.len()).unwrap_or_default(); + assert_eq!(placed, layout.primary.entries.len(), "seed {seed:#x}"); + } +} + +#[test] +fn on_bent_tables_every_disagreement_is_a_named_difference() { + let mut tally: BTreeMap<&'static str, u64> = BTreeMap::new(); + for i in 0..BENT { + let seed = SEED ^ !i.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut rng = Rng::new(seed); + let mut layout = table::valid(&mut rng, &SHAPE); + table::mutate(&mut rng, &mut layout); + let mut img = table::image(&layout); + let mine = ours(&mut img); + match differ(&layout, &img, &mine, &theirs(&img)) { + Ok(named) => *tally.entry(named).or_default() += 1, + Err(why) => panic!("seed {seed:#x}: {why}\n{layout:#?}"), + } + } + for (named, n) in &tally { + eprintln!("{n:>6} {named}"); + } + for reached in ["agree", "both refuse", "an entry toyos-gpt places as no partition: gpt checks no range"] { + assert!(tally.contains_key(reached), "{BENT} bent tables never reached {reached:?}"); + } +} diff --git a/toyos-gpt/tests/parse.rs b/toyos-gpt/tests/parse.rs index b623a054c0..e3c9af819a 100644 --- a/toyos-gpt/tests/parse.rs +++ b/toyos-gpt/tests/parse.rs @@ -215,7 +215,7 @@ impl Image { fn locate_type( &mut self, target: Guid, - out: &mut [toyos_gpt::Partition], + out: &mut [Option], ) -> Result { toyos_gpt::locate_type(self, target, out) } @@ -248,13 +248,13 @@ impl Sectors for Image { fn finds_the_partition_by_unique_guid() { let mut img = Builder::default().build(); let found = img.locate(guid(0xC3)).expect("the table has this GUID"); - assert_eq!(found.partition.index, 2); - assert_eq!(found.partition.first_lba, 200); - assert_eq!(found.partition.last_lba, 299); - assert_eq!(found.partition.lba_count(), 100); - assert_eq!(found.used_entries, 4); - assert!(found.partition.is_efi_system()); - assert_eq!(found.disk_guid, guid(0x5D)); + assert_eq!(found.partition().index(), 2); + assert_eq!(found.partition().first_lba(), 200); + assert_eq!(found.partition().last_lba(), 299); + assert_eq!(found.partition().lba_count().get(), 100); + assert_eq!(found.used_entries(), 4); + assert!(found.partition().is_efi_system()); + assert_eq!(found.disk_guid(), guid(0x5D)); } /// The one that matters: three of the four entries are ESPs, so anything @@ -271,7 +271,7 @@ fn each_guid_finds_its_own_entry() { for (g, index, first, last) in want { let mut img = Builder::default().build(); let found = img.locate(g).expect("present"); - assert_eq!((found.partition.index, found.partition.first_lba, found.partition.last_lba), (index, first, last)); + assert_eq!((found.partition().index(), found.partition().first_lba(), found.partition().last_lba()), (index, first, last)); } } @@ -281,17 +281,11 @@ fn each_guid_finds_its_own_entry() { #[test] fn a_type_scan_lists_every_entry_of_that_type() { let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; + let mut out = [None; 4]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); assert_eq!((scan.matched, scan.listed, scan.used_entries), (3, 3, 4)); assert_eq!(scan.disk_guid, guid(0x5D)); - let found: Vec = out[..scan.listed].iter().map(|p| p.unique_guid).collect(); + let found: Vec = out[..scan.listed].iter().flatten().flatten().map(|p| p.unique_guid()).collect(); assert_eq!(found, vec![guid(0xA1), guid(0xC3), guid(0xD4)]); // A type nothing carries is not an error; it is an empty set. @@ -304,16 +298,10 @@ fn a_type_scan_lists_every_entry_of_that_type() { #[test] fn a_type_scan_says_how_many_it_could_not_hand_back() { let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 1]; + let mut out = [None; 1]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); assert_eq!((scan.matched, scan.listed), (3, 1)); - assert_eq!(out[0].unique_guid, guid(0xA1)); + assert_eq!(out[0].and_then(Result::ok).map(|p| p.unique_guid()), Some(guid(0xA1))); } /// The scan is held to the same CRC as the search: a damaged array yields no @@ -322,13 +310,7 @@ fn a_type_scan_says_how_many_it_could_not_hand_back() { fn a_type_scan_over_a_damaged_array_is_refused() { let mut img = Builder::default().build(); *img.at(ARRAY_LBA, 3) ^= 0x01; - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 4]; + let mut out = [None; 4]; assert!(matches!( img.locate_type(TYPE_ESP, &mut out), Err(GptError::EntryArrayCrc { .. }) @@ -427,7 +409,7 @@ fn the_array_ends_where_the_header_says() { assert_eq!(img.locate(guid(0xD4)), Err(GptError::NotFound { used_entries: 3 })); *img.at(ARRAY_LBA, 3 * 128 + 1) ^= 0xFF; let found = img.locate(guid(0xC3)).expect("still parses"); - assert_eq!(found.used_entries, 3); + assert_eq!(found.used_entries(), 3); } #[test] @@ -572,11 +554,11 @@ fn a_damaged_primary_falls_back_to_a_good_backup() { let mut img = Builder { backup: true, ..Default::default() }.build(); *img.at(1, 0) = b'X'; let found = img.locate(guid(0xC3)).expect("the backup carries this GUID"); - assert_eq!(found.partition.index, 2); - assert_eq!(found.partition.first_lba, 200); - assert_eq!(found.partition.last_lba, 299); - assert_eq!(found.used_entries, 4); - assert_eq!(found.disk_guid, guid(0x5D)); + assert_eq!(found.partition().index(), 2); + assert_eq!(found.partition().first_lba(), 200); + assert_eq!(found.partition().last_lba(), 299); + assert_eq!(found.used_entries(), 4); + assert_eq!(found.disk_guid(), guid(0x5D)); } /// Both copies gone must be a named refusal, not a panic and not a made-up @@ -634,8 +616,8 @@ fn a_four_kibibyte_block_device_parses() { } .build(); let found = img.locate(guid(0x22)).expect("present"); - assert_eq!((found.partition.index, found.partition.first_lba), (1, 21)); - assert_eq!(found.used_entries, 2); + assert_eq!((found.partition().index(), found.partition().first_lba()), (1, 21)); + assert_eq!(found.used_entries(), 2); } #[test] @@ -729,7 +711,7 @@ fn an_honest_table_on_a_floored_device_view_parses() { .build(); let mut floored = Floored(img, 2048); let found = toyos_gpt::locate(&mut floored, guid(0xC3)).expect("an honest disk lost /boot"); - assert_eq!(found.partition.index, 2); + assert_eq!(found.partition().index(), 2); } /// UEFI gives every entry a `UniquePartitionGUID` that must be unique. Two @@ -745,7 +727,7 @@ fn two_entries_claiming_the_target_guid_are_refused() { Err(GptError::DuplicateUniqueGuid { first: 2, second: 3 }) ); // A duplicate of a GUID nobody asked for does not refuse the answer. - assert_eq!(img.locate(guid(0xB2)).map(|f| f.partition.index), Ok(1)); + assert_eq!(img.locate(guid(0xB2)).map(|f| f.partition().index()), Ok(1)); } /// `entry_count` is the table's own byte: 8 entries make a 2-LBA array, whose @@ -769,16 +751,10 @@ fn a_tiny_entry_array_cannot_buy_the_backup_header() { #[test] fn a_list_is_every_used_entry_in_order() { let mut img = Builder::default().build(); - let mut out = [toyos_gpt::Partition { - index: 0, - type_guid: Guid::ZERO, - unique_guid: Guid::ZERO, - first_lba: 0, - last_lba: 0, - }; 8]; + let mut out = [None; 8]; let scan = toyos_gpt::list(&mut img, &mut out).expect("the table parses"); assert_eq!((scan.matched, scan.listed, scan.used_entries), (4, 4, 4)); - let found: Vec<(u32, Guid)> = out[..scan.listed].iter().map(|p| (p.index, p.unique_guid)).collect(); + let found: Vec<(u32, Guid)> = out[..scan.listed].iter().flatten().flatten().map(|p| (p.index(), p.unique_guid())).collect(); assert_eq!( found, vec![(0, guid(0xA1)), (1, guid(0xB2)), (2, guid(0xC3)), (3, guid(0xD4))] diff --git a/toyos-gpt/tests/table/mod.rs b/toyos-gpt/tests/table/mod.rs new file mode 100644 index 0000000000..d1e5816665 --- /dev/null +++ b/toyos-gpt/tests/table/mod.rs @@ -0,0 +1,400 @@ +//! Seeded GPT layouts, value mutations of them, and images with both CRC32s resealed. + +#![allow(dead_code)] + +use toyos_gpt::{crc32, Guid, Sectors}; + +/// splitmix64, so a red run is reproducible from its seed. +pub struct Rng(u64); + +impl Rng { + pub fn new(seed: u64) -> Self { + Self(seed) + } + + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Uniform in `0..n`; `n` is never zero here. + pub fn below(&mut self, n: u64) -> u64 { + self.next() % n + } + + pub fn range(&mut self, lo: u64, hi: u64) -> u64 { + lo + self.below(hi - lo + 1) + } + + pub fn one_in(&mut self, n: u64) -> bool { + self.below(n) == 0 + } + + pub fn pick(&mut self, from: &[T]) -> T { + from[self.below(from.len() as u64) as usize] + } + + pub fn guid(&mut self) -> Guid { + let mut b = [0u8; 16]; + b[..8].copy_from_slice(&self.next().to_le_bytes()); + b[8..].copy_from_slice(&self.next().to_le_bytes()); + b[0] |= 1; + Guid(b) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RawEntry { + pub index: u32, + pub type_guid: Guid, + pub unique: Guid, + pub first: u64, + pub last: u64, +} + +/// One copy of the table, every field as the image will state it. +#[derive(Clone, Debug)] +pub struct Table { + pub revision: u32, + pub header_bytes: u32, + pub reserved: u32, + pub my_lba: u64, + pub first_usable: u64, + pub last_usable: u64, + pub disk_guid: Guid, + pub entry_array_lba: u64, + pub entry_count: u32, + pub entry_bytes: u32, + /// The used entries, at their own indices. + pub entries: Vec, +} + +/// A disk: its geometry, the primary copy, and the backup where it has one. +#[derive(Clone, Debug)] +pub struct Layout { + pub lba_bytes: u32, + pub lba_count: u64, + pub primary: Table, + pub backup: Option, + /// What the device answers for its block count, and the granularity it floors by. + pub reported_lba_count: u64, + pub granularity: u64, + pub mbr_type: u8, + pub mbr_signature: [u8; 2], +} + +/// What a generated layout may be. +pub struct Shape<'a> { + pub lba_sizes: &'a [u32], + pub types: &'a [Guid], + 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. +pub fn valid(rng: &mut Rng, shape: &Shape<'_>) -> Layout { + let lba_bytes = rng.pick(shape.lba_sizes); + let entry_bytes = rng.pick(shape.entry_sizes).min(lba_bytes); + let entry_count = if rng.one_in(3) { 128 } else { rng.range(1, 160) as u32 }; + let array_lbas = (u64::from(entry_count) * u64::from(entry_bytes)).div_ceil(u64::from(lba_bytes)); + let first_usable = 2 + array_lbas + rng.below(3); + let data = if lba_bytes == 512 { rng.range(8, 400) } else { rng.range(8, 48) }; + let lba_count = first_usable + data + array_lbas + 1; + let last_usable = lba_count - 2 - array_lbas; + + let used = rng.below(u64::from(entry_count.min(8)) + 1) as usize; + let mut cuts: Vec = (0..2 * used).map(|_| rng.range(first_usable, last_usable)).collect(); + cuts.sort_unstable(); + let mut indices: Vec = Vec::new(); + while indices.len() < used { + let i = rng.below(u64::from(entry_count)) as u32; + if !indices.contains(&i) { + indices.push(i); + } + } + // Disjoint: each partition starts past the previous one's end. + let mut entries = Vec::new(); + let mut floor = first_usable; + for (k, index) in indices.into_iter().enumerate() { + let first = cuts[2 * k].max(floor); + let last = cuts[2 * k + 1].max(first); + if last > last_usable { + break; + } + floor = last + 1; + entries.push(RawEntry { index, type_guid: rng.pick(shape.types), unique: rng.guid(), first, last }); + } + + let primary = Table { + revision: 0x0001_0000, + header_bytes: 92, + reserved: 0, + my_lba: 1, + first_usable, + last_usable, + disk_guid: rng.guid(), + entry_array_lba: 2, + entry_count, + entry_bytes, + entries, + }; + let backup = (shape.backup && !rng.one_in(3)).then(|| Table { + my_lba: lba_count - 1, + entry_array_lba: lba_count - 1 - array_lbas, + ..primary.clone() + }); + 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, + mbr_type: 0xEE, + mbr_signature: [0x55, 0xAA], + } +} + +/// Values at the edges of what `t` and the disk make meaningful, and past them. +fn edge(rng: &mut Rng, t: &Table, lba_count: u64) -> u64 { + let (fu, lu) = (t.first_usable, t.last_usable); + let near = [ + 0, + 1, + 2, + fu.wrapping_sub(1), + fu, + fu.wrapping_add(1), + lu.wrapping_sub(1), + lu, + lu.wrapping_add(1), + lba_count.wrapping_sub(2), + lba_count.wrapping_sub(1), + lba_count, + lba_count.wrapping_add(1), + u64::from(u32::MAX), + u64::MAX - 1, + u64::MAX, + ]; + match rng.below(4) { + 0 => rng.below(lba_count.max(1)), + 1 => rng.next(), + _ => rng.pick(&near), + } +} + +/// Bend one header field or one entry's value. +fn mutate_table(rng: &mut Rng, t: &mut Table, lba_bytes: u32, lba_count: u64) { + match rng.below(14) { + 0 => t.first_usable = edge(rng, t, lba_count), + 1 => t.last_usable = edge(rng, t, lba_count), + 2 => t.entry_array_lba = edge(rng, t, lba_count), + 3 => { + let random = rng.next() as u32; + t.entry_count = rng.pick(&[0, 1, 2, 3, 4, 7, 8, 127, 128, 129, 1024, 1025, u32::MAX, random]) + } + 4 => t.entry_bytes = rng.pick(&[0, 1, 64, 127, 128, 192, 256, 512, 1024, 4096, 8192, u32::MAX]), + 5 => t.header_bytes = rng.pick(&[0, 91, 92, 93, 128, lba_bytes, lba_bytes + 1, u32::MAX]), + 6 => t.my_lba = rng.pick(&[0, 1, 2, lba_count - 1, lba_count, u64::MAX]), + 7 => t.revision = rng.pick(&[0x0001_0000, 0x0001_0001, 0, u32::MAX]), + 8 => t.reserved = rng.pick(&[0, 1, u32::MAX]), + _ => mutate_entry(rng, t, lba_count), + } +} + +fn mutate_entry(rng: &mut Rng, t: &mut Table, lba_count: u64) { + if t.entries.is_empty() || rng.one_in(6) { + let index = rng.below(u64::from(t.entry_count.clamp(1, 256))) as u32; + t.entries.retain(|e| e.index != index); + let (first, last) = (edge(rng, t, lba_count), edge(rng, t, lba_count)); + t.entries.push(RawEntry { index, type_guid: Guid::EFI_SYSTEM, unique: rng.guid(), first, last }); + return; + } + let k = rng.below(t.entries.len() as u64) as usize; + let other = t.entries[rng.below(t.entries.len() as u64) as usize]; + let value = edge(rng, t, lba_count); + let e = &mut t.entries[k]; + match rng.below(9) { + 0 => e.first = value, + 1 => e.last = value, + // The finding's own shape, and its `+ 1` twin. + 2 => (e.first, e.last) = (e.last, e.first), + 3 => (e.first, e.last) = (e.last.wrapping_add(1), e.last), + 4 => (e.first, e.last) = (0, u64::MAX), + // Onto a neighbour's blocks. + 5 => (e.first, e.last) = (other.last, other.last.max(e.last)), + 6 => e.unique = other.unique, + 7 => e.type_guid = Guid::ZERO, + _ => e.type_guid = Guid::TOYOS_DATA, + } +} + +/// Bend 1 to 3 values of the layout, mostly of the primary copy. +pub fn mutate(rng: &mut Rng, layout: &mut Layout) { + for _ in 0..rng.range(1, 3) { + let (lba_bytes, lba_count) = (layout.lba_bytes, layout.lba_count); + match rng.below(40) { + 0 | 1 => layout.reported_lba_count = edge(rng, &layout.primary, lba_count).min(lba_count + 64), + 2 => layout.mbr_type = rng.pick(&[0x00, 0x07]), + 3 => layout.mbr_signature = [0x55, 0xAB], + 4..=11 if layout.backup.is_some() => { + let backup = layout.backup.as_mut().expect("checked"); + mutate_table(rng, backup, lba_bytes, lba_count) + } + _ => mutate_table(rng, &mut layout.primary, lba_bytes, lba_count), + } + } +} + +/// The image `layout` states, with both copies' CRCs computed over it. +pub fn image(layout: &Layout) -> Image { + let lba = layout.lba_bytes as usize; + let mut disk = vec![0u8; lba * layout.lba_count as usize]; + disk[446 + 4] = layout.mbr_type; + disk[446 + 8..446 + 12].copy_from_slice(&1u32.to_le_bytes()); + disk[446 + 12..446 + 16].copy_from_slice(&u32::try_from(layout.lba_count - 1).unwrap_or(u32::MAX).to_le_bytes()); + disk[510..512].copy_from_slice(&layout.mbr_signature); + + // Each copy at its own place whatever its `my_lba` says; the primary last. + let copies: Vec<(u64, &Table)> = + layout.backup.iter().map(|b| (layout.lba_count - 1, b)).chain([(1, &layout.primary)]).collect(); + for (_, t) in &copies { + let stride = (t.entry_bytes as usize).max(128); + for e in &t.entries { + let at = (t.entry_array_lba as usize) + .checked_mul(lba) + .and_then(|a| a.checked_add(e.index as usize * stride)); + let Some(slot) = at.and_then(|at| disk.get_mut(at..at + 48)) else { continue }; + slot[..16].copy_from_slice(&e.type_guid.0); + slot[16..32].copy_from_slice(&e.unique.0); + slot[32..40].copy_from_slice(&e.first.to_le_bytes()); + slot[40..48].copy_from_slice(&e.last.to_le_bytes()); + } + } + for &(header_lba, t) in &copies { + let at = header_lba as usize * lba; + let mut h = vec![0u8; lba]; + h[..8].copy_from_slice(b"EFI PART"); + h[8..12].copy_from_slice(&t.revision.to_le_bytes()); + h[12..16].copy_from_slice(&t.header_bytes.to_le_bytes()); + h[20..24].copy_from_slice(&t.reserved.to_le_bytes()); + h[24..32].copy_from_slice(&t.my_lba.to_le_bytes()); + let alternate = if header_lba == 1 { layout.lba_count - 1 } else { 1 }; + h[32..40].copy_from_slice(&alternate.to_le_bytes()); + h[40..48].copy_from_slice(&t.first_usable.to_le_bytes()); + h[48..56].copy_from_slice(&t.last_usable.to_le_bytes()); + h[56..72].copy_from_slice(&t.disk_guid.0); + h[72..80].copy_from_slice(&t.entry_array_lba.to_le_bytes()); + h[80..84].copy_from_slice(&t.entry_count.to_le_bytes()); + h[84..88].copy_from_slice(&t.entry_bytes.to_le_bytes()); + let array_bytes = u64::from(t.entry_count) * u64::from(t.entry_bytes); + let array = (t.entry_array_lba as usize) + .checked_mul(lba) + .zip(usize::try_from(array_bytes).ok()) + .and_then(|(at, len)| disk.get(at..at.checked_add(len)?)); + if let Some(array) = array { + h[88..92].copy_from_slice(&crc32(array).to_le_bytes()); + } + if let Some(covered) = h.get(..t.header_bytes as usize) { + let crc = crc32(covered); + h[16..20].copy_from_slice(&crc.to_le_bytes()); + } + disk[at..at + lba].copy_from_slice(&h); + } + Image { + lba_bytes: layout.lba_bytes, + lba_count: layout.reported_lba_count, + granularity: layout.granularity, + bytes: disk, + } +} + +pub struct Image { + pub lba_bytes: u32, + pub lba_count: u64, + pub granularity: u64, + pub bytes: Vec, +} + +impl Sectors for Image { + fn lba_bytes(&self) -> u32 { + self.lba_bytes + } + 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 { + let at = (lba as usize).checked_mul(self.lba_bytes as usize); + match at.and_then(|at| self.bytes.get(at..at.checked_add(buf.len())?)) { + Some(src) => { + buf.copy_from_slice(src); + true + } + None => false, + } + } +} + +/// One copy of the table as this file reads it, trusting nothing the parser decided. +#[derive(Debug)] +pub struct OnDisk { + pub first_usable: u64, + pub last_usable: u64, + pub entries: Vec, +} + +impl OnDisk { + /// Every copy an image carries that this file can read. + pub fn both(img: &Image) -> Vec { + let lba = img.lba_bytes as usize; + let last = img.bytes.len() / lba - 1; + [1, last].into_iter().filter_map(|at| OnDisk::at(&img.bytes, lba, at)).collect() + } + + fn at(disk: &[u8], lba: usize, header_lba: usize) -> Option { + let h = disk.get(header_lba * lba..(header_lba + 1) * lba)?; + if &h[..8] != b"EFI PART" { + return None; + } + let u64_at = |b: &[u8], at: usize| u64::from_le_bytes(b[at..at + 8].try_into().expect("8")); + let u32_at = |b: &[u8], at: usize| u32::from_le_bytes(b[at..at + 4].try_into().expect("4")); + let (count, size) = (u32_at(h, 80) as usize, u32_at(h, 84) as usize); + if size < 48 || count.checked_mul(size)? > 1 << 20 { + return None; + } + let array_at = usize::try_from(u64_at(h, 72)).ok()?.checked_mul(lba)?; + let array = disk.get(array_at..array_at.checked_add(count * size)?)?; + let entries = array + .chunks_exact(size) + .enumerate() + .map(|(i, e)| RawEntry { + index: i as u32, + type_guid: Guid(e[..16].try_into().expect("16")), + unique: Guid(e[16..32].try_into().expect("16")), + first: u64_at(e, 32), + last: u64_at(e, 40), + }) + .filter(|e| !e.type_guid.is_zero()) + .collect(); + Some(OnDisk { first_usable: u64_at(h, 40), last_usable: u64_at(h, 48), entries }) + } + + pub fn entry(&self, index: u32) -> Option<&RawEntry> { + self.entries.iter().find(|e| e.index == index) + } + + /// Whether `e`'s blocks are a range inside this copy's usable blocks. + pub fn places(&self, e: &RawEntry) -> bool { + self.first_usable <= e.first && e.first <= e.last && e.last <= self.last_usable + } +} diff --git a/userland/blockd/src/main.rs b/userland/blockd/src/main.rs index c6a18a9319..1a5d0802d9 100644 --- a/userland/blockd/src/main.rs +++ b/userland/blockd/src/main.rs @@ -125,37 +125,34 @@ impl toyos_gpt::Sectors for Sectors<'_> { fn read_table(ctrl: &mut Controller) -> Vec { let per = BLOCK_BYTES as u64 / ctrl.lba_bytes as u64; let mut sectors = Sectors { ctrl, block: vec![0u8; BLOCK_BYTES] }; - const BLANK: toyos_gpt::Partition = toyos_gpt::Partition { - index: 0, - type_guid: toyos_gpt::Guid([0; 16]), - unique_guid: toyos_gpt::Guid([0; 16]), - first_lba: 0, - last_lba: 0, - }; - let mut found = [BLANK; 128]; - let scan = match toyos_gpt::list(&mut sectors, &mut found) { - Ok(scan) => scan, - Err(why) => { - println!("blockd: the disk carries no partition table this driver reads: {why:?}"); - return Vec::new(); - } - }; + let mut found = [None; 128]; + if let Err(why) = toyos_gpt::list(&mut sectors, &mut found) { + println!("blockd: the disk carries no partition table this driver reads: {why:?}"); + return Vec::new(); + } let mut parts = Vec::new(); - for listed in &found[..scan.listed] { - let unique = listed.unique_guid.0; - let span = match toyos_gpt::locate(&mut sectors, listed.unique_guid) { + for entry in found.iter().flatten() { + let listed = match entry { + Ok(listed) => listed, + Err(unplaced) => { + let span = Err(format!("its blocks are no partition on this disk: {unplaced:?}")); + parts.push(Part { unique: unplaced.unique_guid.0, span }); + continue; + } + }; + let unique = listed.unique_guid().0; + let span = match toyos_gpt::locate(&mut sectors, listed.unique_guid()) { Err(why) => Err(format!("its own table refuses it: {why:?}")), Ok(located) => { - let p = located.partition; - if p.first_lba % per != 0 || p.lba_count() % per != 0 { + let p = located.partition(); + let (first, count) = (p.first_lba(), p.lba_count().get()); + if first % per != 0 || count % per != 0 { Err(format!( - "LBA {}+{} is not whole 4 KiB blocks of {}-byte sectors", - p.first_lba, - p.lba_count(), + "LBA {first}+{count} is not whole 4 KiB blocks of {}-byte sectors", sectors.ctrl.lba_bytes )) } else { - Ok((p.first_lba / per, p.lba_count() / per)) + Ok((first / per, count / per)) } } }; From 4575a20409db992b21a750c2ea212800fe06031a Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 16:56:44 +0200 Subject: [PATCH 2/6] toyos-gpt oracle: every placed partition inside the usable range gpt reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting `place`'s usable-range check left the oracle green: gpt checks no range, so an out-of-range partition agreed. UEFI 2.11 §5.3.3 is now applied to gpt's own decoding of the header, and that mutation reds it. Co-Authored-By: Claude Opus 5.5 --- toyos-gpt/tests/oracle.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/toyos-gpt/tests/oracle.rs b/toyos-gpt/tests/oracle.rs index 69e1df3ec1..21207fc30d 100644 --- a/toyos-gpt/tests/oracle.rs +++ b/toyos-gpt/tests/oracle.rs @@ -59,7 +59,8 @@ enum Theirs { Refused(String), /// Not asked: it allocates the array its header claims before checking it. NotAsked, - Read { disk: Guid, rows: BTreeMap }, + /// `usable` is the header's first and last usable block, as `gpt` decodes them. + Read { disk: Guid, usable: (u64, u64), rows: BTreeMap }, } fn theirs(img: &Image) -> Theirs { @@ -76,6 +77,7 @@ fn theirs(img: &Image) -> Theirs { Err(e) => Theirs::Refused(e.to_string()), Ok(parts) => Theirs::Read { disk: Guid(header.disk_guid.to_bytes_le()), + usable: (header.first_usable, header.last_usable), // `gpt` keys its entries from 1. rows: parts .iter() @@ -116,7 +118,7 @@ fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: & Ok("gpt takes the header CRC over 92 bytes whatever header_size says") } (Ok(_), Theirs::Refused(why)) => Err(format!("toyos-gpt read the table, and gpt refused it: {why}")), - (Ok(o), Theirs::Read { disk, rows }) => { + (Ok(o), Theirs::Read { disk, usable, rows }) => { if t.entry_bytes != 128 { return Ok("gpt strides 128 bytes whatever the entry size"); } @@ -126,6 +128,10 @@ fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: & let mut named = "agree"; for (index, row) in rows { match (o.placed.get(index), o.unplaced.get(index)) { + // UEFI 2.11 §5.3.3 holds every partition inside the usable range. + (Some(mine), None) if mine == row && !(usable.0 <= row.3 && row.3 <= row.4 && row.4 <= usable.1) => { + return Err(format!("toyos-gpt placed {row:?} outside the usable range gpt reads, {usable:?}")); + } (Some(mine), None) if mine == row => {} // A type only toyos-gpt names: gpt answers the zero GUID. (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4) == (&row.0, &row.2, row.3, row.4) => { From 696ec530fcc61cccd0f26b49faf5506f718e7414 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 17:02:33 +0200 Subject: [PATCH 3/6] inspectcase: a backwards GPT entry on the crafted disk, and the kernel lives `inspect dev.*` is SYS_DEVICE_INVENTORY, the call B1 panicked the kernel on. The crafted NVMe disk now states an entry at LBA 500..=400: the inventory lists it nowhere and the kernel's log names the refusal. Co-Authored-By: Claude Opus 5.5 --- tests/common/inspect.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/common/inspect.rs b/tests/common/inspect.rs index 56ff8564ce..0cf683dbe5 100644 --- a/tests/common/inspect.rs +++ b/tests/common/inspect.rs @@ -32,6 +32,8 @@ pub const BOUNDS: &str = "inventory_bounds"; const FREE: &str = "9D1E2F30-4A5B-4C6D-8E7F-0A1B2C3D4E5F"; /// The one init grants test-runner; mirrored in the config. const GRANTED: &str = "B4C5D6E7-F809-4A1B-8C2D-3E4F5A6B7C8D"; +/// An entry whose first block is after its last, which no inventory lists. +const BACKWARDS: &str = "0D5C4B3A-2918-4F7E-8D6C-5B4A39281706"; /// Every path the reader answers for netd on a virtio NIC with a lease. const NET: &[&str] = &[ @@ -64,6 +66,7 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { &[("free", mib, FREE), ("granted", mib, GRANTED)], 96 * mib, )?; + state_backwards(&nvme)?; let config = Path::new(env!("CARGO_MANIFEST_DIR")).join(CONFIG); let options = BootOptions { profile: qemu::Profile::Gop, nvme_image: Some(nvme), ..Default::default() }; @@ -80,6 +83,30 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { Ok(qemu) } +/// Add [`BACKWARDS`] to the table of the disk at `path`, both CRCs resealed. +fn state_backwards(path: &Path) -> Result<(), String> { + let mut disk = gpt::GptConfig::new() + .writable(true) + .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) + .open(path) + .map_err(|e| format!("open the crafted disk: {e}"))?; + let mut parts = disk.partitions().clone(); + parts.insert(disk.find_next_partition_id(), gpt::partition::Partition { + part_type_guid: gpt::partition_types::Type { + guid: super::partclaim::PLAIN_TYPE, + os: gpt::partition_types::OperatingSystem::None, + }, + part_guid: uuid::Uuid::parse_str(BACKWARDS).map_err(|e| format!("{BACKWARDS}: {e}"))?, + first_lba: 500, + last_lba: 400, + flags: 0, + name: "backwards".into(), + }); + disk.update_partitions(parts).map_err(|e| format!("state the backwards entry: {e}"))?; + disk.write().map_err(|e| format!("write the table: {e}"))?; + Ok(()) +} + /// The `path = value` lines of a job's output, and nothing else the console /// carried while it ran. fn answer(result: &TestResult) -> BTreeMap { @@ -374,5 +401,12 @@ fn inventory(qemu: &mut QemuInstance) -> Result<(), String> { if holders(&got, granted) != ["test-runner"] { return Err(format!("`{line}`: {granted} is held by {:?}, not test-runner", holders(&got, granted))); } + if got.values().any(|v| *v == BACKWARDS.to_ascii_lowercase()) { + return Err(format!("`{line}` lists {BACKWARDS}, whose first block is after its last")); + } + let refused = format!("({BACKWARDS}) at LBA 500..=400, whose blocks are no partition on it"); + if !format!("{}{}", qemu.uart_log(), qemu.boot_log()).contains(&refused) { + return Err(format!("the kernel did not say it refused {BACKWARDS}")); + } Ok(()) } From 61ab220140f364add8dbca97ee39170c0cd6ba38 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 17:06:31 +0200 Subject: [PATCH 4/6] inspectcase: state the backwards entry by hand, so DATA stays on the disk The `gpt` crate reads TOYOS-DATA's type back as unused, so rewriting the table through it handed DATA's slot to the backwards entry and dropped DATA. The entry is now written into the first free slot of both copies and both CRCs resealed; toyos-gpt reads the result as three partitions and one Unplaced. Co-Authored-By: Claude Opus 5.5 --- tests/common/inspect.rs | 50 ++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/tests/common/inspect.rs b/tests/common/inspect.rs index 0cf683dbe5..a1404a9d47 100644 --- a/tests/common/inspect.rs +++ b/tests/common/inspect.rs @@ -83,28 +83,36 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { Ok(qemu) } -/// Add [`BACKWARDS`] to the table of the disk at `path`, both CRCs resealed. +/// Write [`BACKWARDS`] into the first free entry of both copies of the table of +/// the 512-byte-block disk at `path`, both CRCs of each resealed. fn state_backwards(path: &Path) -> Result<(), String> { - let mut disk = gpt::GptConfig::new() - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .open(path) - .map_err(|e| format!("open the crafted disk: {e}"))?; - let mut parts = disk.partitions().clone(); - parts.insert(disk.find_next_partition_id(), gpt::partition::Partition { - part_type_guid: gpt::partition_types::Type { - guid: super::partclaim::PLAIN_TYPE, - os: gpt::partition_types::OperatingSystem::None, - }, - part_guid: uuid::Uuid::parse_str(BACKWARDS).map_err(|e| format!("{BACKWARDS}: {e}"))?, - first_lba: 500, - last_lba: 400, - flags: 0, - name: "backwards".into(), - }); - disk.update_partitions(parts).map_err(|e| format!("state the backwards entry: {e}"))?; - disk.write().map_err(|e| format!("write the table: {e}"))?; - Ok(()) + use std::io::{Read, Seek, SeekFrom, Write}; + let guid = |text: &str| uuid::Uuid::parse_str(text).map(|u| u.to_bytes_le()).map_err(|e| format!("{text}: {e}")); + let (ty, unique) = (guid(super::partclaim::PLAIN_TYPE)?, guid(BACKWARDS)?); + let mut disk = std::fs::OpenOptions::new().read(true).write(true).open(path).map_err(|e| format!("open: {e}"))?; + let lbas = disk.metadata().map_err(|e| format!("stat: {e}"))?.len() / 512; + let seek = |disk: &mut std::fs::File, lba: u64| disk.seek(SeekFrom::Start(lba * 512)).map(drop); + for header_lba in [1, lbas - 1] { + let mut header = [0u8; 512]; + seek(&mut disk, header_lba).and_then(|()| disk.read_exact(&mut header)).map_err(|e| format!("read: {e}"))?; + let word = |at: usize| u32::from_le_bytes(header[at..at + 4].try_into().expect("four bytes")); + let array_lba = u64::from_le_bytes(header[72..80].try_into().expect("eight bytes")); + let (entry_bytes, header_bytes) = (word(84) as usize, word(12) as usize); + let mut array = vec![0u8; word(80) as usize * entry_bytes]; + seek(&mut disk, array_lba).and_then(|()| disk.read_exact(&mut array)).map_err(|e| format!("read: {e}"))?; + let free = array.chunks_mut(entry_bytes).find(|e| e[..16] == [0; 16]).ok_or("the table has no free entry")?; + free[..16].copy_from_slice(&ty); + free[16..32].copy_from_slice(&unique); + free[32..40].copy_from_slice(&500u64.to_le_bytes()); + free[40..48].copy_from_slice(&400u64.to_le_bytes()); + header[88..92].copy_from_slice(&toyos_gpt::crc32(&array).to_le_bytes()); + header[16..20].fill(0); + let crc = toyos_gpt::crc32(&header[..header_bytes]); + header[16..20].copy_from_slice(&crc.to_le_bytes()); + seek(&mut disk, array_lba).and_then(|()| disk.write_all(&array)).map_err(|e| format!("write: {e}"))?; + seek(&mut disk, header_lba).and_then(|()| disk.write_all(&header)).map_err(|e| format!("write: {e}"))?; + } + disk.sync_all().map_err(|e| format!("sync: {e}")) } /// The `path = value` lines of a job's output, and nothing else the console From f6b675cb480e94b52c4fcedd1d770d0301ae9274 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 17:51:17 +0200 Subject: [PATCH 5/6] toyos-gpt: review round 1 of #547 The CRC is table-driven again. The table is built by a split_first_mut walk and read with `get`, so the crate's lint line still forbids indexing and nothing in it is allowed locally. On this host's aarch64 and x86_64 the lookup compiles to the loop the indexed table did, with no `None` branch. A scan's slice is cleared before it is filled. Two parse.rs tests now hold that: a slice of stale entries listed over, and a primary whose extra entry and broken array CRC are retried against a good backup. Removing `out.fill(None)` turns both red. One image builder: parse.rs builds its fixed disks as a `table::Layout`, which gains `Table::mirror` and a failing read, and loses its own `Builder`. `Unplaced` and `Stated` are one public type, `Stated`; `place` hands it back when an entry's blocks are no partition. `TypeScan.listed` is gone, since every caller can derive it, and so are `MIN_LBA_BYTES` and `MAX_LBA_BYTES`, whose reason moves to `LbaSize`. The kernel logs an unplaced entry once, in `list`, not again in `collect`. The bootloader carries `Located` into `read_root`, so the overlap proof reaches the one read that depends on it. The oracle's usable-range arm is cut: it held toyos-gpt to its own predicate. inspectcase states its backwards entry on a USB stick (`Profile::GopUsbDisk`, `partclaim::craft_stick`), sealed by `volumes::rewrite_gpt`, and no longer crafts an NVMe disk. Co-Authored-By: Claude Opus 5.5 --- bootloader/src/rootimage.rs | 10 +- kernel/src/gpt.rs | 40 +--- kernel/src/inventory.rs | 5 +- src/image.rs | 5 +- tests/common/inspect.rs | 46 ++-- tests/common/partclaim.rs | 2 +- tests/common/qemu.rs | 5 + tests/common/volumes.rs | 2 +- tests/inspectcase/system.toml | 2 +- toyos-gpt/src/crc32.rs | 29 ++- toyos-gpt/src/lib.rs | 73 ++---- toyos-gpt/tests/oracle.rs | 12 +- toyos-gpt/tests/parse.rs | 439 +++++++++++----------------------- toyos-gpt/tests/table/mod.rs | 20 +- 14 files changed, 247 insertions(+), 443 deletions(-) diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index efd5725f79..3609e088ba 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -21,7 +21,7 @@ use alloc::alloc::Layout; use alloc::string::String; use core::num::NonZeroU64; -use toyos_gpt::{Guid, Partition, Sectors}; +use toyos_gpt::{Guid, Located, Sectors}; use toyos_rootimage::chunk; use toyos_update::slots::{self, Table}; use uefi::prelude::*; @@ -178,9 +178,8 @@ impl<'a> Disk<'a> { /// The partition `guid` names on this disk, checked against the table as /// `toyos_gpt::locate` checks every one. - pub fn locate(&mut self, guid: [u8; 16]) -> Result { + pub fn locate(&mut self, guid: [u8; 16]) -> Result { toyos_gpt::locate(self, Guid(guid)) - .map(|located| located.partition()) .map_err(|e| alloc::format!("partition {} on the boot disk: {e:?}", Guid(guid))) } @@ -196,7 +195,7 @@ impl<'a> Disk<'a> { } (n, _) => return Err(alloc::format!("the boot disk carries {n} slot tables, and a machine has one")), }; - let part = self.locate(listed.unique_guid().0)?; + let part = self.locate(listed.unique_guid().0)?.partition(); let lbas = BLOCK as u64 / u64::from(self.lba_bytes); if part.lba_count().get() < slots::COPIES * lbas { return Err(alloc::format!("the slot table's partition is {} blocks, short of its two copies", part.lba_count())); @@ -223,7 +222,8 @@ impl<'a> Disk<'a> { /// resets the machine, if the firmware honours it. What shows where it /// stopped is the slot's line before the read and the attempt count the /// next pass reads. - pub fn read_root(&mut self, bs: &BootServices, part: &Partition, len: u64) -> Result { + pub fn read_root(&mut self, bs: &BootServices, part: &Located, len: u64) -> Result { + let part = part.partition(); let capacity = part.lba_count().get().saturating_mul(u64::from(self.lba_bytes)); if len > capacity || !len.is_multiple_of(BLOCK as u64) { return Err(alloc::format!("{len} bytes of ROOT do not fit whole in its {capacity}-byte partition")); diff --git a/kernel/src/gpt.rs b/kernel/src/gpt.rs index f564a676eb..e54d473758 100644 --- a/kernel/src/gpt.rs +++ b/kernel/src/gpt.rs @@ -66,9 +66,8 @@ static DATA: Lock> = Lock::new(Vec::new()); /// partition claim is looked for on. Taken alone. static DISKS: Lock> = Lock::new(Vec::new()); -/// Every entry each disk's table stated when [`probe`] read it, for the -/// inventory: a table is outside every partition, so nothing a holder writes -/// changes it, and nothing here reads a disk again to answer. +/// A table is outside every partition, so nothing a holder writes changes it, +/// and nothing here reads a disk again to answer. static LISTED: Lock> = Lock::new(Vec::new()); /// One disk's entries, as [`probe`] listed them. @@ -83,9 +82,8 @@ struct Listed { /// when it is probed. const MAX_LISTED: usize = 128; -/// Every GPT entry on every disk [`probe`] read, and who holds exactly its -/// span now, from the block layer's holds. A partition that is not whole -/// blocks is held by nothing, since no view can be made of it. +/// A partition that is not whole blocks is held by nothing, since no view can +/// be made of it. pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { let listed: Vec<(Handle, u32, Vec)> = LISTED .lock() @@ -112,11 +110,10 @@ fn list(sectors: &mut DeviceSectors<'_>, handle: &Handle, lba_bytes: u32) { // A disk with no table this kernel parses carries no partition, and // `collect` says so, naming the refusal. let Ok(scan) = toyos_gpt::list(sectors, &mut found) else { return }; - if scan.matched as usize > scan.listed { + if scan.matched as usize > MAX_LISTED { log!( - "gpt: device {id} carries {} partitions and the inventory lists {}", - scan.matched, - scan.listed + "gpt: device {id} carries {} partitions and the inventory lists {MAX_LISTED}", + scan.matched ); } let mut parts = Vec::new(); @@ -304,27 +301,14 @@ fn collect( return; } }; - if scan.matched as usize > scan.listed { + if scan.matched as usize > MAX_PER_DEVICE { log!( - "gpt: device {id} carries {} {what} partitions and this kernel looks at {}", - scan.matched, - scan.listed + "gpt: device {id} carries {} {what} partitions and this kernel looks at {MAX_PER_DEVICE}", + scan.matched ); } - for entry in found.iter().flatten() { - let candidate = match entry { - Ok(candidate) => candidate, - Err(unplaced) => { - log!( - "gpt: device {id} names a {what} {} at LBA {}..={}, whose blocks are no \ - partition on it", - unplaced.unique_guid, - unplaced.first, - unplaced.last - ); - continue; - } - }; + // An entry whose blocks are no partition was logged once, by `list`. + for candidate in found.iter().flatten().flatten() { let checked = match toyos_gpt::locate(sectors, candidate.unique_guid()) { Ok(located) => located.partition(), Err(e) => { diff --git a/kernel/src/inventory.rs b/kernel/src/inventory.rs index a7f8ed5c8c..c5796b6ed1 100644 --- a/kernel/src/inventory.rs +++ b/kernel/src/inventory.rs @@ -3,9 +3,8 @@ //! //! **Assembled from what each subsystem already keeps**: the PCI functions and //! their identities are `pcidev`'s from enumeration, the USB devices are the -//! ones the xHCI driver bound, the block devices are the ones registered, and -//! the partitions are what each disk's table stated when `gpt::probe` listed -//! it. A partition's state is the block layer's hold on exactly its span, the +//! ones the xHCI driver bound, and the block devices are the ones registered. +//! A partition's state is the block layer's hold on exactly its span, the //! record every view is refused against. //! //! **A holder is found where its handle is**: every process's table is walked diff --git a/src/image.rs b/src/image.rs index 0c5cc8b6ef..a88169a794 100644 --- a/src/image.rs +++ b/src/image.rs @@ -340,8 +340,7 @@ pub fn slot_table_of(file: &mut std::fs::File) -> Result Result<(u64, u64), String> { let mut out = [None; 16]; let scan = toyos_gpt::list(&mut FileSectors(file), &mut out) @@ -353,7 +352,7 @@ pub fn partition_extent(file: &mut std::fs::File, guid: [u8; 16]) -> Result<(u64 let found: Vec<&toyos_gpt::Entry> = out.iter().flatten().filter(|entry| unique(entry) == toyos_gpt::Guid(guid)).collect(); match found[..] { - [Ok(p)] if scan.listed == scan.matched as usize => { + [Ok(p)] if scan.matched as usize <= out.len() => { Ok((p.first_lba() * u64::from(LBA), p.lba_count().get() * u64::from(LBA))) } [Err(unplaced)] => { diff --git a/tests/common/inspect.rs b/tests/common/inspect.rs index a1404a9d47..767c623acd 100644 --- a/tests/common/inspect.rs +++ b/tests/common/inspect.rs @@ -6,7 +6,7 @@ //! matches too much is a path in the answer this file did not name, and one //! that matches too little is a named path missing from it. Values are judged //! where the machine fixes them — QEMU's user network leases `10.0.2.15/24`, -//! nothing plays audio until `inspect_plays` does, and the NVMe disk this file +//! nothing plays audio until `inspect_plays` does, and the USB stick this file //! crafts has one partition free and one init grants — and read only for shape //! elsewhere. @@ -59,17 +59,13 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { if bins.len() != 3 { return Err(format!("{DENIED}, {PLAYS} and {BOUNDS} were not all built")); } - let nvme = super::lane::dir().join("inspect-disk.img"); + let stick = super::lane::dir().join("inspect-stick.img"); let mib = 1024 * 1024; - super::partclaim::craft_plain_disk( - &nvme, - &[("free", mib, FREE), ("granted", mib, GRANTED)], - 96 * mib, - )?; - state_backwards(&nvme)?; + super::partclaim::craft_stick(&stick, 8 * mib, &[("free", mib, FREE), ("granted", mib, GRANTED)])?; + state_backwards(&stick)?; let config = Path::new(env!("CARGO_MANIFEST_DIR")).join(CONFIG); let options = - BootOptions { profile: qemu::Profile::Gop, nvme_image: Some(nvme), ..Default::default() }; + BootOptions { profile: qemu::Profile::GopUsbDisk, usb_images: vec![stick], ..Default::default() }; let argv = qemu::profile_argv(&options); if !argv.iter().any(|a| a.contains("virtio-net")) || !argv.iter().any(|a| a.contains("virtio-sound")) { return Err("this test needs a virtio NIC and a virtio sound card".to_string()); @@ -83,36 +79,22 @@ pub fn boot(rust_bins: &[(String, Vec)]) -> Result { Ok(qemu) } -/// Write [`BACKWARDS`] into the first free entry of both copies of the table of -/// the 512-byte-block disk at `path`, both CRCs of each resealed. +/// Write [`BACKWARDS`] into the first free entry of the table of the disk at +/// `path`, both copies resealed. fn state_backwards(path: &Path) -> Result<(), String> { - use std::io::{Read, Seek, SeekFrom, Write}; let guid = |text: &str| uuid::Uuid::parse_str(text).map(|u| u.to_bytes_le()).map_err(|e| format!("{text}: {e}")); let (ty, unique) = (guid(super::partclaim::PLAIN_TYPE)?, guid(BACKWARDS)?); - let mut disk = std::fs::OpenOptions::new().read(true).write(true).open(path).map_err(|e| format!("open: {e}"))?; - let lbas = disk.metadata().map_err(|e| format!("stat: {e}"))?.len() / 512; - let seek = |disk: &mut std::fs::File, lba: u64| disk.seek(SeekFrom::Start(lba * 512)).map(drop); - for header_lba in [1, lbas - 1] { - let mut header = [0u8; 512]; - seek(&mut disk, header_lba).and_then(|()| disk.read_exact(&mut header)).map_err(|e| format!("read: {e}"))?; - let word = |at: usize| u32::from_le_bytes(header[at..at + 4].try_into().expect("four bytes")); - let array_lba = u64::from_le_bytes(header[72..80].try_into().expect("eight bytes")); - let (entry_bytes, header_bytes) = (word(84) as usize, word(12) as usize); - let mut array = vec![0u8; word(80) as usize * entry_bytes]; - seek(&mut disk, array_lba).and_then(|()| disk.read_exact(&mut array)).map_err(|e| format!("read: {e}"))?; - let free = array.chunks_mut(entry_bytes).find(|e| e[..16] == [0; 16]).ok_or("the table has no free entry")?; + let mut image = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?; + let len = image.len(); + super::volumes::rewrite_gpt(&mut image, len, |entries, entry_bytes| { + let free = entries.chunks_mut(entry_bytes).find(|e| e[..16] == [0; 16]).ok_or("the table has no free entry")?; free[..16].copy_from_slice(&ty); free[16..32].copy_from_slice(&unique); free[32..40].copy_from_slice(&500u64.to_le_bytes()); free[40..48].copy_from_slice(&400u64.to_le_bytes()); - header[88..92].copy_from_slice(&toyos_gpt::crc32(&array).to_le_bytes()); - header[16..20].fill(0); - let crc = toyos_gpt::crc32(&header[..header_bytes]); - header[16..20].copy_from_slice(&crc.to_le_bytes()); - seek(&mut disk, array_lba).and_then(|()| disk.write_all(&array)).map_err(|e| format!("write: {e}"))?; - seek(&mut disk, header_lba).and_then(|()| disk.write_all(&header)).map_err(|e| format!("write: {e}"))?; - } - disk.sync_all().map_err(|e| format!("sync: {e}")) + Ok(()) + })?; + std::fs::write(path, image).map_err(|e| format!("write {}: {e}", path.display())) } /// The `path = value` lines of a job's output, and nothing else the console diff --git a/tests/common/partclaim.rs b/tests/common/partclaim.rs index 6ffcd9f1f3..71bbafcfeb 100644 --- a/tests/common/partclaim.rs +++ b/tests/common/partclaim.rs @@ -760,7 +760,7 @@ pub(super) fn craft_plain_disk( /// A USB stick of `bytes` carrying `parts`, each a name, a length and its /// unique GUID; their spans. -fn craft_stick( +pub(super) fn craft_stick( path: &Path, bytes: u64, parts: &[(&'static str, u64, &'static str)], diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 7bc5712fa1..c6f140a0a1 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1111,6 +1111,9 @@ pub enum Profile { /// the 82574 nor any virtio function does. E1000eBesideIgb, Gop, + /// [`Profile::Gop`] with a second USB stick beside the boot stick, whose + /// table the test writes: the bus a stick of somebody else's arrives on. + GopUsbDisk, /// A virtio-gpu function and no VGA: the owner's own desktop, and the one /// machine where a mode change can succeed rather than answering /// `NotSupported` ahead of everything a resize does. @@ -1397,6 +1400,7 @@ impl Profile { | Self::E1000eNoServer | Self::E1000eBesideIgb | Self::Gop + | Self::GopUsbDisk | Self::VirtioGpu | Self::Metal | Self::MetalNoUsb @@ -1825,6 +1829,7 @@ impl Profile { hda: &[], iommu: Some(IOMMU_DEFAULT), }, + Self::GopUsbDisk => Shape { usb_disks: &[UsbDisk::DATA], ..Self::Gop.shape() }, Self::VirtioGpu => Shape { // No VGA at all: firmware then publishes no GOP, and the one // display the guest has is the one whose mode it can set. diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index 8bc6ae01ce..3273dc8560 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -3427,7 +3427,7 @@ fn entry_lba(entry: &[u8], at: usize) -> u64 { /// the backup array and header moved to the new end, the primary's pointers to /// them and its last usable LBA, both arrays' and both headers' CRCs, and the /// protective MBR's size. `edit` gets the array and one entry's length. -fn rewrite_gpt( +pub(super) fn rewrite_gpt( image: &mut Vec, len: usize, edit: impl FnOnce(&mut [u8], usize) -> Result<(), String>, diff --git a/tests/inspectcase/system.toml b/tests/inspectcase/system.toml index 1c12310260..075d5118e2 100644 --- a/tests/inspectcase/system.toml +++ b/tests/inspectcase/system.toml @@ -1,6 +1,6 @@ # The one boot that runs all four owners `inspect` reads: logd, the compositor, # soundd and netd, on a machine with a framebuffer, a virtio NIC and a virtio -# sound card (`Profile::Gop`), whose NVMe disk `tests/common/inspect.rs` crafts +# sound card (`Profile::GopUsbDisk`), whose USB stick `tests/common/inspect.rs` crafts # with one partition nobody holds and one init grants test-runner. # # test-runner holds no `launcher`, so every binary it runs is spawned directly diff --git a/toyos-gpt/src/crc32.rs b/toyos-gpt/src/crc32.rs index ddbcff43c0..6cda25a924 100644 --- a/toyos-gpt/src/crc32.rs +++ b/toyos-gpt/src/crc32.rs @@ -8,6 +8,25 @@ const POLY: u32 = 0xEDB8_8320; +/// The CRC of each byte value, built by a `split_first_mut` walk so nothing is indexed. +const TABLE: [u32; 256] = { + let mut table = [0u32; 256]; + let mut rest: &mut [u32] = &mut table; + let mut byte = 0u32; + while let Some((slot, tail)) = rest.split_first_mut() { + let mut crc = byte; + let mut bit = 0u32; + while bit < 8 { + crc = if crc & 1 == 0 { crc.wrapping_shr(1) } else { crc.wrapping_shr(1) ^ POLY }; + bit = bit.wrapping_add(1); + } + *slot = crc; + rest = tail; + byte = byte.wrapping_add(1); + } + table +}; + /// A CRC-32 taken over pieces. The partition entry array is read a block at a /// time and never held whole, and the header's own CRC is taken over the /// header with four bytes of itself zeroed — neither is one contiguous slice. @@ -21,12 +40,10 @@ impl Crc32 { pub fn update(&mut self, data: &[u8]) { for &byte in data { - let mut crc = self.0 ^ u32::from(byte); - for _ in 0..8 { - // `POLY` where the bit shifted out is set, zero where it is not. - crc = crc.wrapping_shr(1) ^ (POLY & (crc & 1).wrapping_neg()); - } - self.0 = crc; + let [low, ..] = (self.0 ^ u32::from(byte)).to_le_bytes(); + // A `u8` is below 256: `get` is never `None`. + let entry = TABLE.get(usize::from(low)).copied().unwrap_or(0); + self.0 = self.0.wrapping_shr(8) ^ entry; } } diff --git a/toyos-gpt/src/lib.rs b/toyos-gpt/src/lib.rs index 36df637a8f..73e5862dd5 100644 --- a/toyos-gpt/src/lib.rs +++ b/toyos-gpt/src/lib.rs @@ -41,17 +41,6 @@ use core::num::NonZeroU64; pub use crc32::{crc32, Crc32}; pub use guid::Guid; -/// The block sizes this crate will parse a GPT out of. -/// -/// The floor is the smallest logical block any device has ever reported and -/// the value every GPT in the wild is laid out in; the ceiling is 4Kn, and is -/// also what the rest of this kernel is written in. It matches the NVMe -/// 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. -pub const MIN_LBA_BYTES: u32 = 512; -pub const MAX_LBA_BYTES: u32 = 4096; - /// The largest partition entry array this crate will walk, in bytes. /// /// Policy, not physics, and generous: UEFI requires the array to be at least @@ -167,7 +156,7 @@ impl GptError { } } -/// One partition entry, after it has been checked against the disk it is on. +/// One partition entry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Partition { index: u32, @@ -180,23 +169,16 @@ pub struct Partition { impl Partition { /// `stated`, if its blocks are a partition inside `header`'s usable range. - fn place(stated: &Stated, header: &Header) -> Result { - let unplaced = Unplaced { - index: stated.index, - type_guid: stated.type_guid, - unique_guid: stated.unique_guid, - first: stated.first, - last: stated.last, - }; + fn place(stated: Stated, header: &Header) -> Result { if stated.first < header.first_usable_lba || stated.last > header.last_usable_lba { - return Err(unplaced); + return Err(stated); } let lba_count = stated .last .checked_sub(stated.first) .and_then(|span| span.checked_add(1)) .and_then(NonZeroU64::new) - .ok_or(unplaced)?; + .ok_or(stated)?; Ok(Self { index: stated.index, type_guid: stated.type_guid, @@ -240,9 +222,10 @@ impl Partition { } } -/// An entry that exists and whose blocks are no partition on its disk. +/// One used entry as the table states it, before anything is proven of its +/// blocks. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Unplaced { +pub struct Stated { pub index: u32, pub type_guid: Guid, pub unique_guid: Guid, @@ -250,8 +233,9 @@ pub struct Unplaced { pub last: u64, } -/// One used entry a scan hands back. -pub type Entry = Result; +/// One used entry a scan hands back: `Err` where its blocks are no partition +/// on its disk. +pub type Entry = Result; /// A located partition plus what the table around it looked like. /// @@ -301,17 +285,6 @@ struct Header { entry_array_crc: u32, } -/// One used entry as the table states it, before anything is proven of its -/// blocks. -#[derive(Debug, Clone, Copy)] -struct Stated { - index: u32, - type_guid: Guid, - unique_guid: Guid, - first: u64, - last: u64, -} - /// Find the partition carrying `target` on `dev`. /// /// Reads only. The order is not negotiable: the protective MBR, then the @@ -374,12 +347,10 @@ fn scan( } } -/// What [`locate_type`] found: `matched` is how many entries carried the type, -/// `listed` how many of those fit the caller's slice. +/// What [`locate_type`] found: `matched` is how many entries carried the type. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TypeScan { pub matched: u32, - pub listed: usize, pub disk_guid: Guid, /// Entries with a non-zero type GUID, i.e. partitions that exist. pub used_entries: u32, @@ -395,6 +366,13 @@ struct Disk { /// A logical block size this crate parses, so one block is a fixed prefix of /// a [`Block`]. +/// +/// The floor is the smallest logical block any device has ever reported and +/// the value every GPT in the wild is laid out in; the ceiling is 4Kn, and is +/// also what the rest of this kernel is written in. It matches the NVMe +/// 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)] enum LbaSize { B512, @@ -467,7 +445,6 @@ fn scan_type_at( out: &mut [Option], ) -> Result { out.fill(None); - let capacity = out.len(); let mut block: Block = [0; 4096]; let block = disk.lba.of_block(&mut block); @@ -485,12 +462,11 @@ fn scan_type_at( } matched = matched.saturating_add(1); if let Some(slot) = slots.next() { - *slot = Some(Partition::place(&stated, &header)); + *slot = Some(Partition::place(stated, &header)); } })?; - let listed = usize::try_from(matched).map_or(capacity, |matched| matched.min(capacity)); - Ok(TypeScan { matched, listed, disk_guid: header.disk_guid, used_entries }) + Ok(TypeScan { matched, disk_guid: header.disk_guid, used_entries }) } /// `locate`'s work against one header, primary or backup — read it, check it, @@ -511,8 +487,8 @@ fn locate_at( let Some(stated) = found else { return Err(GptError::NotFound { used_entries }); }; - let partition = Partition::place(&stated, &header) - .map_err(|unplaced| GptError::PartitionRange { first: unplaced.first, last: unplaced.last })?; + let partition = Partition::place(stated, &header) + .map_err(|stated| GptError::PartitionRange { first: stated.first, last: stated.last })?; check_no_overlap(dev, &header, &partition, disk.lba)?; Ok(Located { partition, disk_guid: header.disk_guid, used_entries }) @@ -827,16 +803,15 @@ fn le_u64(buf: &[u8], at: usize) -> Option { mod tests { use super::*; - /// The public bounds and the sizes [`LbaSize`] admits are one range. + /// The sizes [`LbaSize`] admits are one range. #[test] fn the_block_sizes_parsed_are_the_declared_range() { let parsed: [u32; 4] = [512, 1024, 2048, 4096]; - for bytes in 0..=2 * MAX_LBA_BYTES { + for bytes in 0..=8192 { let admitted = LbaSize::of(bytes).map(LbaSize::bytes); assert_eq!(admitted.is_some(), parsed.contains(&bytes), "{bytes}"); assert_eq!(admitted.unwrap_or(bytes), bytes); } - assert_eq!((parsed[0], parsed[3]), (MIN_LBA_BYTES, MAX_LBA_BYTES)); let mut block: Block = [0; 4096]; for lba in [LbaSize::B512, LbaSize::B1024, LbaSize::B2048, LbaSize::B4096] { assert_eq!(lba.of_block(&mut block).len() as u32, lba.bytes()); diff --git a/toyos-gpt/tests/oracle.rs b/toyos-gpt/tests/oracle.rs index 21207fc30d..998c115ef6 100644 --- a/toyos-gpt/tests/oracle.rs +++ b/toyos-gpt/tests/oracle.rs @@ -38,7 +38,7 @@ struct Ours { fn ours(img: &mut Image) -> Result { let mut out = [None; 64]; let scan = toyos_gpt::list(img, &mut out)?; - assert_eq!(scan.listed, scan.matched as usize, "a table with more entries than this file's slice"); + assert!(scan.matched as usize <= out.len(), "a table with more entries than this file's slice"); let mut read = Ours { disk: scan.disk_guid, placed: BTreeMap::new(), unplaced: BTreeMap::new() }; for entry in out.iter().flatten() { match entry { @@ -59,8 +59,7 @@ enum Theirs { Refused(String), /// Not asked: it allocates the array its header claims before checking it. NotAsked, - /// `usable` is the header's first and last usable block, as `gpt` decodes them. - Read { disk: Guid, usable: (u64, u64), rows: BTreeMap }, + Read { disk: Guid, rows: BTreeMap }, } fn theirs(img: &Image) -> Theirs { @@ -77,7 +76,6 @@ fn theirs(img: &Image) -> Theirs { Err(e) => Theirs::Refused(e.to_string()), Ok(parts) => Theirs::Read { disk: Guid(header.disk_guid.to_bytes_le()), - usable: (header.first_usable, header.last_usable), // `gpt` keys its entries from 1. rows: parts .iter() @@ -118,7 +116,7 @@ fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: & Ok("gpt takes the header CRC over 92 bytes whatever header_size says") } (Ok(_), Theirs::Refused(why)) => Err(format!("toyos-gpt read the table, and gpt refused it: {why}")), - (Ok(o), Theirs::Read { disk, usable, rows }) => { + (Ok(o), Theirs::Read { disk, rows }) => { if t.entry_bytes != 128 { return Ok("gpt strides 128 bytes whatever the entry size"); } @@ -128,10 +126,6 @@ fn differ(layout: &Layout, img: &Image, ours: &Result, theirs: & let mut named = "agree"; for (index, row) in rows { match (o.placed.get(index), o.unplaced.get(index)) { - // UEFI 2.11 §5.3.3 holds every partition inside the usable range. - (Some(mine), None) if mine == row && !(usable.0 <= row.3 && row.3 <= row.4 && row.4 <= usable.1) => { - return Err(format!("toyos-gpt placed {row:?} outside the usable range gpt reads, {usable:?}")); - } (Some(mine), None) if mine == row => {} // A type only toyos-gpt names: gpt answers the zero GUID. (Some(mine), None) if row.1 == unnamed && (&mine.0, &mine.2, mine.3, mine.4) == (&row.0, &row.2, row.3, row.4) => { diff --git a/toyos-gpt/tests/parse.rs b/toyos-gpt/tests/parse.rs index e3c9af819a..fbdbc1d967 100644 --- a/toyos-gpt/tests/parse.rs +++ b/toyos-gpt/tests/parse.rs @@ -6,7 +6,10 @@ //! none of the broken ones panics, allocates by a number the disk chose, or //! returns a partition anyway. -use toyos_gpt::{crc32, GptError, Guid, Located, Sectors}; +mod table; + +use table::{Image, Layout, RawEntry, Table}; +use toyos_gpt::{GptError, Guid, Located, Stated}; const LBA: u32 = 512; const ENTRY: u32 = 128; @@ -26,183 +29,53 @@ fn guid(n: u8) -> Guid { Guid(b) } -#[derive(Clone, Copy)] -struct Entry { - type_guid: Guid, - unique: Guid, - first: u64, - last: u64, -} - -impl Entry { - fn new(type_guid: Guid, unique: Guid, first: u64, last: u64) -> Self { - Self { type_guid, unique, first, last } - } +fn entry(index: u32, type_guid: Guid, unique: Guid, first: u64, last: u64) -> RawEntry { + RawEntry { index, type_guid, unique, first, last } } -struct Builder { - lba_bytes: u32, - lba_count: u64, - entry_count: u32, - entry_bytes: u32, - entry_array_lba: u64, - first_usable: u64, - last_usable: u64, - revision: u32, - header_bytes: u32, - reserved: u32, - my_lba: u64, - entries: Vec, - hybrid_mbr: bool, - no_mbr_signature: bool, - /// Write a second, independent copy of the header and the array at the - /// top of the device — LBA `lba_count - 1` and just below it — the way a - /// real disk carries one. `false` by default and unchanged by it: every - /// test above this field was written against a disk with no backup at - /// all, and adding one silently would let a fallback this crate does not - /// yet have paper over a primary this suite meant to break. - backup: bool, -} - -impl Default for Builder { - fn default() -> Self { - Self { - lba_bytes: LBA, - lba_count: DISK_LBAS, - entry_count: 128, - entry_bytes: ENTRY, - entry_array_lba: ARRAY_LBA, - first_usable: FIRST_USABLE, - last_usable: DISK_LBAS - FIRST_USABLE, +/// The disk every test here breaks one thing of, `edit`ed, with no backup +/// unless the edit [`mirrored`] one: a fallback must not paper over a primary +/// a test meant to break. +fn disk(edit: impl FnOnce(&mut Layout)) -> Image { + let mut layout = Layout { + lba_bytes: LBA, + lba_count: DISK_LBAS, + primary: Table { revision: 0x0001_0000, header_bytes: 92, reserved: 0, my_lba: 1, + first_usable: FIRST_USABLE, + last_usable: DISK_LBAS - FIRST_USABLE, + disk_guid: guid(0x5D), + entry_array_lba: ARRAY_LBA, + entry_count: 128, + entry_bytes: ENTRY, entries: vec![ // Two ESP-typed decoys before the real one, and the real one // is neither first nor an obvious pick: a matcher that keys on // the type GUID, or takes the first used entry, or takes the // biggest, gets a different answer than the one asserted. - Entry::new(TYPE_ESP, guid(0xA1), 40, 99), - Entry::new(TYPE_OTHER, guid(0xB2), 100, 199), - Entry::new(TYPE_ESP, guid(0xC3), 200, 299), - Entry::new(TYPE_ESP, guid(0xD4), 300, 1999), + entry(0, TYPE_ESP, guid(0xA1), 40, 99), + entry(1, TYPE_OTHER, guid(0xB2), 100, 199), + entry(2, TYPE_ESP, guid(0xC3), 200, 299), + entry(3, TYPE_ESP, guid(0xD4), 300, 1999), ], - hybrid_mbr: false, - no_mbr_signature: false, - backup: false, - } - } -} - -impl Builder { - fn build(&self) -> Image { - let lba = self.lba_bytes as usize; - let mut disk = vec![0u8; lba * self.lba_count as usize]; - - if !self.no_mbr_signature { - disk[510] = 0x55; - disk[511] = 0xAA; - } - disk[446 + 4] = 0xEE; - disk[446 + 8..446 + 12].copy_from_slice(&1u32.to_le_bytes()); - disk[446 + 12..446 + 16].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); - if self.hybrid_mbr { - disk[446 + 16 + 4] = 0x83; - } - - // A header may name an array LBA this disk does not have; the parser - // is what has to notice, so the builder just writes no entries. - let array_at = (self.entry_array_lba as usize).saturating_mul(lba).min(disk.len()); - for (i, e) in self.entries.iter().enumerate() { - let at = array_at + i * self.entry_bytes as usize; - if at + 128 > disk.len() { - break; - } - disk[at..at + 16].copy_from_slice(&e.type_guid.0); - disk[at + 16..at + 32].copy_from_slice(&e.unique.0); - disk[at + 32..at + 40].copy_from_slice(&e.first.to_le_bytes()); - disk[at + 40..at + 48].copy_from_slice(&e.last.to_le_bytes()); - } - - let array_bytes = (self.entry_count as usize).saturating_mul(self.entry_bytes as usize); - let array_crc = if array_at.saturating_add(array_bytes) <= disk.len() { - crc32(&disk[array_at..array_at + array_bytes]) - } else { - 0 - }; - - let mut h = vec![0u8; lba]; - h[..8].copy_from_slice(b"EFI PART"); - h[8..12].copy_from_slice(&self.revision.to_le_bytes()); - h[12..16].copy_from_slice(&self.header_bytes.to_le_bytes()); - h[20..24].copy_from_slice(&self.reserved.to_le_bytes()); - h[24..32].copy_from_slice(&self.my_lba.to_le_bytes()); - h[32..40].copy_from_slice(&(self.lba_count - 1).to_le_bytes()); - h[40..48].copy_from_slice(&self.first_usable.to_le_bytes()); - h[48..56].copy_from_slice(&self.last_usable.to_le_bytes()); - h[56..72].copy_from_slice(&guid(0x5D).0); - h[72..80].copy_from_slice(&self.entry_array_lba.to_le_bytes()); - h[80..84].copy_from_slice(&self.entry_count.to_le_bytes()); - h[84..88].copy_from_slice(&self.entry_bytes.to_le_bytes()); - h[88..92].copy_from_slice(&array_crc.to_le_bytes()); - let size = (self.header_bytes as usize).min(lba).max(16); - let crc = crc32(&h[..size]); - h[16..20].copy_from_slice(&crc.to_le_bytes()); - disk[lba..lba * 2].copy_from_slice(&h); - - if self.backup { - // The backup array sits directly below the backup header, at the - // top of the device — the mirror of the primary's layout, where - // the array follows the header. Same entries, same CRC: this - // builds an honest mirror, not an independent second table, because the - // point of `backup` is a torn front recovering from an intact - // back, not two disks disagreeing. - let array_lbas = (self.entry_count as u64 * self.entry_bytes as u64).div_ceil(lba as u64); - let backup_header_lba = self.lba_count - 1; - let backup_array_lba = backup_header_lba - array_lbas; - let backup_array_at = (backup_array_lba as usize).saturating_mul(lba); - for (i, e) in self.entries.iter().enumerate() { - let at = backup_array_at + i * self.entry_bytes as usize; - if at + 128 > disk.len() { - break; - } - disk[at..at + 16].copy_from_slice(&e.type_guid.0); - disk[at + 16..at + 32].copy_from_slice(&e.unique.0); - disk[at + 32..at + 40].copy_from_slice(&e.first.to_le_bytes()); - disk[at + 40..at + 48].copy_from_slice(&e.last.to_le_bytes()); - } - let backup_array_crc = crc32(&disk[backup_array_at..backup_array_at + array_bytes]); - - let mut hb = vec![0u8; lba]; - hb[..8].copy_from_slice(b"EFI PART"); - hb[8..12].copy_from_slice(&self.revision.to_le_bytes()); - hb[12..16].copy_from_slice(&self.header_bytes.to_le_bytes()); - hb[20..24].copy_from_slice(&self.reserved.to_le_bytes()); - hb[24..32].copy_from_slice(&backup_header_lba.to_le_bytes()); - hb[32..40].copy_from_slice(&1u64.to_le_bytes()); // AlternateLBA: the primary, at LBA 1. - hb[40..48].copy_from_slice(&self.first_usable.to_le_bytes()); - hb[48..56].copy_from_slice(&self.last_usable.to_le_bytes()); - hb[56..72].copy_from_slice(&guid(0x5D).0); - hb[72..80].copy_from_slice(&backup_array_lba.to_le_bytes()); - hb[80..84].copy_from_slice(&self.entry_count.to_le_bytes()); - hb[84..88].copy_from_slice(&self.entry_bytes.to_le_bytes()); - hb[88..92].copy_from_slice(&backup_array_crc.to_le_bytes()); - let hb_crc = crc32(&hb[..size]); - hb[16..20].copy_from_slice(&hb_crc.to_le_bytes()); - let backup_header_at = backup_header_lba as usize * lba; - disk[backup_header_at..backup_header_at + lba].copy_from_slice(&hb); - } - - Image { lba_bytes: self.lba_bytes, lba_count: self.lba_count, bytes: disk, fail_at: None } - } + }, + backup: None, + reported_lba_count: DISK_LBAS, + granularity: 1, + mbr_type: 0xEE, + mbr_signature: [0x55, 0xAA], + }; + edit(&mut layout); + layout.reported_lba_count = layout.lba_count; + table::image(&layout) } -struct Image { - lba_bytes: u32, - lba_count: u64, - bytes: Vec, - fail_at: Option, +/// A backup that mirrors the primary as it stands. +fn mirrored(layout: &mut Layout) { + layout.backup = Some(layout.primary.mirror(layout.lba_bytes, layout.lba_count)); } impl Image { @@ -221,32 +94,9 @@ impl Image { } } -impl Sectors for Image { - fn lba_bytes(&self) -> u32 { - self.lba_bytes - } - fn lba_count(&self) -> u64 { - self.lba_count - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::MIN - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - if self.fail_at == Some(lba) { - return false; - } - let at = lba as usize * self.lba_bytes as usize; - let Some(src) = self.bytes.get(at..at + buf.len()) else { - return false; - }; - buf.copy_from_slice(src); - true - } -} - #[test] fn finds_the_partition_by_unique_guid() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let found = img.locate(guid(0xC3)).expect("the table has this GUID"); assert_eq!(found.partition().index(), 2); assert_eq!(found.partition().first_lba(), 200); @@ -269,7 +119,7 @@ fn each_guid_finds_its_own_entry() { (guid(0xD4), 3, 300, 1999), ]; for (g, index, first, last) in want { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let found = img.locate(g).expect("present"); assert_eq!((found.partition().index(), found.partition().first_lba(), found.partition().last_lba()), (index, first, last)); } @@ -280,27 +130,28 @@ fn each_guid_finds_its_own_entry() { /// gets a different set than the one asserted. #[test] fn a_type_scan_lists_every_entry_of_that_type() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let mut out = [None; 4]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed, scan.used_entries), (3, 3, 4)); + assert_eq!((scan.matched, scan.used_entries), (3, 4)); assert_eq!(scan.disk_guid, guid(0x5D)); - let found: Vec = out[..scan.listed].iter().flatten().flatten().map(|p| p.unique_guid()).collect(); + let found: Vec = out.iter().flatten().flatten().map(|p| p.unique_guid()).collect(); assert_eq!(found, vec![guid(0xA1), guid(0xC3), guid(0xD4)]); // A type nothing carries is not an error; it is an empty set. let none = img.locate_type(guid(0x77), &mut out).expect("the table parses"); - assert_eq!((none.matched, none.listed), (0, 0)); + assert_eq!(none.matched, 0); + assert!(out.iter().all(Option::is_none)); } /// A slice too small does not truncate silently: the count of matches is the /// table's, not the caller's, so the caller can tell it was not shown them all. #[test] fn a_type_scan_says_how_many_it_could_not_hand_back() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let mut out = [None; 1]; let scan = img.locate_type(TYPE_ESP, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed), (3, 1)); + assert_eq!(scan.matched, 3); assert_eq!(out[0].and_then(Result::ok).map(|p| p.unique_guid()), Some(guid(0xA1))); } @@ -308,7 +159,7 @@ fn a_type_scan_says_how_many_it_could_not_hand_back() { /// candidates at all, rather than the ones read before the checksum failed. #[test] fn a_type_scan_over_a_damaged_array_is_refused() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(ARRAY_LBA, 3) ^= 0x01; let mut out = [None; 4]; assert!(matches!( @@ -319,22 +170,22 @@ fn a_type_scan_over_a_damaged_array_is_refused() { #[test] fn absent_guid_is_not_found_and_says_how_many_there_were() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); assert_eq!(img.locate(guid(0xEE)), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn a_zero_guid_matches_nothing() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); assert_eq!(img.locate(Guid::ZERO), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn no_protective_mbr() { - let mut img = Builder { no_mbr_signature: true, ..Default::default() }.build(); + let mut img = disk(|l| l.mbr_signature = [0, 0]); assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(0, 446 + 4) = 0x07; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); } @@ -342,46 +193,47 @@ fn no_protective_mbr() { /// A protective record next to a real one means two tables describe this disk. #[test] fn hybrid_mbr_is_refused() { - let mut img = Builder { hybrid_mbr: true, ..Default::default() }.build(); + let mut img = disk(|_| {}); + *img.at(0, 446 + 16 + 4) = 0x83; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoProtectiveMbr)); } #[test] fn header_signature() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(1, 0) = b'X'; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoHeader)); } #[test] fn header_revision() { - let mut img = Builder { revision: 0x0002_0000, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.revision = 0x0002_0000); assert_eq!(img.locate(guid(0xC3)), Err(GptError::UnsupportedRevision(0x0002_0000))); } #[test] fn header_size_bounds() { for bad in [0u32, 91, 513, u32::MAX] { - let mut img = Builder { header_bytes: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.header_bytes = bad); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderSize(bad)), "header_size {bad}"); } } #[test] fn header_reserved_word() { - let mut img = Builder { reserved: 1, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.reserved = 1); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderReserved(1))); } #[test] fn header_must_claim_lba_one() { - let mut img = Builder { my_lba: 2, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.my_lba = 2); assert_eq!(img.locate(guid(0xC3)), Err(GptError::HeaderMisplaced(2))); } #[test] fn one_flipped_bit_in_the_header() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(1, 80) ^= 0x01; match img.locate(guid(0xC3)) { Err(GptError::HeaderCrc { .. }) => {} @@ -391,7 +243,7 @@ fn one_flipped_bit_in_the_header() { #[test] fn one_flipped_bit_in_the_entry_array() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); *img.at(ARRAY_LBA, 32) ^= 0x01; match img.locate(guid(0xC3)) { Err(GptError::EntryArrayCrc { .. }) => {} @@ -404,7 +256,7 @@ fn one_flipped_bit_in_the_entry_array() { /// not be read as an entry either. #[test] fn the_array_ends_where_the_header_says() { - let mut img = Builder { entry_count: 3, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = 3); // Entry 3 is now outside the array. It is still on the disk. assert_eq!(img.locate(guid(0xD4)), Err(GptError::NotFound { used_entries: 3 })); *img.at(ARRAY_LBA, 3 * 128 + 1) ^= 0xFF; @@ -415,7 +267,7 @@ fn the_array_ends_where_the_header_says() { #[test] fn entry_size_must_be_a_power_of_two_multiple_of_128_that_fits_a_block() { for bad in [0u32, 1, 64, 127, 192, 1024, u32::MAX] { - let mut img = Builder { entry_bytes: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_bytes = bad); assert_eq!(img.locate(guid(0xC3)), Err(GptError::EntrySize(bad)), "entry size {bad}"); } } @@ -423,7 +275,7 @@ fn entry_size_must_be_a_power_of_two_multiple_of_128_that_fits_a_block() { #[test] fn a_billion_entries_is_refused_not_read() { for bad in [u32::MAX, 1_000_000_000, 1025] { - let mut img = Builder { entry_count: bad, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = bad); assert_eq!( img.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { entries: bad, entry_size: ENTRY }), @@ -437,23 +289,20 @@ fn a_billion_entries_is_refused_not_read() { #[test] fn the_array_ceiling_is_where_it_says_it_is() { let at_ceiling = (toyos_gpt::MAX_ENTRY_ARRAY_BYTES / ENTRY as u64) as u32; - let over = Builder { entry_count: at_ceiling + 1, ..Default::default() }.build(); - let mut over = over; + let mut over = disk(|l| l.primary.entry_count = at_ceiling + 1); assert!(matches!(over.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { .. }))); - let mut ok = Builder { - entry_count: at_ceiling, - first_usable: 2 + toyos_gpt::MAX_ENTRY_ARRAY_BYTES / LBA as u64, - ..Default::default() - } - .build(); + let mut ok = disk(|l| { + l.primary.entry_count = at_ceiling; + l.primary.first_usable = 2 + toyos_gpt::MAX_ENTRY_ARRAY_BYTES / LBA as u64; + }); // Not TooBig: it is refused, if at all, for a different reason. assert!(!matches!(ok.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { .. }))); } #[test] fn zero_entries_is_refused() { - let mut img = Builder { entry_count: 0, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_count = 0); assert_eq!( img.locate(guid(0xC3)), Err(GptError::EntryArrayTooBig { entries: 0, entry_size: ENTRY }) @@ -463,38 +312,38 @@ fn zero_entries_is_refused() { #[test] fn the_array_may_not_sit_on_the_header_or_past_the_usable_range() { for bad_lba in [0u64, 1] { - let mut img = Builder { entry_array_lba: bad_lba, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_array_lba = bad_lba); assert!( matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. })), "array at LBA {bad_lba}" ); } // Starts legally, ends past the first usable block. - let mut img = Builder { first_usable: 20, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.first_usable = 20); assert!(matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. }))); } #[test] fn an_array_lba_near_the_top_of_the_range_does_not_wrap() { - let mut img = Builder { entry_array_lba: u64::MAX - 1, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.entry_array_lba = u64::MAX - 1); assert!(matches!(img.locate(guid(0xC3)), Err(GptError::EntryArrayMisplaced { .. }))); } #[test] fn usable_range_must_be_a_range_inside_the_device() { - let mut inverted = Builder { first_usable: 500, last_usable: 100, ..Default::default() }.build(); + let mut inverted = disk(|l| { l.primary.first_usable = 500; l.primary.last_usable = 100 }); assert_eq!( inverted.locate(guid(0xC3)), Err(GptError::UsableRange { first: 500, last: 100 }) ); - let mut past_end = Builder { last_usable: DISK_LBAS, ..Default::default() }.build(); + let mut past_end = disk(|l| l.primary.last_usable = DISK_LBAS); assert_eq!( past_end.locate(guid(0xC3)), Err(GptError::UsableRange { first: FIRST_USABLE, last: DISK_LBAS }) ); - let mut over_the_table = Builder { first_usable: 1, ..Default::default() }.build(); + let mut over_the_table = disk(|l| l.primary.first_usable = 1); assert_eq!( over_the_table.locate(guid(0xC3)), Err(GptError::UsableRange { first: 1, last: DISK_LBAS - FIRST_USABLE }) @@ -503,9 +352,7 @@ fn usable_range_must_be_a_range_inside_the_device() { #[test] fn a_partition_outside_the_disk_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 200, u64::MAX); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 200, u64::MAX)); assert_eq!( img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 200, last: u64::MAX }) @@ -514,9 +361,7 @@ fn a_partition_outside_the_disk_is_refused() { #[test] fn a_backwards_partition_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 900, 800); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 900, 800)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 900, last: 800 })); } @@ -524,24 +369,18 @@ fn a_backwards_partition_is_refused() { /// the caller's next act is to write to it. #[test] fn a_partition_over_the_table_is_refused() { - let mut b = Builder::default(); - b.entries[2] = Entry::new(TYPE_ESP, guid(0xC3), 3, 299); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[2] = entry(2, TYPE_ESP, guid(0xC3), 3, 299)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionRange { first: 3, last: 299 })); } #[test] fn an_overlapping_neighbour_is_refused() { - let mut b = Builder::default(); - b.entries[3] = Entry::new(TYPE_OTHER, guid(0xD4), 250, 400); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[3] = entry(3, TYPE_OTHER, guid(0xD4), 250, 400)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionOverlap { index: 3 })); // And the overlap is found when it comes *before* the match too, which is // the case a single streaming pass would miss. - let mut b = Builder::default(); - b.entries[0] = Entry::new(TYPE_OTHER, guid(0xA1), 40, 250); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[0] = entry(0, TYPE_OTHER, guid(0xA1), 40, 250)); assert_eq!(img.locate(guid(0xC3)), Err(GptError::PartitionOverlap { index: 0 })); } @@ -551,7 +390,7 @@ fn an_overlapping_neighbour_is_refused() { /// than refuse a disk that is otherwise fine. #[test] fn a_damaged_primary_falls_back_to_a_good_backup() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); *img.at(1, 0) = b'X'; let found = img.locate(guid(0xC3)).expect("the backup carries this GUID"); assert_eq!(found.partition().index(), 2); @@ -567,7 +406,7 @@ fn a_damaged_primary_falls_back_to_a_good_backup() { /// was unreadable. #[test] fn both_copies_damaged_is_refused_by_name() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); *img.at(1, 0) = b'X'; *img.at(DISK_LBAS - 1, 0) = b'X'; assert_eq!(img.locate(guid(0xC3)), Err(GptError::NoHeader)); @@ -578,14 +417,14 @@ fn both_copies_damaged_is_refused_by_name() { /// alone, so a `NotFound` is not in the set of errors this falls back on. #[test] fn a_valid_primary_that_lacks_the_guid_is_not_retried_against_the_backup() { - let mut img = Builder { backup: true, ..Default::default() }.build(); + let mut img = disk(mirrored); assert_eq!(img.locate(guid(0xEE)), Err(GptError::NotFound { used_entries: 4 })); } #[test] fn a_read_that_does_not_happen_is_an_error() { for lba in [0u64, 1, ARRAY_LBA] { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.fail_at = Some(lba); assert_eq!(img.locate(guid(0xC3)), Err(GptError::ReadFailed(lba))); } @@ -594,7 +433,7 @@ fn a_read_that_does_not_happen_is_an_error() { #[test] fn block_sizes_outside_the_supported_range() { for bad in [0u32, 128, 500, 8192, u32::MAX] { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.lba_bytes = bad; assert_eq!(img.locate(guid(0xC3)), Err(GptError::UnsupportedLbaSize(bad))); } @@ -602,19 +441,11 @@ fn block_sizes_outside_the_supported_range() { #[test] fn a_four_kibibyte_block_device_parses() { - let mut img = Builder { - lba_bytes: 4096, - lba_count: 512, - entry_array_lba: 2, - first_usable: 6, - last_usable: 500, - entries: vec![ - Entry::new(TYPE_OTHER, guid(0x11), 10, 20), - Entry::new(TYPE_ESP, guid(0x22), 21, 400), - ], - ..Default::default() - } - .build(); + let mut img = disk(|l| { + (l.lba_bytes, l.lba_count) = (4096, 512); + (l.primary.first_usable, l.primary.last_usable) = (6, 500); + l.primary.entries = vec![entry(0, TYPE_OTHER, guid(0x11), 10, 20), entry(1, TYPE_ESP, guid(0x22), 21, 400)]; + }); let found = img.locate(guid(0x22)).expect("present"); assert_eq!((found.partition().index(), found.partition().first_lba()), (1, 21)); assert_eq!(found.used_entries(), 2); @@ -622,7 +453,7 @@ fn a_four_kibibyte_block_device_parses() { #[test] fn a_device_with_no_room_for_a_table() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); img.lba_count = 2; assert_eq!(img.locate(guid(0xC3)), Err(GptError::DeviceTooSmall(2))); } @@ -633,7 +464,7 @@ fn a_device_with_no_room_for_a_table() { /// simply *return*. #[test] fn no_byte_of_the_table_can_panic_the_parser() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let reach = (ARRAY_LBA as usize + 32) * LBA as usize; let mut located = 0; for at in 0..reach { @@ -657,9 +488,11 @@ fn no_byte_of_the_table_can_panic_the_parser() { /// exact reader concedes none of them for a coarser reader it does not have. #[test] fn a_usable_range_reaching_the_backup_gpt_is_refused() { - let mut b = Builder { last_usable: DISK_LBAS - 2, backup: true, ..Default::default() }; - b.entries[3] = Entry::new(TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 2); - let mut img = b.build(); + let mut img = disk(|l| { + l.primary.last_usable = DISK_LBAS - 2; + l.primary.entries[3] = entry(3, TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 2); + mirrored(l); + }); assert_eq!( img.locate(guid(0xD4)), Err(GptError::UsableRangeCoversBackup { @@ -668,9 +501,9 @@ fn a_usable_range_reaching_the_backup_gpt_is_refused() { }) ); - let mut img = Builder { last_usable: DISK_LBAS - 34, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.last_usable = DISK_LBAS - 34); img.locate(guid(0xC3)).expect("the last usable LBA below the mirror was refused"); - let mut img = Builder { last_usable: DISK_LBAS - 33, ..Default::default() }.build(); + let mut img = disk(|l| l.primary.last_usable = DISK_LBAS - 33); assert_eq!( img.locate(guid(0xC3)), Err(GptError::UsableRangeCoversBackup { @@ -686,31 +519,13 @@ fn a_usable_range_reaching_the_backup_gpt_is_refused() { /// bound's edge, must parse. The unconceded bound refused every such disk. #[test] fn an_honest_table_on_a_floored_device_view_parses() { - struct Floored(Image, u64); - impl Sectors for Floored { - fn lba_bytes(&self) -> u32 { - self.0.lba_bytes() - } - fn lba_count(&self) -> u64 { - self.1 - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(8).expect("8 is nonzero") - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - self.0.read_lba(lba, buf) - } - } - - let img = Builder { - lba_count: 2055, - last_usable: 2055 - 34, - backup: true, - ..Default::default() - } - .build(); - let mut floored = Floored(img, 2048); - let found = toyos_gpt::locate(&mut floored, guid(0xC3)).expect("an honest disk lost /boot"); + 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); } @@ -719,9 +534,7 @@ fn an_honest_table_on_a_floored_device_view_parses() { /// first-wins — either one could be the partition the firmware meant. #[test] fn two_entries_claiming_the_target_guid_are_refused() { - let mut b = Builder::default(); - b.entries[3] = Entry::new(TYPE_OTHER, guid(0xC3), 300, 1999); - let mut img = b.build(); + let mut img = disk(|l| l.primary.entries[3] = entry(3, TYPE_OTHER, guid(0xC3), 300, 1999)); assert_eq!( img.locate(guid(0xC3)), Err(GptError::DuplicateUniqueGuid { first: 2, second: 3 }) @@ -734,9 +547,10 @@ fn two_entries_claiming_the_target_guid_are_refused() { /// first block remains the usable range's exact ceiling. #[test] fn a_tiny_entry_array_cannot_buy_the_backup_header() { - let mut b = Builder { entry_count: 8, last_usable: DISK_LBAS - 1, ..Default::default() }; - b.entries[3] = Entry::new(TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 1); - let mut img = b.build(); + let mut img = disk(|l| { + (l.primary.entry_count, l.primary.last_usable) = (8, DISK_LBAS - 1); + l.primary.entries[3] = entry(3, TYPE_ESP, guid(0xD4), 300, DISK_LBAS - 1); + }); assert_eq!( img.locate(guid(0xD4)), Err(GptError::UsableRangeCoversBackup { @@ -750,13 +564,38 @@ fn a_tiny_entry_array_cannot_buy_the_backup_header() { /// ones, whatever its type. #[test] fn a_list_is_every_used_entry_in_order() { - let mut img = Builder::default().build(); + let mut img = disk(|_| {}); let mut out = [None; 8]; let scan = toyos_gpt::list(&mut img, &mut out).expect("the table parses"); - assert_eq!((scan.matched, scan.listed, scan.used_entries), (4, 4, 4)); - let found: Vec<(u32, Guid)> = out[..scan.listed].iter().flatten().flatten().map(|p| (p.index(), p.unique_guid())).collect(); + assert_eq!((scan.matched, scan.used_entries), (4, 4)); + let found: Vec<(u32, Guid)> = out.iter().flatten().flatten().map(|p| (p.index(), p.unique_guid())).collect(); assert_eq!( found, vec![(0, guid(0xA1)), (1, guid(0xB2)), (2, guid(0xC3)), (3, guid(0xD4))] ); } + +/// A scan clears the caller's slice before it fills it: every slot it did not +/// fill is `None`, whatever the caller left there. +#[test] +fn a_list_leaves_no_slot_it_did_not_fill() { + let mut img = disk(|_| {}); + let bogus = Stated { index: 99, type_guid: TYPE_ESP, unique_guid: guid(0xEE), first: 1, last: 0 }; + let mut out = [Some(Err(bogus)); 8]; + toyos_gpt::list(&mut img, &mut out).expect("the table parses"); + assert_eq!(out.iter().flatten().count(), 4); +} + +/// A primary whose array CRC fails is walked before the failure is known, and +/// retried against the backup: nothing the primary's walk put in the slice +/// survives the retry. +#[test] +fn a_backup_retry_leaves_no_slot_of_the_primary() { + let mut img = disk(mirrored); + // A fifth used entry, in the primary's array only, so its CRC fails. + *img.at(ARRAY_LBA, 4 * 128) = 0x01; + let mut out = [None; 8]; + let scan = toyos_gpt::list(&mut img, &mut out).expect("the backup parses"); + assert_eq!(scan.used_entries, 4); + assert_eq!(out.iter().flatten().count(), 4); +} diff --git a/toyos-gpt/tests/table/mod.rs b/toyos-gpt/tests/table/mod.rs index d1e5816665..4d1c0ebd48 100644 --- a/toyos-gpt/tests/table/mod.rs +++ b/toyos-gpt/tests/table/mod.rs @@ -72,6 +72,14 @@ pub struct Table { pub entries: Vec, } +impl Table { + /// This copy as the backup of a disk of `lba_count` blocks of `lba_bytes` states it. + pub fn mirror(&self, lba_bytes: u32, lba_count: u64) -> Table { + let array_lbas = (u64::from(self.entry_count) * u64::from(self.entry_bytes)).div_ceil(u64::from(lba_bytes)); + Table { my_lba: lba_count - 1, entry_array_lba: lba_count - 1 - array_lbas, ..self.clone() } + } +} + /// A disk: its geometry, the primary copy, and the backup where it has one. #[derive(Clone, Debug)] pub struct Layout { @@ -144,11 +152,7 @@ pub fn valid(rng: &mut Rng, shape: &Shape<'_>) -> Layout { entry_bytes, entries, }; - let backup = (shape.backup && !rng.one_in(3)).then(|| Table { - my_lba: lba_count - 1, - entry_array_lba: lba_count - 1 - array_lbas, - ..primary.clone() - }); + 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, @@ -313,6 +317,7 @@ pub fn image(layout: &Layout) -> Image { lba_count: layout.reported_lba_count, granularity: layout.granularity, bytes: disk, + fail_at: None, } } @@ -321,6 +326,8 @@ pub struct Image { pub lba_count: u64, pub granularity: u64, pub bytes: Vec, + /// The one block whose read does not happen. + pub fail_at: Option, } impl Sectors for Image { @@ -334,6 +341,9 @@ impl Sectors for Image { 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; + } let at = (lba as usize).checked_mul(self.lba_bytes as usize); match at.and_then(|at| self.bytes.get(at..at.checked_add(buf.len())?)) { Some(src) => { From 70b8fc15bbb6c616f45f0b803a7320e3128c4413 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 18:15:12 +0200 Subject: [PATCH 6/6] toyos-gpt: one match for the one partition a scan owes, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/metal.rs's one_partition and src/image.rs's only_partition each ran the same locate_type scan into a three-way match on (matched, out[0]). Pull that match into image::one_partition_of, returning Result where OnePartitionError carries either the Stated of an unplaced entry or the matched count. only_partition turns that into its String; metal::one_partition turns it into Refusal::Table or Refusal::Partitions { what, matched }, keeping the count only metal.rs needed back. Also drops the PR body's unmeasured codegen sentence about aarch64 and x86_64-unknown-none lookup compilation — no objdump or godbolt output backed it. Review round 2 of #547. Co-Authored-By: Claude Opus 5.5 --- src/image.rs | 34 +++++++++++++++++++++++++++++----- src/metal.rs | 11 +++++------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/image.rs b/src/image.rs index a88169a794..43d25814e9 100644 --- a/src/image.rs +++ b/src/image.rs @@ -480,16 +480,40 @@ pub fn unique_guid_of(file: &mut std::fs::File, kind: toyos_gpt::Guid) -> Result only_partition(&mut FileSectors(file), kind).map(|part| part.unique_guid().0) } +/// Why a scan's `out[0]` and `matched` count did not pick out exactly one +/// partition, once the table itself was readable. +pub enum OnePartitionError { + /// The one entry of the wanted type is no partition on this disk. + Unplaced(toyos_gpt::Stated), + /// Not exactly one entry carried the wanted type. + Matched(u32), +} + +/// The one partition a [`toyos_gpt::locate_type`] scan found, out of its +/// [`toyos_gpt::TypeScan`] and the `out[0]` slot it filled — the match shared +/// by every caller that owes exactly one partition of a type and nothing else. +pub(crate) fn one_partition_of( + scan: toyos_gpt::TypeScan, + first: Option, +) -> Result { + match (scan.matched, first) { + (1, Some(Ok(part))) => Ok(part), + (1, Some(Err(unplaced))) => Err(OnePartitionError::Unplaced(unplaced)), + (matched, _) => Err(OnePartitionError::Matched(matched)), + } +} + /// The one partition of type `kind` on `disk`. pub fn only_partition(disk: &mut dyn toyos_gpt::Sectors, kind: toyos_gpt::Guid) -> Result { let mut out = [None; 2]; let scan = toyos_gpt::locate_type(disk, kind, &mut out) .map_err(|e| format!("no readable partition table: {e:?}"))?; - match (scan.matched, out[0]) { - (1, Some(Ok(part))) => Ok(part), - (1, Some(Err(unplaced))) => Err(format!("the one entry of type {kind} is no partition: {unplaced:?}")), - (n, _) => Err(format!("{n} partitions of type {kind}, where one is owed")), - } + one_partition_of(scan, out[0]).map_err(|e| match e { + OnePartitionError::Unplaced(unplaced) => { + format!("the one entry of type {kind} is no partition: {unplaced:?}") + } + OnePartitionError::Matched(n) => format!("{n} partitions of type {kind}, where one is owed"), + }) } /// Overwrite the file `name` on the FAT partition `guid` of the disk image at diff --git a/src/metal.rs b/src/metal.rs index c2c21c9214..dab449ddb9 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -957,13 +957,12 @@ fn one_partition( let mut out = [None; 2]; let scan = toyos_gpt::locate_type(&mut crate::image::FileSectors(file), guid, &mut out) .map_err(|e| Refusal::Table(format!("{e:?}")))?; - let part = match (scan.matched, out[0]) { - (1, Some(Ok(part))) => part, - (1, Some(Err(unplaced))) => { - return Err(Refusal::Table(format!("the {what} entry is no partition: {unplaced:?}"))) + let part = crate::image::one_partition_of(scan, out[0]).map_err(|e| match e { + crate::image::OnePartitionError::Unplaced(unplaced) => { + Refusal::Table(format!("the {what} entry is no partition: {unplaced:?}")) } - (matched, _) => return Err(Refusal::Partitions { what, matched }), - }; + crate::image::OnePartitionError::Matched(matched) => Refusal::Partitions { what, matched }, + })?; Ok(Part { index: part.index() + 1, start: part.first_lba(),