diff --git a/issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md b/issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md deleted file mode 100644 index b04c0e22f84..00000000000 --- a/issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A file server maps a lent window at the size it expects, not the size it is - -fsd adopts the region a client lends at its hello with -`SharedMemory::adopt(lent, WINDOW_BYTES)` (`userland/fsd/src/main.rs`), and -`adopt` checks nothing about the region: its doc says a peer that promises a -size and sends a smaller region is the reader's to bound. A region -`SYS_SHM_CREATE` made is whole 2 MiB pages, so it is never short of the window. -A region a device claim answered is not made that way — a BAR aperture or a -scanout is whatever the device is — and a program holding one can lend it. The -server then copies a read's bytes into device memory, or past the end of a -short mapping into whatever follows, and a fault there ends DATA's server, -whose restarts are budgeted for the whole machine. - -**Exit**: the kernel answers a region's size and whether it is memory it -allocated to the holder of a handle to it, fsd refuses a hello whose window is -not `WINDOW_BYTES` of that memory by name, and a guest test lends a region a -device claim answered and is refused while the server answers the next client. diff --git a/issues/a-received-handle-has-no-knowable-type.md b/issues/a-received-handle-has-no-knowable-type.md index 51ea977c709..3decfeb0487 100644 --- a/issues/a-received-handle-has-no-knowable-type.md +++ b/issues/a-received-handle-has-no-knowable-type.md @@ -4,46 +4,47 @@ kind: defect opened: 2026-08-10 --- -# A process cannot ask what it received, and using it wrongly is fatal +# A process cannot ask what most handles it received are, and using one wrongly is fatal `SYS_HANDLE_RECV` installs a peer's handles in the receiver's table and answers -only *how many*. There is no syscall that says what any of them names, and -`KObjectRef`'s kind is not reported anywhere. +only *how many*. `SYS_FSTAT` answers what any handle is, needing no right, and +settles two kinds: only a region of memory the kernel allocated answers +`SharedMemory`, which is how `SharedMemory::lent` refuses every other handle +before it maps one (`sdk/toyos/src/shm.rs`), and only a `Connection` answers +`Socket`. It settles no other. A `Process`, a `Namespace`, a `Connector`, an +`Inbox` and a `SysCap` all answer `Unknown`, and an `Acceptor` answers `Pipe`, +as a pipe end does (`fstat` in `kernel/src/object/ops.rs`). Every typed use of a handle resolves through `HandleTable::get::`, and -`HandleError::WrongType` is one of the three kinds `HandleError::refuse` treats -as a bug in the caller: it ends the process, exit 139 -(`kernel/src/object/handle.rs`). The doc argues that *"asking a pipe to accept a -connection is not something a correct program can do"* — which is true for a -handle the process made or was endowed, and **false for one a peer sent**. A +`HandleError::WrongType` ends the caller, exit 139, everywhere but +`HandleTable::get_sent` (`kernel/src/object/handle.rs`). That is right for a +handle the process made or was endowed, and wrong for one a peer sent: a correct program cannot know. -So: **any process that receives a handle over a connection and then uses it -typed can be ended by whoever sent it.** The sender needs nothing but +So: **any process that receives one of those kinds over a connection and then +uses it typed can be ended by whoever sent it.** The sender needs nothing but `Rights::TRANSFER`, which every handle it made carries. ## Where it is reachable today -- `/system/bin/init`'s launcher takes `extra` connectors from a client and hands them - to `SYS_NAMESPACE_BUILD`. That one is closed by making that call's `add` arm - answer `InvalidArgument` rather than fault, and `launcher_refusals` gates it — - but that is one call site, not the class. -- An audio client receives two handles from soundd and calls `SYS_SHM_MAP` on - the first (`sdk/toyos/src/audio.rs`). A hostile *server* ends every client. -- A window client receives its buffer the same way (`sdk/window/src/lib.rs`). -- blockd maps the region a client sends with its open (`Region::adopt` in - `userland/blockd/src/region.rs`), so a *client* holding its connector ends it. -- netstack adopts as pipes the two handles a client sends with a piped socket - (`data_pipes`) and the one with a piped bind (`Sockets::listen`), both in - `system/netstack/src/serve.rs`: a handle that is no pipe end answers a - refusal the node ends that client's stream or listener for - (`system/netstack/src/pipes.rs`), and a datagram socket's are netstack's - own to read and write. +- `toyos::launch` answers a started launch with the `Process` handle the + supervisor sent (`sdk/toyos/src/launch.rs`), and its caller uses it typed. + +A generic use is not reached: a read or a write a handle cannot do answers +`PermissionDenied`, and a watch `NotSupported`, never `WrongType`, which is how +netstack's piped ends are refused (`system/netstack/src/pipes.rs`). The +supervisor hands what a launch's client sent to `SYS_SPAWN` and +`SYS_NAMESPACE_BUILD`, which answer a handle of the wrong kind +`InvalidArgument`. Nothing in the tree is hostile today, so nothing fails. The property the architecture claims — that a process cannot be harmed by what it was not given — does not hold across a transfer. +**Exit**: a process that receives a handle of one of those kinds refuses one of +another kind by name before it uses it typed, and a guest test that sends one +in its place sees it refused while the receiver lives. + ## Three ways out, and none is free 1. **Report the kind.** `SYS_HANDLE_RECV` writes `(RawHandle, kind)` pairs diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index d377a1408a8..bb532bc8f4f 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -213,12 +213,14 @@ macro_rules! kobject { pub(super) static $variant: AtomicU64 = AtomicU64::new(0); )+ - /// `(type name, live count)` per kind, in declaration order — a - /// summed total would hide one kind's leak in another's churn. + /// Live objects of one kind — a summed total would hide one + /// kind's leak in another's churn. #[cfg(feature = "test-actuators")] - pub fn live() -> impl Iterator { + pub fn live(kind: toyos_abi::handle::ObjectKind) -> u64 { use core::sync::atomic::Ordering; - [$((stringify!($variant), $variant.load(Ordering::Relaxed)),)+].into_iter() + match kind { + $(toyos_abi::handle::ObjectKind::$variant => $variant.load(Ordering::Relaxed),)+ + } } } }; @@ -240,7 +242,6 @@ kobject! { deferred Connection => service::ConnectionEnd, deferred Device => device::DeviceClaim, deferred Acceptor => port::Acceptor, - // Kind name is the ABI's; `CENSUS_KIND` asserts the two lists agree. deferred Inbox => inbox::InboxObject, // The flush that makes releasing it safe waits for every CPU, so it runs from the queue, never inline. deferred SharedMem => shm::SharedMemObject, diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index 959490458f7..6981fe0e48a 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -670,7 +670,8 @@ pub fn fstat(object: &KObjectRef) -> Stat { KObjectRef::Console(_) => plain(FileType::Serial), KObjectRef::Acceptor(_) => plain(FileType::Pipe), KObjectRef::SharedMem(m) => Stat { - file_type: FileType::SharedMemory as u64, + // The verdict `pcidev::dma_map` lends a device by: a receiver judges a peer's region on it before mapping. + file_type: if m.ram().is_some() { FileType::SharedMemory } else { FileType::DeviceMemory } as u64, size: m.size(), mtime: 0, volume: 0, diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index 9d84684c4ca..0f58e678174 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -20,6 +20,9 @@ use crate::{DirectMap, UserAddr}; use super::{Held, KObjectVariant, ObjectCore, ZeroHandles}; +// The ABI's word for what `create` rounds to, which a receiver checks a lent region's length against. +const _: () = assert!(toyos_abi::syscall::SHM_PAGE as u64 == PAGE_2M); + /// Physical pages a region keeps alive; behind an `Arc` since one page set /// can back several objects. // Unread by design: the vector's job is only to stay alive. diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index e61b604b560..dc119ec77ed 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -608,20 +608,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> // The two leaks this object model accepts — a stranded Arc, the cross-pair // connection cycle — are visible in no other way than this census. // Per kind, not total: a total would hide one kind's leak behind another's churn. - // Names are checked against OBJECT_KINDS rather than order assumed: this is - // the one place the two declarations meet. - DA::CENSUS_KIND => { - assert!( - crate::object::census::live() - .map(|(kind, _)| kind) - .eq(toyos_abi::syscall::OBJECT_KINDS.iter().copied()), - "kobject! and toyos_abi::syscall::OBJECT_KINDS declare different objects", - ); - match crate::object::census::live().nth(a2 as usize) { - Some((_, live)) => live, - None => SyscallError::InvalidArgument.to_u64(), - } - } + DA::CENSUS_KIND => match u32::try_from(a2).ok().and_then(toyos_abi::handle::ObjectKind::from_word) { + Some(kind) => crate::object::census::live(kind), + None => SyscallError::InvalidArgument.to_u64(), + }, // Armed, not #[cfg]'d, so it doesn't ship in every kernel this suite boots: // the real bound is unreachable (no guest makes 65,536 threads). DA::LOWER_SYSINFO_BOUND => { diff --git a/sdk/abi/src/handle.rs b/sdk/abi/src/handle.rs index ec93f51273a..019f5ecd16e 100644 --- a/sdk/abi/src/handle.rs +++ b/sdk/abi/src/handle.rs @@ -57,6 +57,58 @@ impl RawHandle { /// any table issues. pub const HANDLE_INVALID: RawHandle = RawHandle(u32::MAX); +/// Every kind of kernel object a handle can name, as the word +/// [`CENSUS_KIND`](crate::syscall::debug_action::CENSUS_KIND) takes. +#[repr(u32)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum ObjectKind { + PipeRead = 0, + PipeWrite = 1, + Connection = 2, + Device = 3, + Acceptor = 4, + Inbox = 5, + SharedMem = 6, + Connector = 7, + Namespace = 8, + File = 9, + Console = 10, + SysCap = 11, + Process = 12, +} + +impl ObjectKind { + pub const ALL: [ObjectKind; 13] = [ + Self::PipeRead, + Self::PipeWrite, + Self::Connection, + Self::Device, + Self::Acceptor, + Self::Inbox, + Self::SharedMem, + Self::Connector, + Self::Namespace, + Self::File, + Self::Console, + Self::SysCap, + Self::Process, + ]; + + pub const fn from_word(word: u32) -> Option { + if (word as usize) < Self::ALL.len() { Some(Self::ALL[word as usize]) } else { None } + } +} + +// `from_word` indexes `ALL` by the word: a row out of order would count a +// kind's neighbour. +const _: () = { + let mut i = 0; + while i < ObjectKind::ALL.len() { + assert!(ObjectKind::ALL[i] as usize == i, "ObjectKind::ALL is out of word order"); + i += 1; + } +}; + /// What the holder of a handle may do with the object behind it. /// /// Rights only ever shrink: a duplicate may ask for a subset and nothing diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 906e12a9d36..bc77b9c0f27 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -179,13 +179,16 @@ pub const SYS_NAMESPACE_OPEN: u64 = 102; pub const SYS_HANDLE_SEND: u64 = 103; /// Take the oldest batch of handles the peer sent. See [`handle_recv`]. pub const SYS_HANDLE_RECV: u64 = 104; -/// Make a shared-memory region, sized up to whole 2 MiB pages. +/// Make a shared-memory region, sized up to whole [`SHM_PAGE`]s. /// See [`shm_create`]. pub const SYS_SHM_CREATE: u64 = 105; /// Map a region into the caller. Idempotent: a second call answers the first /// call's address. See [`shm_map`]. pub const SYS_SHM_MAP: u64 = 106; +/// What [`SYS_SHM_CREATE`] rounds a length up to a whole number of. +pub const SHM_PAGE: usize = 2 << 20; + /// Wait for the process a handle names and take its exit code, gated by /// [`Rights::WAIT`]. See [`process_wait`]. /// @@ -554,13 +557,17 @@ pub enum FileType { // 9 was `Nic`, the type a claim on the kernel's own NIC driver reported. // A claim on a PCI function reports `Unknown`: what it is, is the driver's // to know, and this kernel does not know it. - /// A shared-memory region; `size` is its length. A program's stdout and - /// stderr are one when they are its log ring (`toyos::log`). + /// A shared-memory region of memory the kernel allocated to it and maps + /// write-back, as [`shm_create`] makes; `size` is its length. A program's + /// stdout and stderr are one when they are its log ring (`toyos::log`). SharedMemory = 10, /// A directory, which only [`stat`] answers. Directory = 11, /// A symbolic link, which only [`stat`] answers; `size` is its target's length. Symlink = 12, + /// Any other shared-memory region, such as a device's registers; `size` + /// is its length. A peer's buffer is never one. + DeviceMemory = 13, } impl FileType { @@ -578,6 +585,7 @@ impl FileType { 10 => Some(Self::SharedMemory), 11 => Some(Self::Directory), 12 => Some(Self::Symlink), + 13 => Some(Self::DeviceMemory), _ => None, } } @@ -815,7 +823,7 @@ pub mod debug_action { pub const TLB_ACK_DELAY_ARM: u64 = 12; pub const TLB_ACK_DELAY_DISARM: u64 = 13; /// How many kernel objects of one kind are alive right now. The argument is - /// an [`OBJECT_KINDS`](super::OBJECT_KINDS) index. + /// an [`ObjectKind`](crate::handle::ObjectKind) word. /// /// **Per kind and not a total**: an object of one kind that is never /// released is invisible behind ordinary churn in another. @@ -886,31 +894,6 @@ pub mod debug_action { pub const RING0_FIRE_OTHER_SPAN: u64 = 2; } -/// Every kind of kernel object, in the order the kernel's own `kobject!` -/// declares them — so an index into this is the index -/// [`debug_action::CENSUS_KIND`] takes. -/// -/// In the ABI rather than in the kernel alone, because a census nobody can read -/// per kind is a total. The kernel checks this list against its own declaration -/// order when the action is called, so a row added to `kobject!` without a row -/// here is a named refusal rather than an index that quietly names its -/// neighbour. -pub const OBJECT_KINDS: &[&str] = &[ - "PipeRead", - "PipeWrite", - "Connection", - "Device", - "Acceptor", - "Inbox", - "SharedMem", - "Connector", - "Namespace", - "File", - "Console", - "SysCap", - "Process", -]; - /// Create a pipe. Returns the read and write ends. /// /// **Fallible, because `sys_pipe` is.** It answers `ResourceExhausted` on three diff --git a/sdk/toyos/src/audio.rs b/sdk/toyos/src/audio.rs index a33be4eaa11..614652b0283 100644 --- a/sdk/toyos/src/audio.rs +++ b/sdk/toyos/src/audio.rs @@ -182,6 +182,8 @@ pub enum AudioError { /// pipe with it. Handles cross before the frame that names them, so a /// short batch is a protocol violation and never something to wait for. MissingHandles, + /// The slot ring soundserver sent is refused ([`SharedMemory::lent`]). + Lent(crate::shm::Lent), Ipc(IpcError), Protocol(u32), } @@ -215,8 +217,7 @@ impl AudioStream { let slot_count = resp.slot_count as u32; let shm_size = AudioSlotHeader::SIZE + slot_count as usize * resp.client_period_bytes as usize; - let shm = SharedMemory::adopt(batch[STREAM_OPENED_SHM], shm_size) - .map_err(|e| AudioError::Ipc(IpcError::Syscall(e)))?; + let shm = SharedMemory::lent(batch[STREAM_OPENED_SHM], shm_size).map_err(AudioError::Lent)?; let slot_writer = AudioSlotWriter::new(shm, resp.client_period_bytes, slot_count); Ok(Self { diff --git a/sdk/toyos/src/census.rs b/sdk/toyos/src/census.rs index ccf42c877a3..8ee21c2576d 100644 --- a/sdk/toyos/src/census.rs +++ b/sdk/toyos/src/census.rs @@ -8,30 +8,25 @@ use core::fmt; use toyos_abi::syscall::debug_action::CENSUS_KIND; -use toyos_abi::syscall::{self, OBJECT_KINDS}; +use toyos_abi::handle::ObjectKind; +use toyos_abi::syscall; /// How many objects of every kind are alive right now. #[derive(Clone, Copy, PartialEq, Eq)] -pub struct Census([u64; OBJECT_KINDS.len()]); +pub struct Census([u64; ObjectKind::ALL.len()]); impl Census { pub fn now() -> Self { - let mut counts = [0u64; OBJECT_KINDS.len()]; - for (i, slot) in counts.iter_mut().enumerate() { - *slot = syscall::debug_with(CENSUS_KIND, i as u64); + let mut counts = [0u64; ObjectKind::ALL.len()]; + for (slot, kind) in counts.iter_mut().zip(ObjectKind::ALL) { + *slot = syscall::debug_with(CENSUS_KIND, kind as u64); } Self(counts) } - /// Live objects of one kind. The name is an [`OBJECT_KINDS`] entry and a - /// name that is not one is a caller with a typo, so it panics rather than - /// answering zero — which would read as "nothing of that kind leaked". - pub fn kind(&self, name: &str) -> u64 { - let i = OBJECT_KINDS - .iter() - .position(|k| *k == name) - .unwrap_or_else(|| panic!("no kernel object kind is called {name:?}")); - self.0[i] + /// Live objects of one kind. + pub fn kind(&self, kind: ObjectKind) -> u64 { + self.0[kind as usize] } pub fn total(&self) -> u64 { @@ -42,24 +37,24 @@ impl Census { pub fn grown_since( &self, before: &Census, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let earlier = before.0; - OBJECT_KINDS + ObjectKind::ALL .iter() .enumerate() .filter(move |(i, _)| self.0[*i] > earlier[*i]) - .map(move |(i, name)| (*name, earlier[i], self.0[i])) + .map(move |(i, kind)| (*kind, earlier[i], self.0[i])) } } impl fmt::Display for Census { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut first = true; - for (name, live) in OBJECT_KINDS.iter().zip(self.0.iter()).filter(|(_, n)| **n > 0) { + for (kind, live) in ObjectKind::ALL.iter().zip(self.0.iter()).filter(|(_, n)| **n > 0) { if !first { write!(f, ", ")?; } - write!(f, "{name} {live}")?; + write!(f, "{kind:?} {live}")?; first = false; } if first { diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index f59b01bc3ba..01832e09886 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -249,7 +249,16 @@ impl Connection { /// wait for. Every protocol here sends a fixed number per message type, so /// this is the shape every caller wants. pub fn recv_handles_exact(&self) -> Option<[RawHandle; N]> { - recv_handles_exact(self.0.raw()) + const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; + let mut batch = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; + let n = self.recv_handles(&mut batch).ok()?; + if n != N { + for h in &batch[..n] { + syscall::close(*h); + } + return None; + } + Some(core::array::from_fn(|i| batch[i])) } pub fn recv_bytes(&self, header: &IpcHeader, buf: &mut [u8]) -> Result { @@ -325,26 +334,6 @@ fn move_batch( Ok(()) } -/// The `N` handles the frame just read off `handle` says travel with it. -/// -/// See [`Connection::recv_handles_exact`]. This is the spelling for a server -/// that buffers frames and dispatches them by handle rather than holding the -/// `Connection` — the compositor reads every client this way. -pub fn recv_handles_exact(handle: RawHandle) -> Option<[RawHandle; N]> { - const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; - let mut batch = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; - let n = syscall::handle_recv(handle, &mut batch).ok()?; - if n != N { - for h in &batch[..n] { - syscall::close(*h); - } - return None; - } - let mut out = [toyos_abi::HANDLE_INVALID; N]; - out.copy_from_slice(&batch[..N]); - Some(out) -} - /// [`signal`] without blocking. A bare header always fits in one write. pub fn try_signal(handle: RawHandle, msg_type: u32) -> Result<(), TrySendError> { write_whole(handle, &IpcHeader { msg_type, len: 0 }.to_wire()) diff --git a/sdk/toyos/src/shm.rs b/sdk/toyos/src/shm.rs index ea52f91c038..c408db6fb16 100644 --- a/sdk/toyos/src/shm.rs +++ b/sdk/toyos/src/shm.rs @@ -8,10 +8,19 @@ //! //! The mapping goes away with the last handle, so dropping this is all the //! cleanup there is. +//! +//! **A region a peer lends is taken only through [`SharedMemory::lent`]**, +//! which believes the kernel's answer about it and not the peer's: a handle +//! the kernel's [`fstat`](syscall::fstat) says is not memory it allocated to a +//! region — another kind of object, or a device's registers — or is not the +//! length the protocol says, is refused before it is mapped, since mapping a +//! handle that names no region ends this process. [`SharedMemory::adopt`] is +//! for a region the kernel itself handed this process. +use core::fmt; use core::sync::atomic::AtomicU8; -use toyos_abi::syscall::{self, SyscallError}; +use toyos_abi::syscall::{self, FileType, Stat, SyscallError, SHM_PAGE}; use crate::{AsHandle, OwnedHandle, RawHandle}; @@ -34,17 +43,23 @@ impl SharedMemory { Self::adopt(syscall::shm_create(size)?, size) } - /// Map a region a peer sent, or one a device description named. + /// Map a region the kernel itself handed this process — a device's + /// window, a DMA grant — and view its first `size` bytes. /// - /// `size` is the caller's own belief about the region and is not checked - /// against it — a peer that says 4 MiB and sends 2 is a peer, and every - /// reader of this memory is already writing bounds against the size it - /// negotiated. + /// Never for a region a peer sent, which is [`Self::lent`]'s. pub fn adopt(handle: RawHandle, size: usize) -> Result { let handle = OwnedHandle(handle); - let ptr = unsafe { syscall::shm_map(handle.raw()) }?; - assert!(!ptr.is_null(), "shm_map answered null"); - Ok(Self { handle, ptr, size }) + Ok(Self { ptr: map(&handle)?, handle, size }) + } + + /// Map a region a peer lent, whose protocol says it is `bytes` long, and + /// view those bytes; refused, and never mapped, unless the kernel answers + /// it is memory it allocated, `bytes` rounded up to whole [`SHM_PAGE`]s, + /// as [`Self::create`] makes it. The handle is consumed either way. + pub fn lent(handle: RawHandle, bytes: usize) -> Result { + let handle = OwnedHandle(handle); + judge(&syscall::fstat(handle.raw()).map_err(Lent::Kernel)?, bytes)?; + Ok(Self { ptr: map(&handle).map_err(Lent::Kernel)?, handle, size: bytes }) } /// A second handle to the same region, for sending to a peer. @@ -83,8 +98,94 @@ impl SharedMemory { } } +fn map(handle: &OwnedHandle) -> Result<*mut u8, SyscallError> { + // SAFETY: the mapping lives as long as the handle, which the caller keeps beside it. + let ptr = unsafe { syscall::shm_map(handle.raw()) }?; + assert!(!ptr.is_null(), "shm_map answered null"); + Ok(ptr) +} + +/// Why a lent region was refused. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Lent { + /// The kernel would not describe or map it. + Kernel(SyscallError), + /// It is not memory the kernel allocated to a region: another kind of + /// object, or a device's registers, which a reader would read and write + /// as a device. + NotRam, + /// It is not the length the protocol says: `is` bytes, where `bytes` + /// rounded up to whole pages was owed. + Length { is: usize, bytes: usize }, +} + +impl fmt::Display for Lent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kernel(e) => write!(f, "the kernel would not describe or map it ({e:?})"), + Self::NotRam => write!(f, "it is not memory the kernel allocated"), + Self::Length { is, bytes } => write!(f, "it is {is} bytes, not {bytes} in whole pages"), + } + } +} + +/// The kernel's answer about a lent region, held to the protocol's length. +fn judge(stat: &Stat, bytes: usize) -> Result<(), Lent> { + if stat.file_type != FileType::SharedMemory { + return Err(Lent::NotRam); + } + if Some(stat.size as usize) != bytes.checked_next_multiple_of(SHM_PAGE) { + return Err(Lent::Length { is: stat.size as usize, bytes }); + } + Ok(()) +} + impl AsHandle for SharedMemory { fn as_handle(&self) -> RawHandle { self.handle.raw() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn answer(len: usize, file_type: FileType) -> Stat { + Stat { file_type, size: len as u64, ..Stat::default() } + } + + #[test] + fn a_region_the_kernel_made_for_the_length_is_taken() { + assert_eq!(judge(&answer(SHM_PAGE, FileType::SharedMemory), SHM_PAGE), Ok(())); + assert_eq!(judge(&answer(2 * SHM_PAGE, FileType::SharedMemory), SHM_PAGE + 1), Ok(())); + } + + #[test] + fn a_region_that_is_not_ram_is_refused_whatever_its_length() { + assert_eq!(judge(&answer(SHM_PAGE, FileType::DeviceMemory), SHM_PAGE), Err(Lent::NotRam)); + } + + #[test] + fn a_region_longer_than_the_protocol_says_is_refused() { + assert_eq!( + judge(&answer(2 * SHM_PAGE, FileType::SharedMemory), SHM_PAGE), + Err(Lent::Length { is: 2 * SHM_PAGE, bytes: SHM_PAGE }) + ); + } + + #[test] + fn a_region_shorter_than_the_protocol_says_is_refused() { + assert_eq!( + judge(&answer(SHM_PAGE, FileType::SharedMemory), SHM_PAGE + 1), + Err(Lent::Length { is: SHM_PAGE, bytes: SHM_PAGE + 1 }) + ); + } + + #[test] + fn a_length_whose_rounding_overflows_is_refused() { + assert_eq!( + judge(&answer(SHM_PAGE, FileType::SharedMemory), usize::MAX), + Err(Lent::Length { is: SHM_PAGE, bytes: usize::MAX }) + ); + } +} diff --git a/sdk/window/src/lib.rs b/sdk/window/src/lib.rs index 2365c69d3ef..8447d8bc717 100644 --- a/sdk/window/src/lib.rs +++ b/sdk/window/src/lib.rs @@ -14,7 +14,7 @@ use toyos::poller::{Poller, READABLE}; use toyos::endow::{self, EndowError}; use toyos::surface; use toyos::Connection; -use toyos::shm::SharedMemory; +use toyos::shm::{Lent, SharedMemory}; /// Re-exported because [`KeyPress`] is made of them and a client that holds /// its own translator — `console`, a test that stands in for a surface — /// should not have to name a second crate to do what this one does. @@ -127,9 +127,10 @@ pub enum CreateError { NotEndowed, /// The compositor exited: its port is closed for good. CompositorGone, - /// The kernel refused a call of the exchange: the connection, a frame, or - /// the map of the region the compositor sent. + /// The kernel refused a call of the exchange: the connection or a frame. Kernel(SyscallError), + /// The region the compositor sent is refused ([`SharedMemory::lent`]). + Lent(Lent), /// The exchange broke off: the compositor closed the connection, having /// exited or refused (its log says which), or sent a frame this client /// cannot read. @@ -157,6 +158,7 @@ impl std::fmt::Display for CreateError { Self::Kernel(e) => { write!(f, "the kernel refused the exchange with the compositor ({e:?})") } + Self::Lent(why) => write!(f, "the compositor's region is refused: {why}"), Self::BrokenOff => write!(f, "the compositor broke off the exchange"), Self::NoHandle => write!(f, "the compositor's answer arrived without its handle"), Self::AtCapacity => write!(f, "the compositor is at its window limit"), @@ -464,7 +466,7 @@ fn copy(bytes: &[u8]) -> Result<(), CreateError> { return Err(CreateError::Protocol(header.msg_type)); } let [region] = conn.recv_handles_exact::<1>().ok_or(CreateError::NoHandle)?; - let mut region = SharedMemory::adopt(region, bytes.len()).map_err(CreateError::Kernel)?; + let mut region = SharedMemory::lent(region, bytes.len()).map_err(CreateError::Lent)?; region.as_mut_slice().copy_from_slice(bytes); Ok(conn.signal(MSG_COPY_COMMIT)?) } @@ -550,7 +552,7 @@ impl Window { // window and sent nothing with it is not serving this client, whatever // else it is doing. let [buffer] = conn.recv_handles_exact::<1>().ok_or(CreateError::NoHandle)?; - let shm = SharedMemory::adopt(buffer, buf_size).map_err(CreateError::Kernel)?; + let shm = SharedMemory::lent(buffer, buf_size).map_err(CreateError::Lent)?; let poller = Poller::new(1); Ok(Self { @@ -653,7 +655,7 @@ impl Window { let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { return Event::Close; }; - let Ok(shm) = SharedMemory::adopt(buffer, buf_size) else { + let Ok(shm) = SharedMemory::lent(buffer, buf_size) else { return Event::Close; }; self.shm = shm; @@ -676,7 +678,7 @@ impl Window { let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { return Event::ClipboardPaste(Vec::new()); }; - let Ok(shm) = SharedMemory::adopt(buffer, info.len as usize) else { + let Ok(shm) = SharedMemory::lent(buffer, info.len as usize) else { return Event::ClipboardPaste(Vec::new()); }; Event::ClipboardPaste(shm.as_slice().to_vec()) diff --git a/system/compositor/desktop/src/budget.rs b/system/compositor/desktop/src/budget.rs index 18d02810eed..5a37dcb73ea 100644 --- a/system/compositor/desktop/src/budget.rs +++ b/system/compositor/desktop/src/budget.rs @@ -1,5 +1,6 @@ use crate::rect::Rect; use crate::window::Level; +use toyos_abi::syscall::SHM_PAGE; /// Share of physical memory the compositor will hold in window buffers. /// @@ -9,22 +10,19 @@ use crate::window::Level; /// eighths for the kernel, the other daemons, and the clients' own heaps. pub const WINDOW_BUDGET_SHARE: u64 = 8; -/// The kernel rounds every shared region up to one of these -/// (`shared_memory::alloc` calls `align_2m`), so this and not the pixel count -/// is what a window costs. -const PAGE_2M: u64 = 2 * 1024 * 1024; - /// Most physical memory one window can cost at this screen size. /// /// A screen-sized content buffer is the largest the compositor ever hands out: /// `MSG_CREATE_WINDOW` refuses a request bigger than the screen, and every /// path that grows a window afterwards — maximize, snap, drag-resize — is -/// bounded by the screen too. A one-pixel window still costs a whole page. +/// bounded by the screen too. A buffer costs whole [`SHM_PAGE`]s, so a +/// one-pixel window still costs one. /// /// Deliberately a function of the screen — a bigger screen means bigger /// windows means fewer of them — which no bare constant expresses. pub fn window_bytes(screen: Rect) -> u64 { - (screen.area() * 4).div_ceil(PAGE_2M).max(1) * PAGE_2M + let page = SHM_PAGE as u64; + (screen.area() * 4).div_ceil(page).max(1) * page } /// How many windows the compositor will hold at this screen size. @@ -101,10 +99,11 @@ mod tests { #[test] fn a_window_costs_whole_pages() { - assert_eq!(window_bytes(Rect::new(0, 0, 1, 1)), PAGE_2M); + let page = SHM_PAGE as u64; + assert_eq!(window_bytes(Rect::new(0, 0, 1, 1)), page); // 1920*1080*4 = 8_294_400, which is three pages and most of a fourth. - assert_eq!(window_bytes(HD), 4 * PAGE_2M); - assert_eq!(window_bytes(Rect::new(0, 0, 2048, 2048)), 8 * PAGE_2M); + assert_eq!(window_bytes(HD), 4 * page); + assert_eq!(window_bytes(Rect::new(0, 0, 2048, 2048)), 8 * page); } #[test] diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index 31e78b53685..4978fc349ff 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -66,7 +66,7 @@ use toyos_blockring::entry::{Completion, Op}; use toyos_blockring::layout::{ServerRings, DEPTH}; use toyos_blockring::server::{ServerSession, Taken}; use toyos_blockring::wire::{self, Grant, Opened, Refusal}; -use toyos_blockring::{BLOCK_BYTES, PORT, SESSION_BYTES}; +use toyos_blockring::{BLOCK_BYTES, PORT}; /// How long a command may go unanswered before the controller is reset to /// take it back. Policy: NVMe defines no per-command bound, and a flush of a @@ -353,23 +353,15 @@ impl Service { /// What an open answers: a session to admit, or its refusal. `region` is /// the client's and is consumed either way. fn open(&mut self, guid: [u8; 16], region: toyos::RawHandle, grant: Option) -> Result { - let region = Region::adopt(region).map_err(|_| Refusal::Malformed)?; + // A region the kernel did not allocate, or longer than a session, which would spend the claim's bound on the kernel's side for every other client, is refused whole. + let region = Region::lent(region).map_err(|_| Refusal::Malformed)?; let (first, blocks) = self.place(guid, grant)?; let writes = grant.expect("placed through a grant").writes; match self.ctrl.up().claim().dma_map(region.handle()) { - Ok(mapping) if mapping.bytes == SESSION_BYTES as u64 => { + Ok(mapping) => { let (rings, opened) = Opened::over(region.words(), blocks, guid); Ok(Opening { region, rings, opened, device_addr: mapping.device_addr, first, writes }) } - // A region longer than a session would spend the claim's bound on - // the kernel's side for every other client: refused whole. - Ok(mapping) => { - if let Err(why) = self.ctrl.up().claim().dma_unmap(mapping.device_addr) { - panic!("diskserver: the kernel would not take an oversized region back: {why:?}"); - } - self.holds.release(first); - Err(Refusal::Malformed) - } Err(why) => { self.holds.release(first); Err(match why { diff --git a/system/diskserver/src/region.rs b/system/diskserver/src/region.rs index 581bd505105..5f4c62fa5fb 100644 --- a/system/diskserver/src/region.rs +++ b/system/diskserver/src/region.rs @@ -7,7 +7,7 @@ use core::sync::atomic::AtomicU32; -use toyos::shm::SharedMemory; +use toyos::shm::{Lent, SharedMemory}; use toyos_abi::syscall::SyscallError; use toyos_blockring::layout::{RING_WORDS, SESSION_BYTES}; use toyos_blockring::Run; @@ -24,10 +24,10 @@ impl Region { Ok(Self { memory: SharedMemory::create(SESSION_BYTES)? }) } - /// A region a client sent. Shared memory comes in whole 2 MiB pages and - /// never less, so every region a peer can send is at least a session long. - pub fn adopt(handle: toyos::RawHandle) -> Result { - Ok(Self { memory: SharedMemory::adopt(handle, SESSION_BYTES)? }) + /// A region a client sent, refused unless it is memory the kernel + /// allocated and exactly a session long. + pub fn lent(handle: toyos::RawHandle) -> Result { + Ok(Self { memory: SharedMemory::lent(handle, SESSION_BYTES)? }) } /// The ring words, as the rings take them. diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index f7009498074..e3dcaf14849 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -56,7 +56,7 @@ use toyos::fs::*; use toyos::ipc::{self, Connection, RxStep, TrySendError}; use toyos::poller::{Poller, READABLE}; use toyos::port::Acceptor; -use toyos::shm::SharedMemory; +use toyos::shm::{Lent, SharedMemory}; use toyos::volatile::Window; use toyos::Pipe; use toyos_abi::syscall::{SyscallError, DEV_PREFIX, MAX_BADGE, SERVE_PREFIX}; @@ -592,9 +592,11 @@ impl Server { let Some([lent]) = client.conn.recv_handles_exact::<1>() else { return Answer::Drop("its hello carried no window"); }; - match SharedMemory::adopt(lent, WINDOW_BYTES) { + match SharedMemory::lent(lent, WINDOW_BYTES) { Ok(window) => client.window = Some(window), - Err(_) => return Answer::Drop("its window would not map"), + Err(Lent::Kernel(_)) => return Answer::Drop("its window would not map"), + Err(Lent::NotRam) => return Answer::Drop("its window is not memory the kernel allocated"), + Err(Lent::Length { .. }) => return Answer::Drop("its window is not WINDOW_BYTES long"), } let granted = if client.writes { RIGHT_WRITE } else { 0 }; // The volume is this start's pid, which the kernel never issues diff --git a/system/logkeeper/src/origin.rs b/system/logkeeper/src/origin.rs index 90ab18e59e4..0ae1e96fd18 100644 --- a/system/logkeeper/src/origin.rs +++ b/system/logkeeper/src/origin.rs @@ -164,8 +164,8 @@ pub struct Origin { impl Origin { /// Map a ring the supervisor sent, under the name and pid it sent with it. pub fn open(tag: Tag<'_>, pid: u32, ring: toyos::RawHandle, alive: Pipe) -> Result { - let region = SharedMemory::adopt(ring, RING_BYTES) - .map_err(|e| format!("{}'s ring will not map: {e:?}", tag.as_str()))?; + let region = SharedMemory::lent(ring, RING_BYTES) + .map_err(|why| format!("{}'s ring is refused: {why}", tag.as_str()))?; let base = core::ptr::NonNull::new(region.as_ptr()).ok_or("a mapped ring at null")?; // SAFETY: `region` maps `RING_BYTES` and lives as long as this origin, // which is as long as the view is used. diff --git a/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs b/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs index 917821876e7..0456d92f61f 100644 --- a/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs +++ b/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs @@ -148,7 +148,7 @@ fn invited() { // The frame first, then the handle it was sent ahead of. conn.recv_header().expect("the owner's announcement"); let [handle] = conn.recv_handles_exact::<1>().expect("the region the owner sent"); - let region = SharedMemory::adopt(handle, REGION).expect("adopt the region"); + let region = SharedMemory::lent(handle, REGION).expect("take the region"); println!("read {}", String::from_utf8_lossy(®ion.as_slice()[..SECRET.len()])); std::io::stdout().flush().expect("invited: flush the report"); } diff --git a/tests/guest/rust-tests/src/bin/handle_lifetime.rs b/tests/guest/rust-tests/src/bin/handle_lifetime.rs index 3a672863e52..a87580dfa70 100644 --- a/tests/guest/rust-tests/src/bin/handle_lifetime.rs +++ b/tests/guest/rust-tests/src/bin/handle_lifetime.rs @@ -28,6 +28,7 @@ use std::process::{Child, ChildStdout, Command, Stdio}; use toyos::census::Census; use toyos::{namespace, port, AsHandle}; +use toyos_abi::handle::ObjectKind; use toyos_abi::inbox::RingLayout; use toyos_abi::syscall::{self, OpenFlags, SeekFrom, SyscallError, SERVE_PREFIX}; @@ -228,7 +229,7 @@ fn kill_releases_ring() { // Non-vacuity: an instrument that cannot see eight rings arrive cannot see // them leave either, and the reclaim assertion would pass on a kernel that // frees nothing. - let taken = held.kind("Inbox").saturating_sub(before.kind("Inbox")); + let taken = held.kind(ObjectKind::Inbox).saturating_sub(before.kind(ObjectKind::Inbox)); assert!( taken >= HOLDER_RINGS as u64, "the holder made {HOLDER_RINGS} rings and the live Inbox count moved {taken}: \ diff --git a/tests/guest/rust-tests/src/bin/lent_device_memory_refused.rs b/tests/guest/rust-tests/src/bin/lent_device_memory_refused.rs new file mode 100644 index 00000000000..715da260a94 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/lent_device_memory_refused.rs @@ -0,0 +1,35 @@ +//! A file server lets go a hello whose window is a device's registers, which +//! the kernel says are not memory it allocated, and answers the next client as +//! the same process. +//! +//! The device is QEMU's virtio NIC, which no program of +//! `tests/images/testcases.toml` holds. + +use toyos::endow::Endowments; +use toyos::syscap::SysCap; +use toyos::{AsHandle, OwnedHandle, PciDev}; +use toyos_abi::syscall::{self, PciId, SyscallError, SYSCAP_LABEL}; + +#[path = "../lent_hello.rs"] +mod lent_hello; +use lent_hello::{hello, window}; + +const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; + +fn main() { + let served = hello(window()).expect("lent_device_memory_refused: a window of WINDOW_BYTES is served"); + + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("lent_device_memory_refused: a device-minting capability"); + let nic: PciDev = cap.claim_pci(NIC).expect("lent_device_memory_refused: the virtio NIC's claim"); + let info = nic.describe().expect("lent_device_memory_refused: the claim's description"); + let bar = info.bar_bytes.iter().position(|&bytes| bytes != 0).expect("lent_device_memory_refused: a memory BAR"); + let bar = syscall::device_bar_map(nic.as_handle(), bar as u32).expect("lent_device_memory_refused: the BAR's region"); + // SAFETY: the kernel installed the handle for this call, and nothing else answers for it. + let answer = hello(unsafe { OwnedHandle::from_raw(bar) }); + let again = hello(window()).expect("lent_device_memory_refused: a window after the refusal is served"); + + println!("lent_device_memory_refused: a hello lending a device's BAR answered {answer:?}"); + assert_eq!(answer, Err(SyscallError::Gone), "lent_device_memory_refused: the BAR was not let go unanswered"); + assert_eq!(again, served, "lent_device_memory_refused: the server that let go is not the one that answers"); + println!("lent_device_memory_refused: the server that let go answers the next client, volume {served}"); +} diff --git a/tests/guest/rust-tests/src/bin/lent_window_refused.rs b/tests/guest/rust-tests/src/bin/lent_window_refused.rs new file mode 100644 index 00000000000..387e80e96cd --- /dev/null +++ b/tests/guest/rust-tests/src/bin/lent_window_refused.rs @@ -0,0 +1,32 @@ +//! A file server lets go a hello whose window the kernel says is not a region +//! `WINDOW_BYTES` long, whatever the client says: a pipe end, or a region +//! twice that; and the server that let it go answers the next client as the +//! same process. + +use toyos::fs::WINDOW_BYTES; +use toyos_abi::syscall::SyscallError; + +#[path = "../lent_hello.rs"] +mod lent_hello; +use lent_hello::{hello, region, window}; + +fn main() { + let served = hello(window()).expect("lent_window_refused: a window of WINDOW_BYTES is served"); + let (read, _write) = toyos::pipe_pair().expect("lent_window_refused: a pipe"); + + // Every hostile hello is made before any is judged, so a server that serves one is seen serving each. + let answers = [ + ("a pipe end", hello(read.into())), + ("a region twice WINDOW_BYTES long", hello(region(2 * WINDOW_BYTES))), + ]; + let again = hello(window()).expect("lent_window_refused: a window after the refusals is served"); + + for (what, answer) in &answers { + println!("lent_window_refused: a hello lending {what} answered {answer:?}"); + } + assert!( + answers.iter().all(|(_, answer)| *answer == Err(SyscallError::Gone)), + "lent_window_refused: a hostile hello was not let go unanswered" + ); + assert_eq!(again, served, "lent_window_refused: the server that let go is not the one that answers"); +} diff --git a/tests/guest/rust-tests/src/bin/mutual_kill.rs b/tests/guest/rust-tests/src/bin/mutual_kill.rs index 60f609206ba..4a8d52477be 100644 --- a/tests/guest/rust-tests/src/bin/mutual_kill.rs +++ b/tests/guest/rust-tests/src/bin/mutual_kill.rs @@ -121,7 +121,7 @@ fn killer() -> ! { let header = conn.recv_header().expect("the arming frame"); assert_eq!(header.msg_type, ARM, "an unexpected frame"); let [word_handle, victim] = conn.recv_handles_exact::<2>().expect("the word and the victim"); - let go = SharedMemory::adopt(word_handle, WORD_BYTES).expect("map the word"); + let go = SharedMemory::lent(word_handle, WORD_BYTES).expect("map the word"); // SAFETY: the parent sent one handle to the victim, which nothing else here owns. let victim = unsafe { Process::from_raw(victim) }; diff --git a/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs b/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs index b5b37bedddf..11fd6b3b3c6 100644 --- a/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs +++ b/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs @@ -19,6 +19,7 @@ use std::process::{Command, Stdio}; use toyos::census::Census; use toyos::shm::SharedMemory; use toyos::{namespace, port}; +use toyos_abi::handle::ObjectKind; use toyos_abi::syscall::SVC_LABEL; #[path = "../census_wait.rs"] @@ -56,7 +57,7 @@ fn main() { // Non-vacuity: if the instrument could not see sixteen regions arrive, it // cannot see them come back either, and the reclaim assertion below would // pass on a kernel that frees nothing. - let taken = held.kind("SharedMem").saturating_sub(start.kind("SharedMem")); + let taken = held.kind(ObjectKind::SharedMem).saturating_sub(start.kind(ObjectKind::SharedMem)); assert!( taken >= ROUNDS as u64, "{ROUNDS} regions were allocated and the live SharedMem count moved {taken}: \ @@ -93,7 +94,7 @@ fn main() { out.read_line(&mut line).expect("donor release line"); assert_eq!(line.trim(), "released", "the donor did not report letting go"); - let region = SharedMemory::adopt(sent, REGION) + let region = SharedMemory::lent(sent, REGION) .expect("a region this process holds a handle to was reclaimed under it"); assert_eq!( ®ion.as_slice()[..PAYLOAD.len()], diff --git a/tests/guest/rust-tests/src/lent_hello.rs b/tests/guest/rust-tests/src/lent_hello.rs new file mode 100644 index 00000000000..14e8abb36fe --- /dev/null +++ b/tests/guest/rust-tests/src/lent_hello.rs @@ -0,0 +1,36 @@ +//! A raw client's hello to `fs:/home`, lending a window of the caller's +//! choosing, as the lent-window tests make it. + +use toyos::fs::{Reply, Request, HELLO, REPLY, WINDOW_BYTES}; +use toyos::ipc::IpcError; +use toyos::shm::SharedMemory; +use toyos::OwnedHandle; +use toyos_abi::syscall::SyscallError; + +/// What `fs:/home` answers a hello lending `window`: the volume, which is the +/// server's pid, or `Gone` for a server that let the client go unanswered. +pub fn hello(window: OwnedHandle) -> Result { + let names = toyos::endow::namespace().expect("a namespace"); + let conn = names.open("fs:/home").expect("fs:/home"); + conn.send_handles([window]).expect("the window into the connection"); + conn.send(HELLO, &Request::new()).expect("the hello"); + let header = match conn.recv_header() { + Ok(header) => header, + Err(IpcError::Disconnected | IpcError::Syscall(SyscallError::Gone)) => return Err(SyscallError::Gone), + Err(e) => panic!("the hello's answer: {e:?}"), + }; + assert_eq!(header.msg_type, REPLY, "a hello answered by a frame that is no reply"); + let reply: Reply = conn.recv_payload(&header).expect("the reply's words"); + assert_eq!(reply.status, 0, "a hello refused with a word, not let go"); + Ok(reply.value2) +} + +/// A region of `bytes`, for lending. +pub fn region(bytes: usize) -> OwnedHandle { + SharedMemory::create(bytes).expect("a region").share().expect("its handle") +} + +/// A window the server serves. +pub fn window() -> OwnedHandle { + region(WINDOW_BYTES) +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 36f92181a34..4bb3bc94a32 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -193,6 +193,9 @@ const RUST_SKIP: &[&str] = &[ // It claims QEMU's virtio NIC, which the T14 has none of: `bar_map_again` // runs it. "bar_map_again", + // It lends fileserver the virtio NIC's BAR, which the T14 has none of: + // `lent_device_memory_refused` runs it. + "lent_device_memory_refused", // It needs ToyOS's own clang and LLD on ROOT, which only its boot stages: // `hosted_clang_hello` runs it on tests/images/hostedclangcase.toml. "hosted_clang_hello", @@ -409,6 +412,10 @@ const MACHINE_TESTS: &[&str] = &[ // function is its I219, and a kernel that dies on this takes the bench's // machine down with it. "bar_map_again", + // A region that is not memory the kernel allocated, lent to a file server: + // the BAR of a function no program of the boot holds, which on the T14 is + // its I219, as `bar_map_again` says. + "lent_device_memory_refused", // `images/console.toml`'s image, which runs no job and hands no machine // back: a metal boot of it ends with a hand on the power button. "console_image_boots", @@ -3915,6 +3922,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "machine_shutdown_wire_kept" => power::machine_shutdown_wire_held(test_config, true), "machine_shutdown_wire_at_the_seal" => power::machine_shutdown_wire_at_the_seal(test_config), "bar_map_again" => bar_map_again(test_config), + "lent_device_memory_refused" => lent_device_memory_refused(test_config), "console_image_boots" => console_image_boots(), "desktop_without_x2apic" => desktop_without_x2apic(), "nvme_disk_keeps_log_and_home" => nvme_disk_keeps_log_and_home(test_config), @@ -4751,6 +4759,34 @@ fn bar_map_again(test_config: &Path) -> Result<(), String> { Ok(()) } +/// fileserver refuses a hello whose window is the virtio NIC's BAR, by the +/// kernel's answer and by name, and answers the next client as the process +/// that refused it. +fn lent_device_memory_refused(test_config: &Path) -> Result<(), String> { + const JOB: &str = "lent_device_memory_refused"; + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/guest/rust-tests"), JOB); + let mut qemu = + QemuInstance::boot_with_options(test_config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + serial::Serial::boot(&qemu).must_be_clean()?; + let result = qemu.run_test("test_rs_lent_device_memory_refused", Duration::from_secs(60)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + for said in [ + ": its window is not memory the kernel allocated", + "lent_device_memory_refused: the server that let go answers the next client", + ] { + let Some(line) = result.stdout.lines().find(|l| l.contains(said)) else { + return Err(format!("nobody said `{said}`:\n{}", result.stdout)); + }; + eprintln!(" [claims] {}", line.trim()); + } + Ok(()) +} + /// Every device delivery is still cpu0's, and no CPU took a shootdown IPI the /// issuer did not count. ///