From 652dc573c17d36e9043c7c7f9c2fd96419f5b2e0 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 24 Sep 2026 10:33:20 +0200 Subject: [PATCH 1/3] A partition is a claimable device: the ABI names one and moves its blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's ruling: a GPT partition is claimed exactly as a PCI function is, named by its GPT identity in a `devices` entry, minted by init from its `SysCap` and endowed. This is the half the shared sysroot carries; the kernel here refuses every new call by name, and the implementation, init's live grant and the tests are the second branch. The spelling. `part:` names a partition by its unique partition GUID and `part-type:` as the one partition of a type on the machine; a type more than one entry carries names no partition, refused like two identical NICs. Two device classes (8 and 9) rather than a flag, because the class is what says how the selector words are read. The GUID is the uppercase registry form and nothing else, one spelling per partition as `PciId` has one per card; `PartGuid` holds the sixteen bytes a GPT entry stores (UEFI 2.10 §5.3.3) and the ESP type's text and bytes from the specification are its test. `DeviceRequest::MAX_NAME` goes 16 -> 48 for the longest entry, 46 bytes. The claim. `SYS_DEVICE_CLAIM` already had a fourth argument word it passed as zero; the GUID rides the selector and that word, so no claim syscall is added. The transfers. `SYS_PARTITION_READ` (119) and `SYS_PARTITION_WRITE` (120) take the claim, the partition's own first block, a buffer and a block count. The block numbers are the partition's own from 0, so no argument can spell a device block and a neighbour's blocks have no name; the end is the kernel's refusal. The buffers are typed `[[u8; 4096]]`, so a partial block has no spelling and the count is the slice's length. One call carries at most 32 blocks, the run the kernel's own page cache writes back in. Durability is `SYS_FSYNC` on the claim, the same number and the same promise a file has. A claim describes itself once as `PartitionInfo`: its length in blocks and both GUIDs, so a claim named by type can say which partition it got. `toyos-abi` 0.6.0 -> 0.7.0, `toyos` 0.7.0 -> 0.8.0, and `toyos-window` with them for its pins. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- Cargo.lock | 2 +- bootloader/Cargo.lock | 2 +- kernel/Cargo.lock | 2 +- kernel/src/arch/syscall/device.rs | 6 +- kernel/src/arch/syscall/dispatch.rs | 7 +- kernel/src/device.rs | 13 +- tests/toyos-rust-tests/Cargo.lock | 16 +- toyos-abi/Cargo.toml | 2 +- toyos-abi/src/lib.rs | 1 + toyos-abi/src/part.rs | 219 ++++++++++++++++++++++++++++ toyos-abi/src/syscall.rs | 179 +++++++++++++++++++---- toyos/Cargo.lock | 4 +- toyos/Cargo.toml | 4 +- toyos/src/device.rs | 36 ++++- toyos/src/endow.rs | 9 ++ toyos/src/lib.rs | 2 +- toyos/src/syscap.rs | 13 ++ userland/Cargo.lock | 78 +++++----- userland/init/src/main.rs | 5 +- userland/libc/Cargo.lock | 4 +- userland/toyos-window/Cargo.toml | 6 +- 21 files changed, 509 insertions(+), 101 deletions(-) create mode 100644 toyos-abi/src/part.rs diff --git a/Cargo.lock b/Cargo.lock index 565bb6a54b1..a048cf4df4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1090,7 +1090,7 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" dependencies = [ "rustc-std-workspace-core", ] diff --git a/bootloader/Cargo.lock b/bootloader/Cargo.lock index 21b1ab2a2f4..168d1e2954a 100644 --- a/bootloader/Cargo.lock +++ b/bootloader/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" [[package]] name = "toyos-acpi" diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 548bba66169..8a9640f331e 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -74,7 +74,7 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" [[package]] name = "toyos-acpi" diff --git a/kernel/src/arch/syscall/device.rs b/kernel/src/arch/syscall/device.rs index f192f2fe8f4..8bbb6a3ef5e 100644 --- a/kernel/src/arch/syscall/device.rs +++ b/kernel/src/arch/syscall/device.rs @@ -117,9 +117,9 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: /// Mints a device claim, gated on a `SysCap` carrying [`Rights::DEVICE`]. /// /// `selector` says which device where the class alone does not — a PCI -/// function's vendor and device id — and is ignored by every class that names -/// at most one device on the machine. -pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: u64) -> u64 { +/// function's vendor and device id, a partition's GUID — and is ignored by +/// every class that names at most one device on the machine. +pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2]) -> u64 { let Some(class) = device::DeviceType::from_raw(class) else { return SyscallError::InvalidArgument.to_u64(); }; diff --git a/kernel/src/arch/syscall/dispatch.rs b/kernel/src/arch/syscall/dispatch.rs index 934f53a8824..a6a8938590c 100644 --- a/kernel/src/arch/syscall/dispatch.rs +++ b/kernel/src/arch/syscall/dispatch.rs @@ -425,7 +425,12 @@ pub(super) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a1), a2) else { return bad_addr }; sys_endowments(&mut buf) } - SYS_DEVICE_CLAIM => sys_device_claim(RawHandle(a1 as u32), a2, a3), + SYS_DEVICE_CLAIM => sys_device_claim(RawHandle(a1 as u32), a2, [a3, a4]), + // Numbered in the ABI and not yet served: a caller is told which call it made. + SYS_PARTITION_READ | SYS_PARTITION_WRITE => { + crate::log!("syscall {num} (a partition transfer) is not implemented in this kernel"); + SyscallError::NotSupported.to_u64() + } SYS_RT_ENTER => sys_rt_enter(RawHandle(a1 as u32)), SYS_LOG_READ => { // checked_mul before mapping: a product that doesn't fit is a bad argument, diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 8a0f7cdad46..5fbc4443c3e 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -112,9 +112,10 @@ pub enum ClaimError { /// `Claim` lives on this stack frame until the returned object takes it, so a /// failure after `acquire` cannot leave a device held by nobody. /// -/// `selector` says *which* device where the class alone does not — today that -/// is a PCI function's vendor and device id, and every other class ignores it. -pub fn try_claim(class: DeviceType, selector: u64) -> Result, ClaimError> { +/// `selector` says *which* device where the class alone does not — a PCI +/// function's vendor and device id in its first word, a partition's GUID in +/// both — and every other class ignores it. +pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result, ClaimError> { // Availability is checked before acquiring, so an absent device reports `Absent`, not `Owned`. match class { DeviceType::Keyboard => { @@ -143,7 +144,7 @@ pub fn try_claim(class: DeviceType, selector: u64) -> Result, C Ok(DeviceClaim::new(class, framebuffer_info(screen), claim)) } DeviceType::PciFunction => { - let id = toyos_abi::syscall::PciId::from_wire(selector) + let id = toyos_abi::syscall::PciId::from_wire(selector[0]) .ok_or(ClaimError::Absent)?; // The slot's own guard, taken inside: a PCI claim's exclusivity is // per function rather than per class, so there is no flag here to @@ -151,6 +152,10 @@ pub fn try_claim(class: DeviceType, selector: u64) -> Result, C let (info, slot, claim) = crate::pcidev::claim(id)?; Ok(DeviceClaim::new(class, DeviceInfo::PciFunction(info, slot), claim)) } + DeviceType::Partition | DeviceType::PartitionOfType => { + log!("partclaim: this kernel does not hand out partition claims yet — {} refused", class.class_name()); + Err(ClaimError::Unusable) + } DeviceType::HdaAudio => { let (info, pcm) = crate::drivers::hda::info().ok_or(ClaimError::Absent)?; let claim = Claim::acquire(class)?; diff --git a/tests/toyos-rust-tests/Cargo.lock b/tests/toyos-rust-tests/Cargo.lock index 4f87e1a0eab..8057b230101 100644 --- a/tests/toyos-rust-tests/Cargo.lock +++ b/tests/toyos-rust-tests/Cargo.lock @@ -1442,9 +1442,9 @@ dependencies = [ [[package]] name = "toyos" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "toyos-abi 0.6.0", + "toyos-abi 0.7.0", ] [[package]] @@ -1461,7 +1461,7 @@ checksum = "ec119a115b619cec7ca5e56858e98db6558658d6cc9b400906187e62f2877f48" [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" [[package]] name = "toyos-font" @@ -1481,8 +1481,8 @@ dependencies = [ "rustls-pki-types", "rustls-rustcrypto", "sha2", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-tco", "toyos-window", "ureq", @@ -1495,10 +1495,10 @@ version = "0.1.0" [[package]] name = "toyos-window" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-font", "toyos-keymap", ] diff --git a/toyos-abi/Cargo.toml b/toyos-abi/Cargo.toml index afb2e601dd6..ae95bcf548e 100644 --- a/toyos-abi/Cargo.toml +++ b/toyos-abi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" edition = "2021" license = "MIT OR Apache-2.0" description = "The ToyOS kernel ABI: syscall numbers, struct layouts and typed syscall wrappers." diff --git a/toyos-abi/src/lib.rs b/toyos-abi/src/lib.rs index 09264f0b2e0..580e1acc7b1 100644 --- a/toyos-abi/src/lib.rs +++ b/toyos-abi/src/lib.rs @@ -21,6 +21,7 @@ pub mod hda; pub mod inbox; pub mod input; pub mod log; +pub mod part; pub mod pci; pub mod ring; pub mod syscall; diff --git a/toyos-abi/src/part.rs b/toyos-abi/src/part.rs new file mode 100644 index 00000000000..c16c30ed714 --- /dev/null +++ b/toyos-abi/src/part.rs @@ -0,0 +1,219 @@ +//! What a claim on one GPT partition names and hands its holder. +//! +//! A partition is claimed the way a PCI function is: `/bin/init` mints it from a +//! `system.toml` `devices` entry and endows it, and the holder reads and writes +//! the partition's blocks through [`syscall::partition_read`] and +//! [`syscall::partition_write`], durable at [`syscall::fsync`] on the claim. +//! Every block number here is the **partition's own**, from 0: nothing in this +//! ABI names a device block, so a neighbour's blocks have no spelling. +//! +//! A partition has one holder. Which partitions the kernel holds itself, and so +//! refuses, is `kernel/src/block.rs`'s account. +//! +//! [`syscall::partition_read`]: crate::syscall::partition_read +//! [`syscall::partition_write`]: crate::syscall::partition_write +//! [`syscall::fsync`]: crate::syscall::fsync + +/// The unit every partition transfer is in, and the alignment a partition must +/// have on its device to be claimable at all: a partition that began or ended +/// inside one of these would share a transfer with its neighbour. +pub const BLOCK_BYTES: usize = 4096; + +/// One partition block, as the buffers of [`crate::syscall::partition_read`] +/// and [`crate::syscall::partition_write`] are typed: a partial block has no +/// spelling. +pub type Block = [u8; BLOCK_BYTES]; + +/// The most blocks one transfer call carries: the run the kernel's own page +/// cache writes back in, so a claimant holds its disk no longer per call than +/// the kernel does. +pub const MAX_BLOCKS_PER_CALL: usize = 32; + +/// A GUID as the sixteen bytes a GPT entry stores (UEFI 2.10 §5.3.3), which is +/// also the order the claim syscall carries it in. +/// +/// The text form is the registry form, `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` +/// in **uppercase** hex: the first three groups are little-endian integers on +/// the disk and the last two are bytes in order (UEFI 2.10 Appendix A). One +/// spelling per partition, because every reader of a `devices` entry compares +/// the string: a lowercase twin would be a second name for one partition. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct PartGuid(pub [u8; 16]); + +/// Where each byte of the on-disk form is printed, in text order: the three +/// little-endian groups reversed, the last eight in place. +const TEXT_ORDER: [usize; 16] = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; + +/// The dash positions of the 36-byte text form. +const DASHES: [usize; 4] = [8, 13, 18, 23]; + +/// The length of the text form. +pub const GUID_TEXT_LEN: usize = 36; + +const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF"; + +impl PartGuid { + /// The registry text form, exactly, and nothing else. + pub fn parse(text: &str) -> Option { + let text = text.as_bytes(); + if text.len() != GUID_TEXT_LEN { + return None; + } + let mut digits = [0u8; 32]; + let mut n = 0; + for (at, &byte) in text.iter().enumerate() { + if DASHES.contains(&at) { + if byte != b'-' { + return None; + } + continue; + } + digits[n] = match byte { + b'0'..=b'9' => byte - b'0', + b'A'..=b'F' => byte - b'A' + 10, + _ => return None, + }; + n += 1; + } + let mut bytes = [0u8; 16]; + for (i, &at) in TEXT_ORDER.iter().enumerate() { + bytes[at] = (digits[2 * i] << 4) | digits[2 * i + 1]; + } + Some(Self(bytes)) + } + + /// The registry text form written into `buf`. + pub fn write_text(self, buf: &mut [u8; GUID_TEXT_LEN]) -> &str { + let mut at = 0; + for (i, &byte) in TEXT_ORDER.iter().map(|&b| &self.0[b]).enumerate() { + if i == 4 || i == 6 || i == 8 || i == 10 { + buf[at] = b'-'; + at += 1; + } + buf[at] = HEX_UPPER[(byte >> 4) as usize]; + buf[at + 1] = HEX_UPPER[(byte & 0xF) as usize]; + at += 2; + } + core::str::from_utf8(buf).expect("hex digits and dashes are ASCII") + } + + /// The two selector words the claim syscall carries: bytes 0..8 and + /// 8..16, each little-endian. + pub fn wire(self) -> [u64; 2] { + let mut lo = [0u8; 8]; + let mut hi = [0u8; 8]; + lo.copy_from_slice(&self.0[..8]); + hi.copy_from_slice(&self.0[8..]); + [u64::from_le_bytes(lo), u64::from_le_bytes(hi)] + } + + /// The selector words decoded; every pair of words is some GUID. + pub fn from_wire(words: [u64; 2]) -> Self { + let mut bytes = [0u8; 16]; + bytes[..8].copy_from_slice(&words[0].to_le_bytes()); + bytes[8..].copy_from_slice(&words[1].to_le_bytes()); + Self(bytes) + } +} + +/// Which partition a claim names: by its unique partition GUID, or as **the** +/// partition of a type GUID — the one partition of that type on the machine, +/// since a type that more than one entry carries names no partition and the +/// claim is refused rather than resolved to the first. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PartitionName { + Unique(PartGuid), + OfType(PartGuid), +} + +impl PartitionName { + pub const fn guid(self) -> PartGuid { + match self { + Self::Unique(guid) | Self::OfType(guid) => guid, + } + } +} + +/// The partition a claim holds, as its holder reads it once off the claim. +/// +/// Both GUIDs, because a claim named by type has to be able to say which +/// partition it got, and a claim named by partition to say what it holds. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PartitionInfo { + /// The partition's length in [`BLOCK_BYTES`] blocks; block numbers + /// `0..blocks` are the whole of what the claim addresses. + pub blocks: u64, + pub unique_guid: [u8; 16], + pub type_guid: [u8; 16], +} + +/// Every byte belongs to a field: this crosses the boundary through +/// `as_bytes`, so a gap would publish whatever the kernel stack held. +const _: () = assert!(core::mem::size_of::() == 8 + 16 + 16); + +impl PartitionInfo { + pub fn as_bytes(&self) -> &[u8] { + // SAFETY: `self` is a valid `&Self`, readable for `size_of::()` + // bytes, and the const assert above proves the `repr(C)` layout has no + // padding, so every byte the slice exposes is an initialized field. + unsafe { + core::slice::from_raw_parts( + self as *const Self as *const u8, + core::mem::size_of::(), + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The EFI System Partition type's registry text and its on-disk bytes, from + /// the UEFI specification (§5.3.3, Table 5.8): both directions, because a + /// mixed-endian mistake that is self-consistent survives either one alone. + #[test] + fn the_esp_type_guid_is_the_specification_s_bytes() { + const TEXT: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; + const ON_DISK: [u8; 16] = [ + 0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, + 0xC9, 0x3B, + ]; + assert_eq!(PartGuid::parse(TEXT), Some(PartGuid(ON_DISK))); + let mut buf = [0u8; GUID_TEXT_LEN]; + assert_eq!(PartGuid(ON_DISK).write_text(&mut buf), TEXT); + } + + /// One spelling per partition: every near miss a config could plausibly + /// write is refused, not normalised. + #[test] + fn a_guid_is_the_uppercase_registry_form_and_nothing_else() { + for bad in [ + "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "{C12A7328-F81F-11D2-BA4B-00A0C93EC93B}", + "C12A7328F81F11D2BA4B00A0C93EC93B", + "C12A7328-F81F-11D2-BA4B-00A0C93EC93", + "C12A7328-F81F-11D2-BA4B-00A0C93EC93BB", + "C12A7328-F81F-11D2-BA4B00-A0C93EC93B", + "G12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "C12A7328-F81F-11D2-BA4B-00A0C93EC9é", + "", + ] { + assert_eq!(PartGuid::parse(bad), None, "{bad:?} parsed"); + } + } + + /// The claim carries the sixteen bytes as two words and the kernel takes + /// them apart again; a byte lost to a shift claims a different partition. + #[test] + fn a_guid_survives_the_wire() { + let mut every = [0u8; 16]; + for (i, b) in every.iter_mut().enumerate() { + *b = 0xF0 | i as u8; + } + for guid in [PartGuid(every), PartGuid([0; 16]), PartGuid([0xFF; 16])] { + assert_eq!(PartGuid::from_wire(guid.wire()), guid); + } + } +} diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 8149d85f6d6..9950ced794c 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -288,6 +288,12 @@ pub const SYS_DEVICE_BAR_MAP: u64 = 117; /// [`device_dma_alloc`]. pub const SYS_DEVICE_DMA_ALLOC: u64 = 118; +/// Read blocks of a claimed partition. See [`partition_read`]. +pub const SYS_PARTITION_READ: u64 = 119; + +/// Write blocks of a claimed partition. See [`partition_write`]. +pub const SYS_PARTITION_WRITE: u64 = 120; + /// Bins in the per-process syscall profile — one for every number this ABI /// issues, and one at the end for every number it does not. /// @@ -300,7 +306,7 @@ pub const SYSCALL_PROFILE_BINS: usize = 128; /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_DEVICE_DMA_ALLOC < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_PARTITION_WRITE < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; @@ -970,7 +976,9 @@ pub fn fstat(handle: RawHandle) -> Result { Ok(stat) } -/// Flush a file handle to disk. +/// Flush a file handle to disk, or a partition claim's writes out of its +/// device's cache ([`crate::part`]): `Ok` is the device's own word that every +/// write the claim returned from before this call is durable. pub fn fsync(handle: RawHandle) -> Result<(), SyscallError> { check_unit(syscall(SYS_FSYNC, handle.0 as u64, 0, 0, 0)) } @@ -1188,6 +1196,16 @@ device_classes! { /// `pci::` names which function, and [`DeviceRequest`] is /// the one parser of that spelling. PciFunction = 7 => "pci", + /// One GPT partition, named by its unique partition GUID: the holder reads + /// and writes its blocks and no other's ([`crate::part`]). + /// + /// Like `pci`, a class whose name is not the whole of the entry — + /// `part:` — and [`DeviceRequest`] is the one parser. + Partition = 8 => "part", + /// The same claim, naming the partition as the one partition on the machine + /// of a type GUID: `part-type:`. The claim is the same object; the + /// class says how the selector is to be read. + PartitionOfType = 9 => "part-type", } /// A PCI function named by what identifies the *card*, not the slot firmware @@ -1245,7 +1263,8 @@ fn hex16(text: &str) -> Option { } /// What one `devices` entry asks for: a class, and for -/// [`DeviceType::PciFunction`] which function. +/// [`DeviceType::PciFunction`] which function, and for a partition which +/// partition. /// /// One parser, because four places read the same spelling — the build system's /// gate, `/system/bin/init`'s mint, the kernel's claim and the claimant's own @@ -1255,36 +1274,60 @@ fn hex16(text: &str) -> Option { pub enum DeviceRequest { Class(DeviceType), Pci(PciId), + Partition(crate::part::PartitionName), } impl DeviceRequest { /// The spelling that carries a function id after the class name. pub const PCI_PREFIX: &'static str = "pci:"; - /// The longest a `devices` entry, and so a `dev:` label's tail, can be. - pub const MAX_NAME: usize = 16; + /// The spelling that carries a unique partition GUID. + pub const PART_PREFIX: &'static str = "part:"; + + /// The spelling that carries a partition type GUID. + pub const PART_TYPE_PREFIX: &'static str = "part-type:"; + + /// The longest a `devices` entry, and so a `dev:` label's tail, can be: + /// `part-type:` and a GUID, rounded up to a multiple of eight. + pub const MAX_NAME: usize = 48; pub fn parse(name: &str) -> Option { - match name.strip_prefix(Self::PCI_PREFIX) { - Some(id) => PciId::parse(id).map(Self::Pci), - // A bare `pci` names no function, and `from_class_name` would - // otherwise accept it and leave the selector at zero. - None if name == DeviceType::PciFunction.class_name() => None, - None => DeviceType::from_class_name(name).map(Self::Class), + use crate::part::{PartGuid, PartitionName}; + if let Some(id) = name.strip_prefix(Self::PCI_PREFIX) { + return PciId::parse(id).map(Self::Pci); + } + if let Some(guid) = name.strip_prefix(Self::PART_PREFIX) { + return PartGuid::parse(guid).map(|g| Self::Partition(PartitionName::Unique(g))); + } + if let Some(guid) = name.strip_prefix(Self::PART_TYPE_PREFIX) { + return PartGuid::parse(guid).map(|g| Self::Partition(PartitionName::OfType(g))); + } + let class = DeviceType::from_class_name(name)?; + match class { + // A bare `pci`, `part` or `part-type` names no device, and would + // otherwise leave the selector at zero. + DeviceType::PciFunction | DeviceType::Partition | DeviceType::PartitionOfType => None, + _ => Some(Self::Class(class)), } } pub const fn class(self) -> DeviceType { + use crate::part::PartitionName; match self { Self::Class(class) => class, Self::Pci(_) => DeviceType::PciFunction, + Self::Partition(PartitionName::Unique(_)) => DeviceType::Partition, + Self::Partition(PartitionName::OfType(_)) => DeviceType::PartitionOfType, } } - pub const fn selector(self) -> u64 { + /// The two selector words [`device_claim`] carries; a class that names at + /// most one device on the machine carries none. + pub fn selector(self) -> [u64; 2] { match self { - Self::Class(_) => 0, - Self::Pci(id) => id.wire(), + Self::Class(_) => [0, 0], + Self::Pci(id) => [id.wire(), 0], + Self::Partition(name) => name.guid().wire(), } } @@ -1292,12 +1335,26 @@ impl DeviceRequest { /// [`Self::MAX_NAME`] bounds. The name is also the tail of the `dev:` label /// the claim arrives under, and it is composed here so the two cannot drift. pub fn write_name(self, buf: &mut [u8; Self::MAX_NAME]) -> &str { + use crate::part::{PartitionName, GUID_TEXT_LEN}; let id = match self { Self::Class(class) => { let name = class.class_name(); buf[..name.len()].copy_from_slice(name.as_bytes()); return core::str::from_utf8(&buf[..name.len()]).expect("a class name is ASCII"); } + Self::Partition(name) => { + let prefix = match name { + PartitionName::Unique(_) => Self::PART_PREFIX, + PartitionName::OfType(_) => Self::PART_TYPE_PREFIX, + }; + let at = prefix.len(); + buf[..at].copy_from_slice(prefix.as_bytes()); + let mut text = [0u8; GUID_TEXT_LEN]; + name.guid().write_text(&mut text); + buf[at..at + GUID_TEXT_LEN].copy_from_slice(&text); + return core::str::from_utf8(&buf[..at + GUID_TEXT_LEN]) + .expect("a prefix and a GUID's text are ASCII"); + } Self::Pci(id) => id, }; let prefix = Self::PCI_PREFIX.as_bytes(); @@ -1321,10 +1378,12 @@ const HEX: &[u8; 16] = b"0123456789abcdef"; /// Mint a device claim for `request`, presenting a `SysCap` handle that carries /// [`Rights::DEVICE`]. `NotFound` for a class no driver registered, or a PCI -/// function this machine does not have — init endows what exists and logs what -/// it did not. `AlreadyExists` names one already claimed; a PCI request -/// matching more than one function answers `InvalidArgument`, because an -/// ambiguous name is refused rather than resolved to the first match. +/// function or partition this machine does not have — init endows what exists +/// and logs what it did not. `AlreadyExists` names one already claimed, and +/// `PermissionDenied` one the kernel drives itself — for a partition, one it +/// has mounted. A request matching more than one function or partition answers +/// `InvalidArgument`, because an ambiguous name is refused rather than resolved +/// to the first match. /// /// The claim comes back **without** [`Rights::DUP`], so it can only be moved, /// which is what makes endowing one to a child a provable hand-off. @@ -1332,14 +1391,49 @@ const HEX: &[u8; 16] = b"0123456789abcdef"; /// [`Rights::DEVICE`]: crate::handle::Rights::DEVICE /// [`Rights::DUP`]: crate::handle::Rights::DUP pub fn device_claim(syscap: RawHandle, request: DeviceRequest) -> Result { - check(syscall( - SYS_DEVICE_CLAIM, - syscap.0 as u64, - request.class() as u64, - request.selector(), - 0, + let [lo, hi] = request.selector(); + check(syscall(SYS_DEVICE_CLAIM, syscap.0 as u64, request.class() as u64, lo, hi)) + .map(|v| RawHandle(v as u32)) +} + +/// Read `buf.len()` blocks of the partition `claim` holds, from its block +/// `first` — the partition's own number, from 0. +/// +/// `InvalidArgument` for a run that does not end inside the partition or is +/// longer than [`crate::part::MAX_BLOCKS_PER_CALL`], `Io` for a device that +/// did not do it, and `Gone` for a claim whose last handle has been let go. +pub fn partition_read( + claim: RawHandle, + first: u64, + buf: &mut [crate::part::Block], +) -> Result<(), SyscallError> { + check_unit(syscall( + SYS_PARTITION_READ, + claim.0 as u64, + first, + buf.as_mut_ptr() as u64, + buf.len() as u64, + )) +} + +/// Write `buf.len()` blocks to the partition `claim` holds, from its block +/// `first`, refused as [`partition_read`] is. +/// +/// `Ok` is the device having taken the blocks, and **not** their being +/// durable: that is [`fsync`] on the claim, which a writer that must survive a +/// power cut calls before it tells anyone the write happened. +pub fn partition_write( + claim: RawHandle, + first: u64, + buf: &[crate::part::Block], +) -> Result<(), SyscallError> { + check_unit(syscall( + SYS_PARTITION_WRITE, + claim.0 as u64, + first, + buf.as_ptr() as u64, + buf.len() as u64, )) - .map(|v| RawHandle(v as u32)) } /// Enter the real-time scheduling band, presenting a `SysCap` handle that @@ -2148,12 +2242,12 @@ mod tests { let want = DeviceRequest::Pci(PciId { vendor: 0x1af4, device: 0x1041 }); assert_eq!(DeviceRequest::parse("pci:1af4:1041"), Some(want)); assert_eq!(want.class(), DeviceType::PciFunction); - assert_eq!(want.selector(), 0x1af4_1041); + assert_eq!(want.selector(), [0x1af4_1041, 0]); assert_eq!( DeviceRequest::parse("framebuffer"), Some(DeviceRequest::Class(DeviceType::Framebuffer)) ); - assert_eq!(DeviceRequest::Class(DeviceType::Framebuffer).selector(), 0); + assert_eq!(DeviceRequest::Class(DeviceType::Framebuffer).selector(), [0, 0]); // Every one of these is a name a config could plausibly write and the // kernel must not resolve to a function. for bad in [ @@ -2162,6 +2256,10 @@ mod tests { "pci:1af4", "nic", // the retired class the NIC used to be claimed as "gpu", // a class name this table has never had + "part", // a bare partition class names no partition + "part-type", + "part:", + "part-type:c12a7328-f81f-11d2-ba4b-00a0c93ec93b", // one spelling: uppercase "", ] { assert_eq!(DeviceRequest::parse(bad), None, "{bad:?} parsed"); @@ -2173,13 +2271,38 @@ mod tests { /// holder cannot look up. #[test] fn a_request_writes_back_the_name_it_parsed() { - for name in ["pci:1af4:1041", "pci:8086:15fc", "pci:0000:0000", "hda-audio", "mouse"] { + for name in [ + "pci:1af4:1041", + "pci:8086:15fc", + "pci:0000:0000", + "hda-audio", + "mouse", + "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", + // The longest entry there is, which is what `MAX_NAME` is for. + "part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ] { let request = DeviceRequest::parse(name).expect("a name this table has"); let mut buf = [0u8; DeviceRequest::MAX_NAME]; assert_eq!(request.write_name(&mut buf), name); } } + /// The two partition spellings are two classes over one GUID, so a type + /// row can never be read as the partition whose unique GUID it happens to + /// spell, nor the reverse. + #[test] + fn a_partition_entry_says_which_of_its_guids_it_names() { + use crate::part::{PartGuid, PartitionName}; + let esp = PartGuid::parse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B").expect("the ESP type"); + let by_type = DeviceRequest::parse("part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); + let by_unique = DeviceRequest::parse("part:C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); + assert_eq!(by_type, Some(DeviceRequest::Partition(PartitionName::OfType(esp)))); + assert_eq!(by_unique, Some(DeviceRequest::Partition(PartitionName::Unique(esp)))); + assert_eq!(by_type.map(DeviceRequest::class), Some(DeviceType::PartitionOfType)); + assert_eq!(by_unique.map(DeviceRequest::class), Some(DeviceType::Partition)); + assert_eq!(by_type.map(DeviceRequest::selector), Some(esp.wire())); + } + /// The selector is one word on the wire and the kernel decodes it back; /// a vendor lost to a shift would claim a different card. #[test] diff --git a/toyos/Cargo.lock b/toyos/Cargo.lock index 29364dedc83..3dffe8757e3 100644 --- a/toyos/Cargo.lock +++ b/toyos/Cargo.lock @@ -10,7 +10,7 @@ checksum = "aa9c45b374136f52f2d6311062c7146bff20fec063c3f5d46a410bd937746955" [[package]] name = "toyos" -version = "0.7.0" +version = "0.8.0" dependencies = [ "rustc-std-workspace-core", "toyos-abi", @@ -18,7 +18,7 @@ dependencies = [ [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" dependencies = [ "rustc-std-workspace-core", ] diff --git a/toyos/Cargo.toml b/toyos/Cargo.toml index 118264b2865..007d1f163aa 100644 --- a/toyos/Cargo.toml +++ b/toyos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos" -version = "0.7.0" +version = "0.8.0" edition = "2021" license = "MIT OR Apache-2.0" description = "The ToyOS userland SDK: typed handles, IPC, ports, namespaces, surfaces and net." @@ -11,4 +11,4 @@ rustc-dep-of-std = ["core", "toyos-abi/rustc-dep-of-std"] [dependencies] core = { version = "1.0.0", optional = true, package = "rustc-std-workspace-core" } -toyos-abi = { path = "../toyos-abi", version = "0.6.0", default-features = false } +toyos-abi = { path = "../toyos-abi", version = "0.7.0", default-features = false } diff --git a/toyos/src/device.rs b/toyos/src/device.rs index c8c725dccdf..e10864041de 100644 --- a/toyos/src/device.rs +++ b/toyos/src/device.rs @@ -11,8 +11,8 @@ use crate::{Device, AsHandle}; use crate::ipc::IpcPayload; use toyos_abi::RawHandle; -/// SAFETY: each is `#[repr(C)]` over `u32`/`u16`/`u8` fields and `RawHandle`s -/// (`repr(transparent)` over a `u32`), so none holds a pointer or a field with +/// SAFETY: each is `#[repr(C)]` over unsigned integers, arrays of them and +/// `RawHandle`s (`repr(transparent)` over a `u32`), so none holds a pointer or a field with /// an invalid bit pattern, and the padding-free layout is the `const` assertion /// beside each declaration in `toyos-abi` — the one its `as_bytes` already /// reads on to publish these bytes. @@ -24,6 +24,7 @@ device_info!( toyos_abi::pci::PciFunctionInfo, toyos_abi::virtio_sound::VirtioSoundInfo, toyos_abi::hda::HdaInfo, + toyos_abi::part::PartitionInfo, ); /// Read a claim's description, whose buffer fields are handles this call @@ -181,6 +182,37 @@ impl AsHandle for PciDev { fn as_handle(&self) -> RawHandle { self.0.as_handle() } } +/// One GPT partition, read and written in its own block numbers +/// ([`toyos_abi::part`]); a neighbour's blocks have no spelling here. +pub struct PartitionDev(pub(crate) Device); + +impl PartitionDev { + /// Which partition this is and how long; read once per claim. + pub fn describe(&self) -> Result { + read_info(&self.0) + } + + /// `buf.len()` blocks from the partition's block `first`. + pub fn read(&self, first: u64, buf: &mut [toyos_abi::part::Block]) -> Result<(), SyscallError> { + syscall::partition_read(self.0.as_handle(), first, buf) + } + + /// `buf.len()` blocks to the partition's block `first`; durable only once + /// [`Self::sync`] answers `Ok`. + pub fn write(&self, first: u64, buf: &[toyos_abi::part::Block]) -> Result<(), SyscallError> { + syscall::partition_write(self.0.as_handle(), first, buf) + } + + /// Every write this claim returned from, out of the device's cache. + pub fn sync(&self) -> Result<(), SyscallError> { + syscall::fsync(self.0.as_handle()) + } +} + +impl AsHandle for PartitionDev { + fn as_handle(&self) -> RawHandle { self.0.as_handle() } +} + /// A virtio-sound device the kernel brought up and drives no policy on. /// /// What the claimant gets is the region the descriptors point into, mapped diff --git a/toyos/src/endow.rs b/toyos/src/endow.rs index d813af712ad..ce3971b4ae9 100644 --- a/toyos/src/endow.rs +++ b/toyos/src/endow.rs @@ -72,6 +72,7 @@ from_handle! { crate::Mouse => |h| crate::Mouse(Device(h)), crate::FramebufferDev => |h| crate::FramebufferDev(Device(h)), crate::PciDev => |h| crate::PciDev(Device(h)), + crate::PartitionDev => |h| crate::PartitionDev(Device(h)), crate::HdaDev => |h| crate::HdaDev(Device(h)), crate::VirtioSoundDev => |h| crate::VirtioSoundDev(Device(h)), } @@ -208,6 +209,14 @@ pub fn pci_function(id: toyos_abi::syscall::PciId) -> Option { with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) } +/// The claim for one partition the manifest says this program gets, looked up +/// by the entry's own spelling exactly as [`pci_function`] is. +pub fn partition(name: toyos_abi::part::PartitionName) -> Option { + let mut buf = [0u8; DeviceRequest::MAX_NAME]; + let name = DeviceRequest::Partition(name).write_name(&mut buf); + with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) +} + /// Compose `` on the stack. Nothing in the SDK allocates, and a /// name past the label bound matches nothing rather than being truncated into /// a label that happens to exist. diff --git a/toyos/src/lib.rs b/toyos/src/lib.rs index 1245ef9c196..02b47116354 100644 --- a/toyos/src/lib.rs +++ b/toyos/src/lib.rs @@ -31,7 +31,7 @@ pub mod system; pub use ipc::Connection; pub use device::{ - DmaRegion, FramebufferDev, HdaDev, Keyboard, Mouse, PciDev, VirtioSoundDev, + DmaRegion, FramebufferDev, HdaDev, Keyboard, Mouse, PartitionDev, PciDev, VirtioSoundDev, }; pub use toyos_abi::RawHandle; diff --git a/toyos/src/syscap.rs b/toyos/src/syscap.rs index bd1982fa01e..07149e2f0d4 100644 --- a/toyos/src/syscap.rs +++ b/toyos/src/syscap.rs @@ -40,6 +40,19 @@ impl SysCap { self.mint(DeviceRequest::Pci(id)) } + /// Mint the claim for one GPT partition, by its unique GUID or as the one + /// partition of a type. Apart from [`Self::claim`] for the reason + /// [`Self::claim_pci`] is: this one cannot be asked without naming which. + /// + /// `PermissionDenied` is a partition the kernel has mounted, and + /// `AlreadyExists` one another process holds: a partition has one holder. + pub fn claim_partition( + &self, + name: toyos_abi::part::PartitionName, + ) -> Result { + self.mint(DeviceRequest::Partition(name)) + } + fn mint(&self, request: DeviceRequest) -> Result { let raw = syscall::device_claim(self.0.raw(), request)?; // SAFETY: the kernel installed this handle in this process's table for diff --git a/userland/Cargo.lock b/userland/Cargo.lock index 84a6c61aaaa..d5a9de210c0 100644 --- a/userland/Cargo.lock +++ b/userland/Cargo.lock @@ -477,12 +477,12 @@ name = "compositor" version = "0.1.0" dependencies = [ "sprite", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-desktop", "toyos-font 0.1.0", "toyos-manifest", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -499,10 +499,10 @@ name = "console" version = "0.1.0" dependencies = [ "terminal", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -987,7 +987,7 @@ version = "0.1.0" dependencies = [ "filepicker-api", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -1107,16 +1107,16 @@ name = "filepicker" version = "0.1.0" dependencies = [ "filepicker-api", - "toyos 0.7.0", + "toyos 0.8.0", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] name = "filepicker-api" version = "0.1.0" dependencies = [ - "toyos 0.7.0", + "toyos 0.8.0", ] [[package]] @@ -1125,7 +1125,7 @@ version = "0.1.0" dependencies = [ "sprite", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -1526,8 +1526,8 @@ dependencies = [ name = "init" version = "0.1.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-manifest", ] @@ -1799,8 +1799,8 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" name = "logd" version = "0.1.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-logstream", "toyos-wallclock", ] @@ -1833,8 +1833,8 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" name = "metalprobe" version = "0.1.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-tco", ] @@ -1919,8 +1919,8 @@ name = "netd" version = "0.0.0" dependencies = [ "smoltcp", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-i219", "toyos-tco", ] @@ -2299,7 +2299,7 @@ version = "0.1.0" dependencies = [ "filepicker-api", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -3265,7 +3265,7 @@ dependencies = [ name = "shell" version = "0.1.0" dependencies = [ - "toyos 0.7.0", + "toyos 0.8.0", "toyos-manifest", ] @@ -3417,8 +3417,8 @@ name = "soundd" version = "0.0.0" dependencies = [ "rubato", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-hda", "toyos-mixer", ] @@ -3557,17 +3557,17 @@ dependencies = [ name = "terminal" version = "0.1.0" dependencies = [ - "toyos 0.7.0", + "toyos 0.8.0", "toyos-font 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] name = "test-runner" version = "0.1.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-tco", ] @@ -3697,10 +3697,10 @@ name = "toybox" version = "0.1.0" dependencies = [ "cpal", - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-keymap 0.1.0", - "toyos-window 0.7.0", + "toyos-window 0.8.0", ] [[package]] @@ -3723,9 +3723,9 @@ dependencies = [ [[package]] name = "toyos" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "toyos-abi 0.6.0", + "toyos-abi 0.7.0", ] [[package]] @@ -3742,13 +3742,13 @@ checksum = "ec119a115b619cec7ca5e56858e98db6558658d6cc9b400906187e62f2877f48" [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" [[package]] name = "toyos-desktop" version = "0.1.0" dependencies = [ - "toyos-abi 0.6.0", + "toyos-abi 0.7.0", ] [[package]] @@ -3783,14 +3783,14 @@ checksum = "06db3c1cb438056c9b9a766c8f0dbbfdf3331e723a5bc7f287badc0cfb2f7f78" name = "toyos-logstream" version = "0.1.0" dependencies = [ - "toyos-abi 0.6.0", + "toyos-abi 0.7.0", ] [[package]] name = "toyos-manifest" version = "0.1.0" dependencies = [ - "toyos-abi 0.6.0", + "toyos-abi 0.7.0", ] [[package]] @@ -3819,10 +3819,10 @@ dependencies = [ [[package]] name = "toyos-window" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "toyos 0.7.0", - "toyos-abi 0.6.0", + "toyos 0.8.0", + "toyos-abi 0.7.0", "toyos-font 0.1.0", "toyos-keymap 0.1.0", ] diff --git a/userland/init/src/main.rs b/userland/init/src/main.rs index fc831dccfab..0c1c97f6a66 100644 --- a/userland/init/src/main.rs +++ b/userland/init/src/main.rs @@ -528,7 +528,7 @@ fn refused(name: &str, why: SyscallError) -> String { SyscallError::NotFound => format!("no {name} on this machine"), SyscallError::AlreadyExists => format!("{name} is already claimed"), SyscallError::InvalidArgument => { - format!("{name} names more than one function on this machine") + format!("{name} names more than one device on this machine") } SyscallError::PermissionDenied => { format!("{name} is driven by the kernel and cannot be claimed") @@ -536,7 +536,7 @@ fn refused(name: &str, why: SyscallError) -> String { SyscallError::ResourceExhausted => format!("no claim slot is free for {name}"), SyscallError::NotSupported => { format!("{name} is on this machine and could not be handed over; the kernel's \ - `pcidev:` line says why") + `pcidev:` or `partclaim:` line says why") } other => format!("{name} was refused with {other:?}"), } @@ -616,6 +616,7 @@ fn start<'a>( let minted = match request { DeviceRequest::Class(class) => syscap.claim::(class), DeviceRequest::Pci(id) => syscap.claim_pci::(id), + DeviceRequest::Partition(name) => syscap.claim_partition::(name), }; match minted { Ok(claim) => { diff --git a/userland/libc/Cargo.lock b/userland/libc/Cargo.lock index 9c06505e33a..7b43eff29f6 100644 --- a/userland/libc/Cargo.lock +++ b/userland/libc/Cargo.lock @@ -27,14 +27,14 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "toyos" -version = "0.7.0" +version = "0.8.0" dependencies = [ "toyos-abi", ] [[package]] name = "toyos-abi" -version = "0.6.0" +version = "0.7.0" [[package]] name = "toyos-libc" diff --git a/userland/toyos-window/Cargo.toml b/userland/toyos-window/Cargo.toml index 6347d4af42e..177f5dc87f2 100644 --- a/userland/toyos-window/Cargo.toml +++ b/userland/toyos-window/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos-window" -version = "0.7.0" +version = "0.8.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Client library for ToyOS windows: create one, draw into it, read its events." @@ -8,9 +8,9 @@ repository = "https://github.com/ToyOSOrg/ToyOS" [dependencies] toyos-font = { path = "../toyos-font", version = "0.1.0" } -toyos-abi = { path = "../../toyos-abi", version = "0.6.0" } +toyos-abi = { path = "../../toyos-abi", version = "0.7.0" } toyos-keymap = { path = "../../toyos-keymap", version = "0.1.0" } -toyos = { path = "../../toyos", version = "0.7.0" } +toyos = { path = "../../toyos", version = "0.8.0" } # `window` is taken on crates.io, so the package is `toyos-window`; the lib # target keeps the name every caller spells, `toyos/src`'s citations included. From cb4d26ec81d38a2bcde506a0acfba02701705c8a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 24 Sep 2026 10:36:36 +0200 Subject: [PATCH 2/3] Every kernel match on a device class answers a partition claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class enum gained two rows and four exhaustive matches in the object layer did not know them; the kernel did not compile. A partition claim has no readiness source, answers every read with its description (its bytes move through the transfer calls, never through a read of the claim), stats as Unknown and is always readable. None of these is reachable yet: the claim itself is still refused by name. `PartitionInfo` gains `unique()` and `of_type()`, the two GUIDs as the `PartGuid`s a `part:` and a `part-type:` entry spell — what a holder compares its description against. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- kernel/src/object/ops.rs | 11 +++++++++++ toyos-abi/src/part.rs | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index 119d83b4dd2..379202cd2a3 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -238,6 +238,9 @@ pub fn read_source(object: &KObjectRef) -> Option { device_registry::DeviceType::HdaAudio => Some(Source::Hda), device_registry::DeviceType::VirtioSound => Some(Source::VirtioSound), device_registry::DeviceType::Framebuffer => None, + // A partition answers its description and has nothing to wait for. + device_registry::DeviceType::Partition + | device_registry::DeviceType::PartitionOfType => None, }, // Named unconditionally: the source alone cannot enforce rights. KObjectRef::SysCap(_) => Some(Source::Log), @@ -332,6 +335,10 @@ pub fn read_device( } } device_registry::DeviceType::Framebuffer => Some(claim.describe(table, buf)), + // Every read is the description: a partition's bytes move through + // `SYS_PARTITION_READ`, never through a read of the claim. + device_registry::DeviceType::Partition + | device_registry::DeviceType::PartitionOfType => Some(claim.describe(table, buf)), // The description first and interrupts after, the shape the HDA stub // has: a driver reads what it is driving once, and everything it reads // afterwards is what its device has been doing. @@ -543,6 +550,8 @@ pub fn fstat(object: &KObjectRef) -> Stat { device_registry::DeviceType::PciFunction => FileType::Unknown, device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => FileType::Unknown, + device_registry::DeviceType::Partition + | device_registry::DeviceType::PartitionOfType => FileType::Unknown, }), } } @@ -662,6 +671,8 @@ pub fn has_data(object: &KObjectRef) -> bool { !d.info_read() || d.pci_slot().is_some_and(crate::pcidev::has_irq) } device_registry::DeviceType::Framebuffer => true, + device_registry::DeviceType::Partition + | device_registry::DeviceType::PartitionOfType => true, device_registry::DeviceType::HdaAudio => { !d.info_read() || crate::drivers::hda::has_pending() } diff --git a/toyos-abi/src/part.rs b/toyos-abi/src/part.rs index c16c30ed714..5a59366906b 100644 --- a/toyos-abi/src/part.rs +++ b/toyos-abi/src/part.rs @@ -153,6 +153,16 @@ pub struct PartitionInfo { const _: () = assert!(core::mem::size_of::() == 8 + 16 + 16); impl PartitionInfo { + /// The partition's unique GUID, as a claim by `part:` would name it. + pub const fn unique(&self) -> PartGuid { + PartGuid(self.unique_guid) + } + + /// Its type GUID, as a claim by `part-type:` would name it. + pub const fn of_type(&self) -> PartGuid { + PartGuid(self.type_guid) + } + pub fn as_bytes(&self) -> &[u8] { // SAFETY: `self` is a valid `&Self`, readable for `size_of::()` // bytes, and the const assert above proves the `repr(C)` layout has no From 6d030c13494607ad26e67c6b59f4acf69d07bcae Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 24 Sep 2026 11:48:03 +0200 Subject: [PATCH 3/3] A partition is named by its unique GUID only, and a claim refuses a selector word its class does not read Round-1 review of #487. - `part-type:` / `PartitionOfType` is deleted. The A/B slots share TOYOS-ROOT's type, so a type names neither slot, and whether a type grant resolved depended on which other disks were plugged in. Whoever writes a GPT and the `system.toml` that names its slots fixes each slot's unique GUID and spells `part:`. `PartitionName` goes with it: a request carries a `PartGuid`, `SysCap::claim_partition` and `endow::partition` take one, and `PartitionInfo` loses the type GUID only a type-named claim needed (24 bytes, asserted). - `SYS_DEVICE_CLAIM` refuses a nonzero selector word its class does not read (`InvalidArgument`): both words for a class that names one device, the second for `pci`. It used to drop them. - `MAX_BLOCKS_PER_CALL` loses its rationale: nothing ties it to the page cache's own write-back run. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- kernel/src/arch/syscall/device.rs | 17 ++++++- kernel/src/device.rs | 6 +-- kernel/src/object/ops.rs | 12 ++--- toyos-abi/src/part.rs | 35 ++------------- toyos-abi/src/syscall.rs | 75 +++++++++++-------------------- toyos/src/endow.rs | 4 +- toyos/src/syscap.rs | 10 ++--- 7 files changed, 59 insertions(+), 100 deletions(-) diff --git a/kernel/src/arch/syscall/device.rs b/kernel/src/arch/syscall/device.rs index 8bbb6a3ef5e..1bc975ba880 100644 --- a/kernel/src/arch/syscall/device.rs +++ b/kernel/src/arch/syscall/device.rs @@ -117,12 +117,25 @@ pub(super) fn sys_device_reg(handle: RawHandle, offset: u64, width: u64, value: /// Mints a device claim, gated on a `SysCap` carrying [`Rights::DEVICE`]. /// /// `selector` says which device where the class alone does not — a PCI -/// function's vendor and device id, a partition's GUID — and is ignored by -/// every class that names at most one device on the machine. +/// function's vendor and device id, a partition's GUID — and a word the class +/// does not read is refused, never dropped: whoever set it meant a device this +/// claim would not name. pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2]) -> u64 { let Some(class) = device::DeviceType::from_raw(class) else { return SyscallError::InvalidArgument.to_u64(); }; + let read = match class { + device::DeviceType::Keyboard + | device::DeviceType::Mouse + | device::DeviceType::Framebuffer + | device::DeviceType::HdaAudio + | device::DeviceType::VirtioSound => 0, + device::DeviceType::PciFunction => 1, + device::DeviceType::Partition => 2, + }; + if selector[read..].iter().any(|&word| word != 0) { + return SyscallError::InvalidArgument.to_u64(); + } if let Err(e) = demand_syscap(syscap, Rights::DEVICE) { return e.refuse(); } diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 5fbc4443c3e..e2c68e42f6a 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -114,7 +114,7 @@ pub enum ClaimError { /// /// `selector` says *which* device where the class alone does not — a PCI /// function's vendor and device id in its first word, a partition's GUID in -/// both — and every other class ignores it. +/// both — and the syscall has refused a word the class does not read. pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result, ClaimError> { // Availability is checked before acquiring, so an absent device reports `Absent`, not `Owned`. match class { @@ -152,8 +152,8 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { - log!("partclaim: this kernel does not hand out partition claims yet — {} refused", class.class_name()); + DeviceType::Partition => { + log!("partclaim: this kernel does not hand out partition claims yet"); Err(ClaimError::Unusable) } DeviceType::HdaAudio => { diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index 379202cd2a3..e7935307644 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -239,8 +239,7 @@ pub fn read_source(object: &KObjectRef) -> Option { device_registry::DeviceType::VirtioSound => Some(Source::VirtioSound), device_registry::DeviceType::Framebuffer => None, // A partition answers its description and has nothing to wait for. - device_registry::DeviceType::Partition - | device_registry::DeviceType::PartitionOfType => None, + device_registry::DeviceType::Partition => None, }, // Named unconditionally: the source alone cannot enforce rights. KObjectRef::SysCap(_) => Some(Source::Log), @@ -337,8 +336,7 @@ pub fn read_device( device_registry::DeviceType::Framebuffer => Some(claim.describe(table, buf)), // Every read is the description: a partition's bytes move through // `SYS_PARTITION_READ`, never through a read of the claim. - device_registry::DeviceType::Partition - | device_registry::DeviceType::PartitionOfType => Some(claim.describe(table, buf)), + device_registry::DeviceType::Partition => Some(claim.describe(table, buf)), // The description first and interrupts after, the shape the HDA stub // has: a driver reads what it is driving once, and everything it reads // afterwards is what its device has been doing. @@ -550,8 +548,7 @@ pub fn fstat(object: &KObjectRef) -> Stat { device_registry::DeviceType::PciFunction => FileType::Unknown, device_registry::DeviceType::HdaAudio | device_registry::DeviceType::VirtioSound => FileType::Unknown, - device_registry::DeviceType::Partition - | device_registry::DeviceType::PartitionOfType => FileType::Unknown, + device_registry::DeviceType::Partition => FileType::Unknown, }), } } @@ -671,8 +668,7 @@ pub fn has_data(object: &KObjectRef) -> bool { !d.info_read() || d.pci_slot().is_some_and(crate::pcidev::has_irq) } device_registry::DeviceType::Framebuffer => true, - device_registry::DeviceType::Partition - | device_registry::DeviceType::PartitionOfType => true, + device_registry::DeviceType::Partition => true, device_registry::DeviceType::HdaAudio => { !d.info_read() || crate::drivers::hda::has_pending() } diff --git a/toyos-abi/src/part.rs b/toyos-abi/src/part.rs index 5a59366906b..31e09c699f6 100644 --- a/toyos-abi/src/part.rs +++ b/toyos-abi/src/part.rs @@ -24,9 +24,7 @@ pub const BLOCK_BYTES: usize = 4096; /// spelling. pub type Block = [u8; BLOCK_BYTES]; -/// The most blocks one transfer call carries: the run the kernel's own page -/// cache writes back in, so a claimant holds its disk no longer per call than -/// the kernel does. +/// The most blocks one transfer call carries. pub const MAX_BLOCKS_PER_CALL: usize = 32; /// A GUID as the sixteen bytes a GPT entry stores (UEFI 2.10 §5.3.3), which is @@ -116,28 +114,7 @@ impl PartGuid { } } -/// Which partition a claim names: by its unique partition GUID, or as **the** -/// partition of a type GUID — the one partition of that type on the machine, -/// since a type that more than one entry carries names no partition and the -/// claim is refused rather than resolved to the first. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum PartitionName { - Unique(PartGuid), - OfType(PartGuid), -} - -impl PartitionName { - pub const fn guid(self) -> PartGuid { - match self { - Self::Unique(guid) | Self::OfType(guid) => guid, - } - } -} - /// The partition a claim holds, as its holder reads it once off the claim. -/// -/// Both GUIDs, because a claim named by type has to be able to say which -/// partition it got, and a claim named by partition to say what it holds. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PartitionInfo { @@ -145,24 +122,18 @@ pub struct PartitionInfo { /// `0..blocks` are the whole of what the claim addresses. pub blocks: u64, pub unique_guid: [u8; 16], - pub type_guid: [u8; 16], } /// Every byte belongs to a field: this crosses the boundary through /// `as_bytes`, so a gap would publish whatever the kernel stack held. -const _: () = assert!(core::mem::size_of::() == 8 + 16 + 16); +const _: () = assert!(core::mem::size_of::() == 8 + 16); impl PartitionInfo { - /// The partition's unique GUID, as a claim by `part:` would name it. + /// The partition's unique GUID, as its `part:` entry names it. pub const fn unique(&self) -> PartGuid { PartGuid(self.unique_guid) } - /// Its type GUID, as a claim by `part-type:` would name it. - pub const fn of_type(&self) -> PartGuid { - PartGuid(self.type_guid) - } - pub fn as_bytes(&self) -> &[u8] { // SAFETY: `self` is a valid `&Self`, readable for `size_of::()` // bytes, and the const assert above proves the `repr(C)` layout has no diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 9950ced794c..a3d9f5373c4 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1202,10 +1202,6 @@ device_classes! { /// Like `pci`, a class whose name is not the whole of the entry — /// `part:` — and [`DeviceRequest`] is the one parser. Partition = 8 => "part", - /// The same claim, naming the partition as the one partition on the machine - /// of a type GUID: `part-type:`. The claim is the same object; the - /// class says how the selector is to be read. - PartitionOfType = 9 => "part-type", } /// A PCI function named by what identifies the *card*, not the slot firmware @@ -1274,7 +1270,7 @@ fn hex16(text: &str) -> Option { pub enum DeviceRequest { Class(DeviceType), Pci(PciId), - Partition(crate::part::PartitionName), + Partition(crate::part::PartGuid), } impl DeviceRequest { @@ -1284,40 +1280,31 @@ impl DeviceRequest { /// The spelling that carries a unique partition GUID. pub const PART_PREFIX: &'static str = "part:"; - /// The spelling that carries a partition type GUID. - pub const PART_TYPE_PREFIX: &'static str = "part-type:"; - /// The longest a `devices` entry, and so a `dev:` label's tail, can be: - /// `part-type:` and a GUID, rounded up to a multiple of eight. + /// `part:` and a GUID, rounded up to a multiple of eight. pub const MAX_NAME: usize = 48; pub fn parse(name: &str) -> Option { - use crate::part::{PartGuid, PartitionName}; if let Some(id) = name.strip_prefix(Self::PCI_PREFIX) { return PciId::parse(id).map(Self::Pci); } if let Some(guid) = name.strip_prefix(Self::PART_PREFIX) { - return PartGuid::parse(guid).map(|g| Self::Partition(PartitionName::Unique(g))); - } - if let Some(guid) = name.strip_prefix(Self::PART_TYPE_PREFIX) { - return PartGuid::parse(guid).map(|g| Self::Partition(PartitionName::OfType(g))); + return crate::part::PartGuid::parse(guid).map(Self::Partition); } let class = DeviceType::from_class_name(name)?; match class { - // A bare `pci`, `part` or `part-type` names no device, and would - // otherwise leave the selector at zero. - DeviceType::PciFunction | DeviceType::Partition | DeviceType::PartitionOfType => None, + // A bare `pci` or `part` names no device, and would otherwise + // leave the selector at zero. + DeviceType::PciFunction | DeviceType::Partition => None, _ => Some(Self::Class(class)), } } pub const fn class(self) -> DeviceType { - use crate::part::PartitionName; match self { Self::Class(class) => class, Self::Pci(_) => DeviceType::PciFunction, - Self::Partition(PartitionName::Unique(_)) => DeviceType::Partition, - Self::Partition(PartitionName::OfType(_)) => DeviceType::PartitionOfType, + Self::Partition(_) => DeviceType::Partition, } } @@ -1327,7 +1314,7 @@ impl DeviceRequest { match self { Self::Class(_) => [0, 0], Self::Pci(id) => [id.wire(), 0], - Self::Partition(name) => name.guid().wire(), + Self::Partition(guid) => guid.wire(), } } @@ -1335,22 +1322,18 @@ impl DeviceRequest { /// [`Self::MAX_NAME`] bounds. The name is also the tail of the `dev:` label /// the claim arrives under, and it is composed here so the two cannot drift. pub fn write_name(self, buf: &mut [u8; Self::MAX_NAME]) -> &str { - use crate::part::{PartitionName, GUID_TEXT_LEN}; + use crate::part::GUID_TEXT_LEN; let id = match self { Self::Class(class) => { let name = class.class_name(); buf[..name.len()].copy_from_slice(name.as_bytes()); return core::str::from_utf8(&buf[..name.len()]).expect("a class name is ASCII"); } - Self::Partition(name) => { - let prefix = match name { - PartitionName::Unique(_) => Self::PART_PREFIX, - PartitionName::OfType(_) => Self::PART_TYPE_PREFIX, - }; - let at = prefix.len(); - buf[..at].copy_from_slice(prefix.as_bytes()); + Self::Partition(guid) => { + let at = Self::PART_PREFIX.len(); + buf[..at].copy_from_slice(Self::PART_PREFIX.as_bytes()); let mut text = [0u8; GUID_TEXT_LEN]; - name.guid().write_text(&mut text); + guid.write_text(&mut text); buf[at..at + GUID_TEXT_LEN].copy_from_slice(&text); return core::str::from_utf8(&buf[..at + GUID_TEXT_LEN]) .expect("a prefix and a GUID's text are ASCII"); @@ -1383,7 +1366,7 @@ const HEX: &[u8; 16] = b"0123456789abcdef"; /// `PermissionDenied` one the kernel drives itself — for a partition, one it /// has mounted. A request matching more than one function or partition answers /// `InvalidArgument`, because an ambiguous name is refused rather than resolved -/// to the first match. +/// to the first match; so does a nonzero selector word the class does not read. /// /// The claim comes back **without** [`Rights::DUP`], so it can only be moved, /// which is what makes endowing one to a child a provable hand-off. @@ -2257,9 +2240,10 @@ mod tests { "nic", // the retired class the NIC used to be claimed as "gpu", // a class name this table has never had "part", // a bare partition class names no partition - "part-type", "part:", - "part-type:c12a7328-f81f-11d2-ba4b-00a0c93ec93b", // one spelling: uppercase + "part:c12a7328-f81f-11d2-ba4b-00a0c93ec93b", // one spelling: uppercase + // A partition is named by its unique GUID and nothing else. + "part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B", "", ] { assert_eq!(DeviceRequest::parse(bad), None, "{bad:?} parsed"); @@ -2277,9 +2261,8 @@ mod tests { "pci:0000:0000", "hda-audio", "mouse", - "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", // The longest entry there is, which is what `MAX_NAME` is for. - "part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", ] { let request = DeviceRequest::parse(name).expect("a name this table has"); let mut buf = [0u8; DeviceRequest::MAX_NAME]; @@ -2287,20 +2270,16 @@ mod tests { } } - /// The two partition spellings are two classes over one GUID, so a type - /// row can never be read as the partition whose unique GUID it happens to - /// spell, nor the reverse. + /// A `part:` entry is the partition class carrying its GUID's sixteen bytes + /// in both selector words. #[test] - fn a_partition_entry_says_which_of_its_guids_it_names() { - use crate::part::{PartGuid, PartitionName}; - let esp = PartGuid::parse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B").expect("the ESP type"); - let by_type = DeviceRequest::parse("part-type:C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); - let by_unique = DeviceRequest::parse("part:C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); - assert_eq!(by_type, Some(DeviceRequest::Partition(PartitionName::OfType(esp)))); - assert_eq!(by_unique, Some(DeviceRequest::Partition(PartitionName::Unique(esp)))); - assert_eq!(by_type.map(DeviceRequest::class), Some(DeviceType::PartitionOfType)); - assert_eq!(by_unique.map(DeviceRequest::class), Some(DeviceType::Partition)); - assert_eq!(by_type.map(DeviceRequest::selector), Some(esp.wire())); + fn a_partition_entry_carries_its_guid_in_both_selector_words() { + use crate::part::PartGuid; + let guid = PartGuid::parse("0FC63DAF-8483-4772-8E79-3D69D8477DE4").expect("a GUID"); + let request = DeviceRequest::parse("part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"); + assert_eq!(request, Some(DeviceRequest::Partition(guid))); + assert_eq!(request.map(DeviceRequest::class), Some(DeviceType::Partition)); + assert_eq!(request.map(DeviceRequest::selector), Some(guid.wire())); } /// The selector is one word on the wire and the kernel decodes it back; diff --git a/toyos/src/endow.rs b/toyos/src/endow.rs index ce3971b4ae9..ffcb06c5856 100644 --- a/toyos/src/endow.rs +++ b/toyos/src/endow.rs @@ -211,9 +211,9 @@ pub fn pci_function(id: toyos_abi::syscall::PciId) -> Option { /// The claim for one partition the manifest says this program gets, looked up /// by the entry's own spelling exactly as [`pci_function`] is. -pub fn partition(name: toyos_abi::part::PartitionName) -> Option { +pub fn partition(guid: toyos_abi::part::PartGuid) -> Option { let mut buf = [0u8; DeviceRequest::MAX_NAME]; - let name = DeviceRequest::Partition(name).write_name(&mut buf); + let name = DeviceRequest::Partition(guid).write_name(&mut buf); with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) } diff --git a/toyos/src/syscap.rs b/toyos/src/syscap.rs index 07149e2f0d4..a5de1b724af 100644 --- a/toyos/src/syscap.rs +++ b/toyos/src/syscap.rs @@ -40,17 +40,17 @@ impl SysCap { self.mint(DeviceRequest::Pci(id)) } - /// Mint the claim for one GPT partition, by its unique GUID or as the one - /// partition of a type. Apart from [`Self::claim`] for the reason - /// [`Self::claim_pci`] is: this one cannot be asked without naming which. + /// Mint the claim for one GPT partition, by its unique GUID. Apart from + /// [`Self::claim`] for the reason [`Self::claim_pci`] is: this one cannot be + /// asked without naming which. /// /// `PermissionDenied` is a partition the kernel has mounted, and /// `AlreadyExists` one another process holds: a partition has one holder. pub fn claim_partition( &self, - name: toyos_abi::part::PartitionName, + guid: toyos_abi::part::PartGuid, ) -> Result { - self.mint(DeviceRequest::Partition(name)) + self.mint(DeviceRequest::Partition(guid)) } fn mint(&self, request: DeviceRequest) -> Result {