From 14e1334e200758c3b87f4165d4636a7f37dc2b52 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 00:43:21 +0200 Subject: [PATCH 1/4] The kernel answers what a received handle is and what a lent region is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SYS_HANDLE_RECV writes a `Received { handle, kind }` per handle, the kind an `ObjectKind` word the kernel's `kobject!` maps by variant name, so a row without an ABI kind does not compile. `ObjectKind` replaces `OBJECT_KINDS`: one list of kinds in the ABI, and the census reads it by kind, not by name. SYS_SHM_MAP writes a `ShmMapping { addr, len, ram }`: the mapping's length, which is the whole region, and `SharedMemObject::ram()`'s verdict — memory the kernel allocated, as against a BAR or a scanout. The SDK takes both. `recv_handles_exact` is told the kinds the protocol sends and refuses any other batch, closing it, where a handle of the wrong kind used typed ends the receiver (exit 139). `SharedMemory::lent` takes a region a peer lent only if the kernel says it is RAM and exactly the protocol's length in whole SHM_PAGEs; `adopt` is left to regions the kernel itself answered and asserts the view fits the mapping. Every caller is moved: fileserver, diskserver, logkeeper, soundserver's and the compositor's clients, netstack's piped requests (whose pipe ends are now told apart by kind, so a reversed pair is refused), the supervisor's launch batch, toyos::log's stdio ring, and the guest tests. diskserver's oversize branch after `dma_map` goes: `lent` refuses that region first. Closes issues/a-received-handle-has-no-knowable-type.md and issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md: the `lent_window_refused` machine test lends fileserver a pipe end, a region twice WINDOW_BYTES and the virtio NIC's BAR, each refused by name, and the same fileserver answers the next hello. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...ps-a-lent-window-at-the-size-it-expects.md | 23 ---- .../a-received-handle-has-no-knowable-type.md | 75 ----------- kernel/src/object/mod.rs | 7 + kernel/src/object/shm.rs | 3 + kernel/src/syscall/dispatch.rs | 13 +- kernel/src/syscall/ipc.rs | 17 ++- lib/logkeeper-api/src/lib.rs | 2 +- sdk/abi/src/handle.rs | 93 +++++++++++++ sdk/abi/src/syscall.rs | 86 +++++++----- sdk/toyos/src/audio.rs | 9 +- sdk/toyos/src/census.rs | 27 ++-- sdk/toyos/src/fs.rs | 2 +- sdk/toyos/src/ipc.rs | 40 +++--- sdk/toyos/src/launch.rs | 2 +- sdk/toyos/src/lib.rs | 1 + sdk/toyos/src/log/stdio.rs | 9 +- sdk/toyos/src/shm.rs | 127 ++++++++++++++++-- sdk/window/src/lib.rs | 25 ++-- system/diskserver/src/main.rs | 18 +-- system/diskserver/src/region.rs | 10 +- system/fileserver/src/main.rs | 12 +- system/logkeeper/src/main.rs | 4 +- system/logkeeper/src/origin.rs | 4 +- system/netstack/src/pipes.rs | 5 - system/netstack/src/serve.rs | 12 +- system/supervisor/src/main.rs | 5 +- .../rust-tests/src/bin/abuse_shared_grant.rs | 4 +- .../rust-tests/src/bin/fs_stream_offset.rs | 2 +- .../rust-tests/src/bin/handle_lifetime.rs | 4 +- .../rust-tests/src/bin/lent_window_refused.rs | 70 ++++++++++ tests/guest/rust-tests/src/bin/mutual_kill.rs | 6 +- .../src/bin/netstack_socket_churn.rs | 35 +++-- .../rust-tests/src/bin/pipe_other_end_gone.rs | 2 +- .../guest/rust-tests/src/bin/process_tree.rs | 2 +- .../src/bin/shm_release_reclaims.rs | 8 +- .../rust-tests/src/bin/spawn_lands_claimed.rs | 2 +- tests/toyos.rs | 38 ++++++ 37 files changed, 527 insertions(+), 277 deletions(-) delete mode 100644 issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md delete mode 100644 issues/a-received-handle-has-no-knowable-type.md create mode 100644 tests/guest/rust-tests/src/bin/lent_window_refused.rs 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 deleted file mode 100644 index 51ea977c709..00000000000 --- a/issues/a-received-handle-has-no-knowable-type.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-10 ---- - -# A process cannot ask what it received, and using it 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. - -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 -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 -`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. - -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. - -## Three ways out, and none is free - -1. **Report the kind.** `SYS_HANDLE_RECV` writes `(RawHandle, kind)` pairs - instead of bare handles. One ABI shape change, in-tree only — no fork names - `handle_recv` (the whole fork estate was swept for this branch and - `handle_recv` is not in it). The receiver then refuses by - name and the fail-fast policy is untouched. -2. **A syscall** answering the kind of a handle. Cheaper to write and - worse: it is a second place to ask, and it makes "what is this" a round trip - rather than part of the answer that produced it. -3. **`WrongType` stops being fatal.** Rejected: it is fatal for a reason, and - three quarters of its call sites really are a bug in the caller. - -(1) is the recommendation. It is not this review's to take — it is an ABI -change, and a new syscall is the owner's to approve. - -## Ruled not a merge blocker, 2026-08-10 - -Judged while clearing PR #22's blockers, with the reasoning written down because -the next reader will ask why a class this wide was left open. - -- **The fix is an ABI shape change and the ABI was the owner's to approve.** It - widens `SYS_HANDLE_RECV`'s answer from `n` to `n` pairs, which is a syscall - the owner approved changing shape after the fact. - -So it stays open, and what it is waiting on is a decision rather than an -instrument. - -`launcher_refusals` is deleted; `issues/parallel-tests-red-under-other-suites.md` records the commit that restores it. diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index d377a1408a8..09e2c366152 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -164,6 +164,13 @@ macro_rules! kobject { } } + /// This object's type, as `SYS_HANDLE_RECV` answers it. + pub fn object_kind(&self) -> toyos_abi::handle::ObjectKind { + match self { + $(Self::$variant(_) => toyos_abi::handle::ObjectKind::$variant,)+ + } + } + fn run_zero_handles(&self) { match self { $(Self::$variant(o) => o.on_zero_handles(),)+ 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..ce953fe1024 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -508,7 +508,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> return SyscallError::InvalidArgument.to_u64(); } let Some(mut out) = (a3 as usize) - .checked_mul(HANDLE_LEN) + .checked_mul(core::mem::size_of::()) .and_then(|len| ctx.user_bytes_mut(UserAddr::new(a2), len as u64)) else { return bad_addr; @@ -516,7 +516,12 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> sys_handle_recv(RawHandle(a1 as u32), &mut out, a3 as usize) } SYS_SHM_CREATE => sys_shm_create(a1), - SYS_SHM_MAP => sys_shm_map(RawHandle(a1 as u32)), + SYS_SHM_MAP => { + // Taken before the mapping exists: a bad address must not leave a mapping the caller was never told of. + let len = core::mem::size_of::() as u64; + let Some(mut out) = ctx.user_bytes_mut(UserAddr::new(a2), len) else { return bad_addr }; + sys_shm_map(RawHandle(a1 as u32), &mut out) + } SYS_PORT_CREATE => sys_port_create(), SYS_PORT_MINT => { // Copied in before any handle is looked up: the kernel holds the badge, never the caller's page. @@ -614,8 +619,8 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> 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", + .eq(toyos_abi::handle::ObjectKind::ALL.iter().map(|kind| kind.name())), + "kobject! and toyos_abi::handle::ObjectKind declare different objects", ); match crate::object::census::live().nth(a2 as usize) { Some((_, live)) => live, diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index b16b2760c70..77838a23a76 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -360,7 +360,8 @@ pub(super) fn sys_shm_create(size: u64) -> u64 { process::with_process_data(|data| handle_result(ops::install(&mut data.handles, object))) } -pub(super) fn sys_shm_map(h: RawHandle) -> u64 { +/// Map a region into the caller and answer where, how long, and whether it is RAM this kernel allocated: what a receiver needs to refuse a region a peer lent it. +pub(super) fn sys_shm_map(h: RawHandle, out: &mut crate::user_ptr::UserBytesMut) -> u64 { let shm = match process::with_process_data(|data| { data.handles.get::(h, Rights::MAP) }) { @@ -369,7 +370,11 @@ pub(super) fn sys_shm_map(h: RawHandle) -> u64 { }; let pt = process::current_address_space(); match shm.map_into(process::current_process(), &pt) { - Ok(vaddr) => vaddr, + Ok(addr) => { + let ram = u32::from(shm.ram().is_some()); + out.write_at(0, toyos_abi::usersafe::bytes(&ShmMapping { addr, len: shm.size(), ram, _pad: 0 })); + 0 + } Err(e) => e.to_u64(), } } @@ -408,7 +413,7 @@ pub(super) fn sys_handle_send(conn_h: RawHandle, handles: &crate::user_ptr::User } } -/// Take the oldest batch the peer sent. Never blocks; zero means none queued. +/// Take the oldest batch the peer sent, each handle with its kind. Never blocks; zero means none queued. pub(super) fn sys_handle_recv( conn_h: RawHandle, out: &mut crate::user_ptr::UserBytesMut, @@ -430,8 +435,10 @@ pub(super) fn sys_handle_recv( let Some(batch) = conn.recv_bounded(cap)? else { return Ok(0) }; let count = batch.len(); for (i, entry) in batch.into_iter().enumerate() { - let h = data.handles.install(entry).expect("room was asked for first"); - out.write_at(i * HANDLE_LEN, &h.0.to_ne_bytes()); + let kind = entry.object().object_kind() as u32; + let handle = data.handles.install(entry).expect("room was asked for first"); + let received = toyos_abi::handle::Received { handle, kind }; + out.write_at(i * size_of::(), toyos_abi::usersafe::bytes(&received)); } Ok::<_, crate::object::Refusal>(count as u64) }); diff --git a/lib/logkeeper-api/src/lib.rs b/lib/logkeeper-api/src/lib.rs index dc32932d5fe..72ee41eecd7 100644 --- a/lib/logkeeper-api/src/lib.rs +++ b/lib/logkeeper-api/src/lib.rs @@ -24,7 +24,7 @@ pub fn read() -> Result { } let boot_so_far: u64 = conn.recv_payload(&header).map_err(|e| format!("logkeeper's answer is short: {e:?}"))?; - let [raw] = conn.recv_handles_exact::<1>().ok_or("logkeeper's answer carried no pipe")?; + let [raw] = conn.recv_handles_exact([toyos::ObjectKind::PipeRead]).ok_or("logkeeper's answer carried no pipe")?; // SAFETY: the kernel moved this handle into this table with the frame // that names it, and nothing else answers for it. let pipe = unsafe { Pipe::from_raw(raw) }; diff --git a/sdk/abi/src/handle.rs b/sdk/abi/src/handle.rs index ec93f51273a..c6cf067a84f 100644 --- a/sdk/abi/src/handle.rs +++ b/sdk/abi/src/handle.rs @@ -57,6 +57,99 @@ impl RawHandle { /// any table issues. pub const HANDLE_INVALID: RawHandle = RawHandle(u32::MAX); +/// Every kind of kernel object a handle can name, in the order the kernel's +/// `kobject!` declares them: the kernel answers a received handle's kind as +/// this word, and an index into [`ObjectKind::ALL`] is the one +/// [`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 } + } + + pub const fn name(self) -> &'static str { + match self { + Self::PipeRead => "PipeRead", + Self::PipeWrite => "PipeWrite", + Self::Connection => "Connection", + Self::Device => "Device", + Self::Acceptor => "Acceptor", + Self::Inbox => "Inbox", + Self::SharedMem => "SharedMem", + Self::Connector => "Connector", + Self::Namespace => "Namespace", + Self::File => "File", + Self::Console => "Console", + Self::SysCap => "SysCap", + Self::Process => "Process", + } + } +} + +// `from_word` indexes `ALL` by the word: a row out of order would answer a +// received handle's kind as its 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; + } +}; + +crate::user_safe! { + /// One handle [`handle_recv`](crate::syscall::handle_recv) installed, and + /// what it names: a receiver refuses a handle of the wrong kind by name, + /// where using it typed would end the receiver. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct Received { + pub handle: RawHandle, + /// An [`ObjectKind`] word; [`Received::kind`] decodes it. + pub kind: u32, + } +} + +impl Received { + /// The kernel writes only [`ObjectKind`] words here, so any other is a + /// kernel and SDK that disagree on the ABI. + pub fn kind(&self) -> ObjectKind { + ObjectKind::from_word(self.kind) + .unwrap_or_else(|| panic!("handle_recv answered object kind {}, which the ABI has no kind for", self.kind)) + } +} + /// 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..398498491b8 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -177,15 +177,20 @@ pub const SYS_NAMESPACE_OPEN: u64 = 102; /// **handles are sent before the frame that announces them** and a receiver /// that has the frame already has the handles. pub const SYS_HANDLE_SEND: u64 = 103; -/// Take the oldest batch of handles the peer sent. See [`handle_recv`]. +/// Take the oldest batch of handles the peer sent, each with its kind. 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`]. +/// Map a region into the caller, and answer where, how long, and whether it +/// is memory the kernel allocated. 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`]. /// @@ -434,7 +439,7 @@ pub const MAX_LABELS_LEN: usize = 4096; /// Bytes of label blob one spawn may carry: the kernel adds [`SELF_LABEL`]'s. pub const MAX_SPAWN_LABELS_LEN: usize = MAX_LABELS_LEN - SELF_LABEL.len(); -use crate::handle::Rights; +use crate::handle::{Received, Rights}; use crate::pci::{DmaGrant, DmaMapping}; use crate::{Pid, RawHandle, HANDLE_INVALID}; @@ -815,7 +820,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::ALL`](crate::handle::ObjectKind::ALL) index. /// /// **Per kind and not a total**: an object of one kind that is never /// released is invisible behind ordinary churn in another. @@ -886,31 +891,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 @@ -2001,12 +1981,13 @@ pub fn handle_send(conn: RawHandle, handles: &[RawHandle]) -> Result<(), Syscall )) } -/// Take the oldest batch the peer sent, into `out`. Answers how many it wrote. +/// Take the oldest batch the peer sent, into `out`, each handle with the kind +/// of object it names. Answers how many it wrote. /// /// Never blocks: zero means nothing is queued right now. A batch larger than /// `out` is `InvalidArgument` and stays queued — `out` should be /// [`MAX_TRANSFER_HANDLES`] long. -pub fn handle_recv(conn: RawHandle, out: &mut [RawHandle]) -> Result { +pub fn handle_recv(conn: RawHandle, out: &mut [Received]) -> Result { check(syscall( SYS_HANDLE_RECV, conn.0 as u64, @@ -2027,7 +2008,38 @@ pub fn shm_create(size: usize) -> Result { check(syscall(SYS_SHM_CREATE, size as u64, 0, 0, 0)).map(|v| RawHandle(v as u32)) } -/// Map the region `shm` names into this process. Needs [`Rights::MAP`]. +crate::user_safe! { + /// Where [`shm_map`] put a region, and what the region is. + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub struct ShmMapping { + pub addr: u64, + /// The mapping's length: the whole region. + pub len: u64, + /// 1 for memory the kernel allocated, as [`SYS_SHM_CREATE`] makes; 0 + /// for anything else, a device's registers or a scanout. + pub ram: u32, + pub _pad: u32, + } +} + +impl ShmMapping { + pub fn ptr(&self) -> *mut u8 { + core::ptr::with_exposed_provenance_mut(self.addr as usize) + } + + /// Whether the region is memory the kernel allocated: the only kind a + /// process may take from a peer as a buffer. + pub fn is_ram(&self) -> bool { + match self.ram { + 0 => false, + 1 => true, + other => panic!("shm_map answered memory word {other}, which is neither 0 nor 1"), + } + } +} + +/// Map the region `shm` names into this process, and answer where, how long, +/// and whether it is memory the kernel allocated. Needs [`Rights::MAP`]. /// /// Idempotent: a second call answers the first call's address. [`SyscallError::Gone`] /// when the region's last handle was closed under the call: nothing is mapped. @@ -2036,9 +2048,9 @@ pub fn shm_create(size: usize) -> Result { /// /// # Safety /// Caller must manage the returned pointer. -pub unsafe fn shm_map(shm: RawHandle) -> Result<*mut u8, SyscallError> { - check(syscall(SYS_SHM_MAP, shm.0 as u64, 0, 0, 0)) - .map(|addr| core::ptr::with_exposed_provenance_mut(addr as usize)) +pub unsafe fn shm_map(shm: RawHandle) -> Result { + let mut out = ShmMapping { addr: 0, len: 0, ram: 0, _pad: 0 }; + check_unit(syscall(SYS_SHM_MAP, shm.0 as u64, core::ptr::from_mut(&mut out) as u64, 0, 0)).map(|()| out) } /// The machine's [header](SYSINFO_HEADER_SIZE), then as much of the process diff --git a/sdk/toyos/src/audio.rs b/sdk/toyos/src/audio.rs index a33be4eaa11..a8f251ef9ee 100644 --- a/sdk/toyos/src/audio.rs +++ b/sdk/toyos/src/audio.rs @@ -48,6 +48,8 @@ crate::ipc_payload! { pub const STREAM_OPENED_HANDLES: usize = 2; pub const STREAM_OPENED_SHM: usize = 0; pub const STREAM_OPENED_SIGNAL: usize = 1; +pub const STREAM_OPENED_KINDS: [crate::ObjectKind; STREAM_OPENED_HANDLES] = + [crate::ObjectKind::SharedMem, crate::ObjectKind::PipeRead]; pub struct AudioSlotWriter { shm: SharedMemory, @@ -182,6 +184,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), } @@ -209,14 +213,13 @@ impl AudioStream { }; let batch = control - .recv_handles_exact::() + .recv_handles_exact(STREAM_OPENED_KINDS) .ok_or(AudioError::MissingHandles)?; let signal = crate::Pipe(crate::OwnedHandle(batch[STREAM_OPENED_SIGNAL])); 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..e139afde7d0 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()]; + let mut counts = [0u64; ObjectKind::ALL.len()]; for (i, slot) in counts.iter_mut().enumerate() { *slot = syscall::debug_with(CENSUS_KIND, i 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 { @@ -44,22 +39,22 @@ impl Census { before: &Census, ) -> 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.name(), 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, "{} {live}", kind.name())?; first = false; } if first { diff --git a/sdk/toyos/src/fs.rs b/sdk/toyos/src/fs.rs index 23028af0585..2805243e7d9 100644 --- a/sdk/toyos/src/fs.rs +++ b/sdk/toyos/src/fs.rs @@ -476,7 +476,7 @@ impl Dir { pub fn stream(&mut self, fid: u64, generation: u64, offset: u64) -> Result { self.fid_call(STREAM, generation, &Request { fid, offset, ..Request::new() })?; let conn = self.conn.as_ref().ok_or(SyscallError::Gone)?; - let [end] = conn.recv_handles_exact::<1>().ok_or(SyscallError::Unknown)?; + let [end] = conn.recv_handles_exact([crate::ObjectKind::PipeWrite]).ok_or(SyscallError::Unknown)?; Ok(Pipe(crate::OwnedHandle(end))) } diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index f59b01bc3ba..85d8d389906 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -3,6 +3,7 @@ //! Wire format: `[u32 msg_type][u32 len][len bytes payload]`. use toyos_abi::RawHandle; +use toyos_abi::handle::{ObjectKind, Received}; use toyos_abi::syscall::{self, SyscallError}; use crate::{AsHandle, OwnedHandle}; @@ -229,27 +230,31 @@ impl Connection { move_batch(handles, |batch| syscall::handle_send(self.0.raw(), batch)) } - /// Take the batch the peer sent with the frame just received. + /// Take the batch the peer sent with the frame just received, each handle + /// with the kind of object it names. /// /// Never blocks. An empty answer for a frame that promised handles is a /// peer that sent them out of order, which is a protocol error and not /// something to wait for. pub fn recv_handles( &self, - out: &mut [RawHandle; syscall::MAX_TRANSFER_HANDLES], + out: &mut [Received; syscall::MAX_TRANSFER_HANDLES], ) -> Result { syscall::handle_recv(self.0.raw(), out) } - /// The `N` handles this frame's message type says travel with it. + /// The handles of the kinds this frame's message type says travel with + /// it, in that order. /// - /// `None` for any other count, with whatever did arrive closed rather than - /// leaked: a short batch is a peer that sent its frame first, a long one is - /// a protocol this endpoint does not speak, and neither is something to - /// 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()) + /// `None` for any other count or any other kind, with whatever did arrive + /// closed rather than leaked: a short batch is a peer that sent its frame + /// first, a long one or a handle of another kind is a protocol this + /// endpoint does not speak — and using a handle of the wrong kind typed + /// would end this process — and none is something to wait for. Every + /// protocol here sends a fixed batch per message type, so this is the + /// shape every caller wants. + pub fn recv_handles_exact(&self, kinds: [ObjectKind; N]) -> Option<[RawHandle; N]> { + recv_handles_exact(self.0.raw(), kinds) } pub fn recv_bytes(&self, header: &IpcHeader, buf: &mut [u8]) -> Result { @@ -330,19 +335,18 @@ fn move_batch( /// 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]> { +pub fn recv_handles_exact(handle: RawHandle, kinds: [ObjectKind; N]) -> Option<[RawHandle; N]> { const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; - let mut batch = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; + let mut batch = [Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; syscall::MAX_TRANSFER_HANDLES]; let n = syscall::handle_recv(handle, &mut batch).ok()?; - if n != N { - for h in &batch[..n] { - syscall::close(*h); + let batch = &batch[..n]; + if !batch.iter().map(Received::kind).eq(kinds) { + for received in batch { + syscall::close(received.handle); } return None; } - let mut out = [toyos_abi::HANDLE_INVALID; N]; - out.copy_from_slice(&batch[..N]); - Some(out) + Some(core::array::from_fn(|i| batch[i].handle)) } /// [`signal`] without blocking. A bare header always fits in one write. diff --git a/sdk/toyos/src/launch.rs b/sdk/toyos/src/launch.rs index 86d740daafc..aa61f37148a 100644 --- a/sdk/toyos/src/launch.rs +++ b/sdk/toyos/src/launch.rs @@ -373,7 +373,7 @@ pub fn launch<'a>( conn.send_bytes(MSG_LAUNCH, &buf[..len]).map_err(LaunchError::Sent)?; let header = conn.recv_header().map_err(LaunchError::Sent)?; match header.msg_type { - MSG_LAUNCHED => match conn.recv_handles_exact::<1>() { + MSG_LAUNCHED => match conn.recv_handles_exact([crate::ObjectKind::Process]) { Some([process]) => Ok(Outcome::Started(process)), None => Err(LaunchError::Sent(IpcError::Malformed)), }, diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 1a72dfcdb64..953f80d8460 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -40,6 +40,7 @@ pub use device::{ }; pub use toyos_abi::RawHandle; +pub use toyos_abi::handle::ObjectKind; pub use toyos_abi::ioport; /// Trait for types that wrap a kernel handle. diff --git a/sdk/toyos/src/log/stdio.rs b/sdk/toyos/src/log/stdio.rs index b8f3df6ddf6..30368d573e3 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -237,8 +237,11 @@ mod target { let own = syscall::dup(slot).map_err(Why::Dup)?; // SAFETY: `own` is a live shared-memory handle this process holds and // never closes, so the mapping stays for the process's life. - let at = unsafe { syscall::shm_map(own) }.map_err(Why::Map)?; - let at = NonNull::new(at).ok_or(Why::Null)?; + let mapping = unsafe { syscall::shm_map(own) }.map_err(Why::Map)?; + if !mapping.is_ram() { + return Err(Why::NotRam); + } + let at = NonNull::new(mapping.ptr()).ok_or(Why::Null)?; // SAFETY: a fresh mapping of a region at least `RING_BYTES` long. if !unsafe { Ring::at(at) }.is_laid_out() { return Err(Why::NotARing); @@ -259,6 +262,7 @@ mod target { Dup(SyscallError), Map(SyscallError), Null, + NotRam, NotARing, } @@ -274,6 +278,7 @@ mod target { Self::Dup(e) => write!(f, "holds a ring that will not duplicate: {e:?}"), Self::Map(e) => write!(f, "holds a ring that will not map: {e:?}"), Self::Null => write!(f, "holds a ring the kernel mapped at null"), + Self::NotRam => write!(f, "holds a region that is not memory the kernel allocated"), Self::NotARing => write!(f, "holds a region that is not a log ring"), } } diff --git a/sdk/toyos/src/shm.rs b/sdk/toyos/src/shm.rs index ea52f91c038..e94b231480b 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 +//! of another kind is refused before this is reached +//! ([`Connection::recv_handles_exact`](crate::ipc::Connection::recv_handles_exact)), +//! and a region that is not memory the kernel allocated, or not the length +//! the protocol says, is refused here. [`SharedMemory::adopt`] is for a region +//! the kernel itself answered: a device's window or a scanout. +use core::fmt; use core::sync::atomic::AtomicU8; -use toyos_abi::syscall::{self, SyscallError}; +use toyos_abi::syscall::{self, ShmMapping, SyscallError, SHM_PAGE}; use crate::{AsHandle, OwnedHandle, RawHandle}; @@ -34,17 +43,29 @@ 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 answered — a device's window, a scanout, 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. The kernel + /// is no peer, so a mapping shorter than `size` is this caller's bug. 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 }) + let (handle, mapping) = map(handle)?; + assert!( + size <= mapping.len as usize, + "shm: a {size}-byte view of a region the kernel maps at {} bytes", + mapping.len as usize + ); + Ok(Self { handle, ptr: mapping.ptr(), size }) + } + + /// Map a region a peer lent, whose protocol says it is `bytes` long, and + /// view those bytes; refused 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, mapping) = map(handle).map_err(Lent::Map)?; + judge(&mapping, bytes)?; + Ok(Self { handle, ptr: mapping.ptr(), size: bytes }) } /// A second handle to the same region, for sending to a peer. @@ -83,8 +104,94 @@ impl SharedMemory { } } +fn map(handle: RawHandle) -> Result<(OwnedHandle, ShmMapping), SyscallError> { + let handle = OwnedHandle(handle); + // SAFETY: the mapping lives as long as the handle, which the caller keeps beside it. + let mapping = unsafe { syscall::shm_map(handle.raw()) }?; + assert!(!mapping.ptr().is_null(), "shm_map answered null"); + Ok((handle, mapping)) +} + +/// Why a lent region was refused. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Lent { + /// The kernel would not map it. + Map(SyscallError), + /// It is not memory the kernel allocated: a device's registers or a + /// scanout, 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::Map(e) => write!(f, "the kernel would not 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(mapping: &ShmMapping, bytes: usize) -> Result<(), Lent> { + if !mapping.is_ram() { + return Err(Lent::NotRam); + } + if Some(mapping.len as usize) != bytes.checked_next_multiple_of(SHM_PAGE) { + return Err(Lent::Length { is: mapping.len 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, ram: u32) -> ShmMapping { + ShmMapping { addr: 1 << 30, len: len as u64, ram, _pad: 0 } + } + + #[test] + fn a_region_the_kernel_made_for_the_length_is_taken() { + assert_eq!(judge(&answer(SHM_PAGE, 1), SHM_PAGE), Ok(())); + assert_eq!(judge(&answer(2 * SHM_PAGE, 1), SHM_PAGE + 1), Ok(())); + } + + #[test] + fn a_region_that_is_not_ram_is_refused_whatever_its_length() { + assert_eq!(judge(&answer(SHM_PAGE, 0), SHM_PAGE), Err(Lent::NotRam)); + } + + #[test] + fn a_region_longer_than_the_protocol_says_is_refused() { + assert_eq!( + judge(&answer(2 * SHM_PAGE, 1), 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, 1), 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, 1), 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..e13f56fd5e8 100644 --- a/sdk/window/src/lib.rs +++ b/sdk/window/src/lib.rs @@ -14,7 +14,8 @@ 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}; +use toyos::ObjectKind; /// 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 +128,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 +159,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"), @@ -463,8 +466,8 @@ fn copy(bytes: &[u8]) -> Result<(), CreateError> { if header.msg_type != MSG_COPY_REGION || header.len() != 0 { 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 [region] = conn.recv_handles_exact([ObjectKind::SharedMem]).ok_or(CreateError::NoHandle)?; + 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)?) } @@ -549,8 +552,8 @@ impl Window { // The buffer crossed ahead of the frame. A compositor that announced a // 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 [buffer] = conn.recv_handles_exact([ObjectKind::SharedMem]).ok_or(CreateError::NoHandle)?; + let shm = SharedMemory::lent(buffer, buf_size).map_err(CreateError::Lent)?; let poller = Poller::new(1); Ok(Self { @@ -650,10 +653,10 @@ impl Window { // The old buffer is already gone on the compositor's side, so // a window that cannot map the new one has nothing to draw // into and its session is over. - let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { + let Some([buffer]) = self.conn.recv_handles_exact([ObjectKind::SharedMem]) 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; @@ -673,10 +676,10 @@ impl Window { let Ok(info) = self.conn.recv_payload::(header) else { return Event::Close; }; - let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { + let Some([buffer]) = self.conn.recv_handles_exact([ObjectKind::SharedMem]) 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/diskserver/src/main.rs b/system/diskserver/src/main.rs index 31e78b53685..b9d5d9c4388 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 { @@ -766,7 +758,7 @@ fn handshake(service: &mut Service, acceptor: &toyos::port::Acceptor, p: Pending return; } let guid = wire::guid(p.rx.payload(payload_len)); - let handles = p.conn.recv_handles_exact::<1>(); + let handles = p.conn.recv_handles_exact([toyos::ObjectKind::SharedMem]); let (Some(guid), Some([region]), wire::MSG_OPEN) = (guid, handles, msg_type) else { if let Some([region]) = handles { toyos_abi::syscall::close(region); 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..40e0913ba9f 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -56,9 +56,9 @@ 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::{ObjectKind, Pipe}; use toyos_abi::syscall::{SyscallError, DEV_PREFIX, MAX_BADGE, SERVE_PREFIX}; /// Clients served at once, machine-wide: one connection per directory per @@ -589,12 +589,14 @@ impl Server { let why = format!("it is refused, since its share is served {SERVED_SHARE} clients already"); return Answer::Refuse(SyscallError::ResourceExhausted, why); } - let Some([lent]) = client.conn.recv_handles_exact::<1>() else { + let Some([lent]) = client.conn.recv_handles_exact([ObjectKind::SharedMem]) 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::Map(_)) => 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/main.rs b/system/logkeeper/src/main.rs index d1bfcbdbb2f..58793d00bac 100644 --- a/system/logkeeper/src/main.rs +++ b/system/logkeeper/src/main.rs @@ -87,7 +87,7 @@ use toyos::poller::{Poller, READABLE, WRITABLE}; use toyos::port::Acceptor; use toyos::say; use toyos::syscap::SysCap; -use toyos::{Console, Pipe}; +use toyos::{Console, ObjectKind, Pipe}; use toyos_abi::syscall::SyscallError; use toyos_logstream::{ ProgramLine, Registration, Tag, CONSOLE, FLUSH, FLUSHED, LOG_OPENED, MAX_TAG, ORIGINS, REGISTER, RESUME, @@ -320,7 +320,7 @@ impl Log { panic!("logkeeper: the supervisor registered a ring under no name"); }; let name = registration.tag.as_str(); - let Some([ring, alive]) = self.from_supervisor.recv_handles_exact::<2>() else { + let Some([ring, alive]) = self.from_supervisor.recv_handles_exact([ObjectKind::SharedMem, ObjectKind::PipeRead]) else { panic!("logkeeper: the supervisor registered {name:?} with no ring"); }; // SAFETY: the kernel moved this handle into this table with 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/system/netstack/src/pipes.rs b/system/netstack/src/pipes.rs index 5aa1b4cc44a..f3dd2d9443c 100644 --- a/system/netstack/src/pipes.rs +++ b/system/netstack/src/pipes.rs @@ -7,11 +7,6 @@ //! keeps: the handle closes when the node drops its half, which is how a //! client reads the end of its stream or finds its writes refused, and from //! then the watched half names nothing. -//! -//! **Untrusted input.** An end is whatever handle its client moved, and -//! nothing checks its kind at intake: one that is no pipe end, or the wrong -//! end, answers a refusal here that is neither a full pipe nor a vanished -//! reader, and the node ends that client's stream or listener for it. use std::rc::{Rc, Weak}; diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index 7a95dbf023e..2db71327cd8 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -29,7 +29,7 @@ use std::time::Duration; use toyos::net::*; use toyos::poller::{OTHER_END_GONE, READABLE, WRITABLE, Poller}; -use toyos::{ipc, say, AsHandle, Pipe}; +use toyos::{ipc, say, AsHandle, ObjectKind, Pipe}; use toyos_abi::syscall::SyscallError; use toyos_inspect::Snapshot; use toyos_net_node::{ @@ -119,10 +119,14 @@ pub struct Sockets { readable: Vec, } +/// The ends a client moves for a stream or a datagram socket, in +/// [`DATA_TO_CLIENT`], [`DATA_FROM_CLIENT`] order: a reversed pair is refused. +const DATA_KINDS: [ObjectKind; DATA_HANDLES] = [ObjectKind::PipeWrite, ObjectKind::PipeRead]; + /// The two ends a connect or an accept moves, each split into the node's half /// and the watched one. fn data_pipes(client: &Client) -> Option<(Pipes, Watched, Watched)> { - let [to_client, from_client] = client.conn.recv_handles_exact::<{ DATA_HANDLES }>()?; + let [to_client, from_client] = client.conn.recv_handles_exact(DATA_KINDS)?; // SAFETY: the kernel moved both handles into this process with the frame // just read, and nothing else answers for either. let (to_client, from_client) = unsafe { (Pipe::from_raw(to_client), Pipe::from_raw(from_client)) }; @@ -414,7 +418,7 @@ impl Sockets { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some([wakes]) = msg.client.conn.recv_handles_exact::<{ NOTIFY_HANDLES }>() else { + let Some([wakes]) = msg.client.conn.recv_handles_exact([ObjectKind::PipeWrite]) else { msg.client.error(ERR_INVALID_INPUT); return; }; @@ -471,7 +475,7 @@ impl Sockets { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some([to_client, from_client]) = msg.client.conn.recv_handles_exact::<{ DATA_HANDLES }>() else { + let Some([to_client, from_client]) = msg.client.conn.recv_handles_exact(DATA_KINDS) else { msg.client.error(ERR_INVALID_INPUT); return; }; diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 3bcec5fa71b..5ae46a31bec 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -1754,7 +1754,8 @@ impl Supervisor<'_> { if msg_type != launch::MSG_LAUNCH { return; } - let mut batch = [toyos::RawHandle(0); toyos_abi::syscall::MAX_TRANSFER_HANDLES]; + let mut batch = [toyos_abi::handle::Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; + toyos_abi::syscall::MAX_TRANSFER_HANDLES]; let received = conn.recv_handles(&mut batch).unwrap_or(0); // **Owned on the statement after they arrive, and before anything can @@ -1762,7 +1763,7 @@ impl Supervisor<'_> { // here releases them — a launcher that leaked a handle per refused launch // would exhaust the one table the machine cannot do without, and a client // picks which refusal it takes. - let mut held = Moved(batch[..received].to_vec()); + let mut held = Moved(batch[..received].iter().map(|r| r.handle).collect()); let Some(request) = Request::decode(payload) else { return }; // `extra_names` drops an empty or non-UTF-8 name, so its count is what 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..fa2b145781f 100644 --- a/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs +++ b/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs @@ -147,8 +147,8 @@ fn invited() { let conn = toyos::endow::service(SERVICE).expect("the invited peer holds a connector"); // 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 [handle] = conn.recv_handles_exact([toyos::ObjectKind::SharedMem]).expect("the region the owner sent"); + 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/fs_stream_offset.rs b/tests/guest/rust-tests/src/bin/fs_stream_offset.rs index 6f6bfb5737c..9a8f204e627 100644 --- a/tests/guest/rust-tests/src/bin/fs_stream_offset.rs +++ b/tests/guest/rust-tests/src/bin/fs_stream_offset.rs @@ -48,7 +48,7 @@ fn main() { if reply.status == 0 { // What the server's end would have been: the pipe's bytes drained // at an offset that overflows. - if let Some([end]) = conn.recv_handles_exact::<1>() { + if let Some([end]) = conn.recv_handles_exact([toyos::ObjectKind::PipeWrite]) { let _ = toyos_abi::syscall::write(end, &[0x5A; 16]); toyos_abi::syscall::close(end); } diff --git a/tests/guest/rust-tests/src/bin/handle_lifetime.rs b/tests/guest/rust-tests/src/bin/handle_lifetime.rs index 3a672863e52..af383bb75ff 100644 --- a/tests/guest/rust-tests/src/bin/handle_lifetime.rs +++ b/tests/guest/rust-tests/src/bin/handle_lifetime.rs @@ -27,7 +27,7 @@ use std::os::toyos::process::CommandExt; use std::process::{Child, ChildStdout, Command, Stdio}; use toyos::census::Census; -use toyos::{namespace, port, AsHandle}; +use toyos::{namespace, port, AsHandle, ObjectKind}; use toyos_abi::inbox::RingLayout; use toyos_abi::syscall::{self, OpenFlags, SeekFrom, SyscallError, SERVE_PREFIX}; @@ -228,7 +228,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_window_refused.rs b/tests/guest/rust-tests/src/bin/lent_window_refused.rs new file mode 100644 index 00000000000..dd662873985 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/lent_window_refused.rs @@ -0,0 +1,70 @@ +//! A file server believes the kernel about the window a client lends it, never +//! the client: a hello whose window is no region, a region longer than +//! `WINDOW_BYTES`, or a device's registers is refused, and the server that +//! refused it answers the next client as the same process. +//! +//! The device is QEMU's virtio NIC, which no program of +//! `tests/images/testcases.toml` holds: its BAR is the one region a test can +//! lend that is not memory the kernel allocated. + +use toyos::endow::Endowments; +use toyos::fs::{Reply, Request, HELLO, REPLY, WINDOW_BYTES}; +use toyos::ipc::IpcError; +use toyos::shm::SharedMemory; +use toyos::syscap::SysCap; +use toyos::{AsHandle, OwnedHandle, PciDev}; +use toyos_abi::syscall::{self, PciId, SyscallError, SYSCAP_LABEL}; + +const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; + +/// 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. +fn hello(window: OwnedHandle) -> Result { + let names = toyos::endow::namespace().expect("lent_window_refused: a namespace"); + let conn = names.open("fs:/home").expect("lent_window_refused: fs:/home"); + conn.send_handles([window]).expect("lent_window_refused: the window into the connection"); + conn.send(HELLO, &Request::new()).expect("lent_window_refused: 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!("lent_window_refused: the hello's answer: {e:?}"), + }; + assert_eq!(header.msg_type, REPLY, "lent_window_refused: a hello answered by a frame that is no reply"); + let reply: Reply = conn.recv_payload(&header).expect("lent_window_refused: the reply's words"); + assert_eq!(reply.status, 0, "lent_window_refused: a hello refused with a word, not let go"); + Ok(reply.value2) +} + +fn region(bytes: usize) -> OwnedHandle { + SharedMemory::create(bytes).expect("lent_window_refused: a region").share().expect("lent_window_refused: its handle") +} + +fn main() { + let served = hello(region(WINDOW_BYTES)).expect("lent_window_refused: a window of WINDOW_BYTES is served"); + + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("lent_window_refused: a device-minting capability"); + let nic: PciDev = cap.claim_pci(NIC).expect("lent_window_refused: the virtio NIC's claim"); + let info = nic.describe().expect("lent_window_refused: the claim's description"); + let bar = info.bar_bytes.iter().position(|&bytes| bytes != 0).expect("lent_window_refused: a memory BAR"); + let bar = syscall::device_bar_map(nic.as_handle(), bar as u32).expect("lent_window_refused: the BAR's region"); + 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))), + // SAFETY: the kernel installed the handle for this call, and nothing else answers for it. + ("a device's BAR", hello(unsafe { OwnedHandle::from_raw(bar) })), + ]; + let again = hello(region(WINDOW_BYTES)).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 refused is not the one that answers"); + println!("lent_window_refused: the server that refused answers the next client, volume {served}"); +} diff --git a/tests/guest/rust-tests/src/bin/mutual_kill.rs b/tests/guest/rust-tests/src/bin/mutual_kill.rs index 60f609206ba..c6bbe288330 100644 --- a/tests/guest/rust-tests/src/bin/mutual_kill.rs +++ b/tests/guest/rust-tests/src/bin/mutual_kill.rs @@ -120,8 +120,10 @@ fn killer() -> ! { let conn = ns.open(SERVICE).expect("connect through the endowed connector"); 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 [word_handle, victim] = conn + .recv_handles_exact([toyos::ObjectKind::SharedMem, toyos::ObjectKind::Process]) + .expect("the word and the victim"); + 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/netstack_socket_churn.rs b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs index 0534d66d57a..a8e13db2051 100644 --- a/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs +++ b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs @@ -8,13 +8,12 @@ //! drops both pipe ends, and waits for netstack's own count of live piped //! connections to return. //! -//! argv[2] is the port of a second, which holds every connection it accepts -//! and reads nothing. On it, a client that moves netstack a handle that is no -//! pipe end where its receive end belongs, and keeps its send end: the kernel -//! refuses netstack's watch of it, and that refusal is all that ends the -//! connection. +//! A connect that moves netstack a handle that is no pipe end where its +//! receive end belongs is refused `InvalidInput` by the kind the kernel +//! answered for it, and netstack holds nothing for it. //! -//! On the same server, a client that shut its sending half down and left: +//! argv[2] is the port of a second, which holds every connection it accepts +//! and reads nothing. On it, a client that shut its sending half down and left: //! netstack let its send pipe go at the shutdown and its peer sends nothing, //! so nothing but the kernel's word on its receive pipe's other end can tell //! netstack it is gone. @@ -141,17 +140,16 @@ fn a_connect_past_the_places_is_refused(port: u16) { assert_eq!(count("net.places.held"), max, "every place is held where the connect was refused"); } -/// A connection the peer holds open, whose client keeps its send end and moved -/// netstack an acceptor for the end netstack writes the peer's bytes into. The -/// peer sends none, so netstack never writes it: its watch is refused, and the -/// connection is reset and let go. +/// A connect whose client moved netstack an acceptor for the end netstack +/// writes the peer's bytes into: refused before netstack holds a place or a +/// stream for it. fn a_receive_end_that_is_no_pipe_end_is_refused(port: u16) { - let live = count("net.piped.live"); + let (live, held) = (count("net.piped.live"), count("net.places.held")); let (acceptor, _connector) = toyos::port::create().expect("a port"); // SAFETY: the acceptor's one handle, which nothing else answers for. let no_pipe_end = unsafe { OwnedHandle::from_raw(acceptor.into_raw()) }; let (from_client, kept) = toyos::pipe_pair().expect("a pipe"); - let connected: TcpConnectResponse = NetstackConn::connect() + let connected = NetstackConn::connect() .and_then(|netstack| { netstack.request_with_handles( [no_pipe_end, OwnedHandle::from(from_client)], @@ -159,13 +157,14 @@ fn a_receive_end_that_is_no_pipe_end_is_refused(port: u16) { &TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: 0 }, ) }) - .and_then(|pending| pending.response()) - .unwrap_or_else(|e| panic!("the holding server, with an acceptor for a receive end: {e:?}")); - returns( - "net.piped.live", - live, - &format!("the kernel refused the watch of connection {}'s receive end, which is no pipe end", connected.socket_id), + .and_then(|pending| pending.response::()); + assert_eq!( + connected.map(|c| c.socket_id).err(), + Some(NetError::InvalidInput), + "a connect with an acceptor for its receive end" ); + assert_eq!(count("net.piped.live"), live, "netstack keeps a stream for a connect it refused"); + assert_eq!(count("net.places.held"), held, "netstack holds a place for a connect it refused"); drop(kept); } diff --git a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs index f67c2e683ec..785233ee320 100644 --- a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs +++ b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs @@ -85,7 +85,7 @@ fn test() { let (reader, writer) = pipe(""); client.send_handles([OwnedHandle::from(writer)]).expect("the write end into the connection's queue"); held(&reader, "its handle sits unreceived in a connection's queue"); - let [received] = server.recv_handles_exact::<1>().expect("the handle the queue held"); + let [received] = server.recv_handles_exact([toyos::ObjectKind::PipeWrite]).expect("the handle the queue held"); held(&reader, "its handle was received"); syscall::close(received); assert_eq!(at_last(&reader), Ok(OTHER_END_GONE), "the received handle closed"); diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index d2ab4685c15..9025523b91a 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -161,7 +161,7 @@ fn grow() -> Grown { let conn = acceptor.accept().expect("B connected back"); let header = conn.recv_header().expect("B's word that its subtree is grown"); assert_eq!(header.msg_type, MSG_GROWN, "B said something else"); - let [c, d, b_self, e] = conn.recv_handles_exact::<4>().expect("B sent four handles"); + let [c, d, b_self, e] = conn.recv_handles_exact([toyos::ObjectKind::Process; 4]).expect("B sent four handles"); // SAFETY: B moved each into this table with the frame, and nothing else // answers for them. let (c, d, e) = unsafe { (Process::from_raw(c), Process::from_raw(d), Process::from_raw(e)) }; 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..a38c482ce38 100644 --- a/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs +++ b/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs @@ -18,7 +18,7 @@ use std::process::{Command, Stdio}; use toyos::census::Census; use toyos::shm::SharedMemory; -use toyos::{namespace, port}; +use toyos::{namespace, port, ObjectKind}; use toyos_abi::syscall::SVC_LABEL; #[path = "../census_wait.rs"] @@ -56,7 +56,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}: \ @@ -84,7 +84,7 @@ fn main() { let conn = acceptor.accept().expect("the donor connects"); conn.recv_header().expect("the donor announces its region"); let [sent] = conn - .recv_handles_exact::<1>() + .recv_handles_exact([ObjectKind::SharedMem]) .expect("the donor sent the region ahead of the frame"); // Only now, after the donor has let go — its line says so. @@ -93,7 +93,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/bin/spawn_lands_claimed.rs b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs index fcb71ec8ee1..21ee12db088 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -51,7 +51,7 @@ fn test() { let conn = acceptor.accept().expect("the place connected back"); let header = conn.recv_header().expect("the place's word"); assert_eq!(header.msg_type, MSG_SELF, "the place said something else"); - let [place_self] = conn.recv_handles_exact::<1>().expect("the place sent its self"); + let [place_self] = conn.recv_handles_exact([toyos::ObjectKind::Process]).expect("the place sent its self"); assert!(place.try_wait().expect("ask after the place").is_none(), "the place ended before the marked spawn"); assert_eq!( diff --git a/tests/toyos.rs b/tests/toyos.rs index 36f92181a34..5d7daf27aa7 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_window_refused` runs it. + "lent_window_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_window_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_window_refused" => lent_window_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,36 @@ fn bar_map_again(test_config: &Path) -> Result<(), String> { Ok(()) } +/// fileserver refuses a hello whose window is a pipe end, a region longer than +/// `WINDOW_BYTES`, or the virtio NIC's BAR, each by the kernel's answer and by +/// name, and answers the next client as the process that refused them. +fn lent_window_refused(test_config: &Path) -> Result<(), String> { + const JOB: &str = "lent_window_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_window_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 hello carried no window", + ": its window is not WINDOW_BYTES long", + ": its window is not memory the kernel allocated", + "lent_window_refused: the server that refused 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. /// From af89f493a287f70e950608603b7e82bffc4232c4 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 09:18:36 +0200 Subject: [PATCH 2/4] A lent region is judged through SYS_FSTAT before it is mapped Review round 1 of #867, and the orchestrator's ruling on its NOTE 1: the kernel gains only what it alone can answer. SYS_FSTAT already answered a region's size, so SYS_SHM_MAP is back to answering an address and ShmMapping is gone. The RAM bit is a new FileType, DeviceMemory, for a region SharedMemObject::ram() says is not memory the kernel allocated to it; SharedMemory stays the type of one it did. SharedMemory::lent reads the fstat answer, judges it, and only then maps, so a refused region is never mapped. toyos::log's stdio ring already refused anything but SharedMemory, so its separate RAM check goes. The census answers by ObjectKind: kobject! maps each ABI kind to its counter by name, so the runtime name-equality assertion and ObjectKind::name() go, and the SDK's Census prints kinds with Debug. The free recv_handles_exact had one caller and is folded into the method. netstack's refused-watch arms are still reached: a pipe end of the right kind moved without Rights::WAIT passes the kind check and is refused the watch. netstack_socket_churn's case moves such a receive end, which is main's case with that handle in place of the acceptor the kind check now refuses at intake. The hostile-hello test is split by tier: lent_window_refused, a suite row, lends a pipe end and a region twice WINDOW_BYTES; lent_device_memory_refused, a machine test, lends the virtio NIC's BAR. Both share lent_hello.rs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- kernel/src/object/mod.rs | 11 +-- kernel/src/object/ops.rs | 3 +- kernel/src/syscall/dispatch.rs | 25 ++---- kernel/src/syscall/ipc.rs | 9 +-- sdk/abi/src/handle.rs | 25 +----- sdk/abi/src/syscall.rs | 55 ++++--------- sdk/toyos/src/census.rs | 10 +-- sdk/toyos/src/ipc.rs | 31 +++---- sdk/toyos/src/log/stdio.rs | 9 +-- sdk/toyos/src/shm.rs | 80 +++++++++---------- system/fileserver/src/main.rs | 2 +- system/netstack/src/serve.rs | 4 +- .../src/bin/lent_device_memory_refused.rs | 35 ++++++++ .../rust-tests/src/bin/lent_window_refused.rs | 62 +++----------- .../src/bin/netstack_socket_churn.rs | 54 +++++++------ tests/guest/rust-tests/src/lent_hello.rs | 36 +++++++++ tests/toyos.rs | 24 +++--- 17 files changed, 214 insertions(+), 261 deletions(-) create mode 100644 tests/guest/rust-tests/src/bin/lent_device_memory_refused.rs create mode 100644 tests/guest/rust-tests/src/lent_hello.rs diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index 09e2c366152..10067fe8838 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -220,12 +220,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),)+ + } } } }; @@ -247,7 +249,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/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index ce953fe1024..6fa07c137a8 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -516,12 +516,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> sys_handle_recv(RawHandle(a1 as u32), &mut out, a3 as usize) } SYS_SHM_CREATE => sys_shm_create(a1), - SYS_SHM_MAP => { - // Taken before the mapping exists: a bad address must not leave a mapping the caller was never told of. - let len = core::mem::size_of::() as u64; - let Some(mut out) = ctx.user_bytes_mut(UserAddr::new(a2), len) else { return bad_addr }; - sys_shm_map(RawHandle(a1 as u32), &mut out) - } + SYS_SHM_MAP => sys_shm_map(RawHandle(a1 as u32)), SYS_PORT_CREATE => sys_port_create(), SYS_PORT_MINT => { // Copied in before any handle is looked up: the kernel holds the badge, never the caller's page. @@ -613,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::handle::ObjectKind::ALL.iter().map(|kind| kind.name())), - "kobject! and toyos_abi::handle::ObjectKind 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/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index 77838a23a76..678796eda23 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -360,8 +360,7 @@ pub(super) fn sys_shm_create(size: u64) -> u64 { process::with_process_data(|data| handle_result(ops::install(&mut data.handles, object))) } -/// Map a region into the caller and answer where, how long, and whether it is RAM this kernel allocated: what a receiver needs to refuse a region a peer lent it. -pub(super) fn sys_shm_map(h: RawHandle, out: &mut crate::user_ptr::UserBytesMut) -> u64 { +pub(super) fn sys_shm_map(h: RawHandle) -> u64 { let shm = match process::with_process_data(|data| { data.handles.get::(h, Rights::MAP) }) { @@ -370,11 +369,7 @@ pub(super) fn sys_shm_map(h: RawHandle, out: &mut crate::user_ptr::UserBytesMut) }; let pt = process::current_address_space(); match shm.map_into(process::current_process(), &pt) { - Ok(addr) => { - let ram = u32::from(shm.ram().is_some()); - out.write_at(0, toyos_abi::usersafe::bytes(&ShmMapping { addr, len: shm.size(), ram, _pad: 0 })); - 0 - } + Ok(vaddr) => vaddr, Err(e) => e.to_u64(), } } diff --git a/sdk/abi/src/handle.rs b/sdk/abi/src/handle.rs index c6cf067a84f..234d8971c36 100644 --- a/sdk/abi/src/handle.rs +++ b/sdk/abi/src/handle.rs @@ -57,10 +57,9 @@ impl RawHandle { /// any table issues. pub const HANDLE_INVALID: RawHandle = RawHandle(u32::MAX); -/// Every kind of kernel object a handle can name, in the order the kernel's -/// `kobject!` declares them: the kernel answers a received handle's kind as -/// this word, and an index into [`ObjectKind::ALL`] is the one -/// [`CENSUS_KIND`](crate::syscall::debug_action::CENSUS_KIND) takes. +/// Every kind of kernel object a handle can name: the kernel answers a +/// received handle's kind as this word, and +/// [`CENSUS_KIND`](crate::syscall::debug_action::CENSUS_KIND) takes one. #[repr(u32)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum ObjectKind { @@ -99,24 +98,6 @@ impl ObjectKind { pub const fn from_word(word: u32) -> Option { if (word as usize) < Self::ALL.len() { Some(Self::ALL[word as usize]) } else { None } } - - pub const fn name(self) -> &'static str { - match self { - Self::PipeRead => "PipeRead", - Self::PipeWrite => "PipeWrite", - Self::Connection => "Connection", - Self::Device => "Device", - Self::Acceptor => "Acceptor", - Self::Inbox => "Inbox", - Self::SharedMem => "SharedMem", - Self::Connector => "Connector", - Self::Namespace => "Namespace", - Self::File => "File", - Self::Console => "Console", - Self::SysCap => "SysCap", - Self::Process => "Process", - } - } } // `from_word` indexes `ALL` by the word: a row out of order would answer a diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 398498491b8..10bf5d35a19 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -183,9 +183,8 @@ pub const SYS_HANDLE_RECV: u64 = 104; /// 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, and answer where, how long, and whether it -/// is memory the kernel allocated. Idempotent: a second call answers the -/// first call's address. See [`shm_map`]. +/// 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. @@ -559,13 +558,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 { @@ -583,6 +586,7 @@ impl FileType { 10 => Some(Self::SharedMemory), 11 => Some(Self::Directory), 12 => Some(Self::Symlink), + 13 => Some(Self::DeviceMemory), _ => None, } } @@ -820,7 +824,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 [`ObjectKind::ALL`](crate::handle::ObjectKind::ALL) 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. @@ -2008,38 +2012,7 @@ pub fn shm_create(size: usize) -> Result { check(syscall(SYS_SHM_CREATE, size as u64, 0, 0, 0)).map(|v| RawHandle(v as u32)) } -crate::user_safe! { - /// Where [`shm_map`] put a region, and what the region is. - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - pub struct ShmMapping { - pub addr: u64, - /// The mapping's length: the whole region. - pub len: u64, - /// 1 for memory the kernel allocated, as [`SYS_SHM_CREATE`] makes; 0 - /// for anything else, a device's registers or a scanout. - pub ram: u32, - pub _pad: u32, - } -} - -impl ShmMapping { - pub fn ptr(&self) -> *mut u8 { - core::ptr::with_exposed_provenance_mut(self.addr as usize) - } - - /// Whether the region is memory the kernel allocated: the only kind a - /// process may take from a peer as a buffer. - pub fn is_ram(&self) -> bool { - match self.ram { - 0 => false, - 1 => true, - other => panic!("shm_map answered memory word {other}, which is neither 0 nor 1"), - } - } -} - -/// Map the region `shm` names into this process, and answer where, how long, -/// and whether it is memory the kernel allocated. Needs [`Rights::MAP`]. +/// Map the region `shm` names into this process. Needs [`Rights::MAP`]. /// /// Idempotent: a second call answers the first call's address. [`SyscallError::Gone`] /// when the region's last handle was closed under the call: nothing is mapped. @@ -2048,9 +2021,9 @@ impl ShmMapping { /// /// # Safety /// Caller must manage the returned pointer. -pub unsafe fn shm_map(shm: RawHandle) -> Result { - let mut out = ShmMapping { addr: 0, len: 0, ram: 0, _pad: 0 }; - check_unit(syscall(SYS_SHM_MAP, shm.0 as u64, core::ptr::from_mut(&mut out) as u64, 0, 0)).map(|()| out) +pub unsafe fn shm_map(shm: RawHandle) -> Result<*mut u8, SyscallError> { + check(syscall(SYS_SHM_MAP, shm.0 as u64, 0, 0, 0)) + .map(|addr| core::ptr::with_exposed_provenance_mut(addr as usize)) } /// The machine's [header](SYSINFO_HEADER_SIZE), then as much of the process diff --git a/sdk/toyos/src/census.rs b/sdk/toyos/src/census.rs index e139afde7d0..8ee21c2576d 100644 --- a/sdk/toyos/src/census.rs +++ b/sdk/toyos/src/census.rs @@ -18,8 +18,8 @@ pub struct Census([u64; ObjectKind::ALL.len()]); impl Census { pub fn now() -> Self { let mut counts = [0u64; ObjectKind::ALL.len()]; - for (i, slot) in counts.iter_mut().enumerate() { - *slot = syscall::debug_with(CENSUS_KIND, i as u64); + for (slot, kind) in counts.iter_mut().zip(ObjectKind::ALL) { + *slot = syscall::debug_with(CENSUS_KIND, kind as u64); } Self(counts) } @@ -37,13 +37,13 @@ impl Census { pub fn grown_since( &self, before: &Census, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let earlier = before.0; ObjectKind::ALL .iter() .enumerate() .filter(move |(i, _)| self.0[*i] > earlier[*i]) - .map(move |(i, kind)| (kind.name(), earlier[i], self.0[i])) + .map(move |(i, kind)| (*kind, earlier[i], self.0[i])) } } @@ -54,7 +54,7 @@ impl fmt::Display for Census { if !first { write!(f, ", ")?; } - write!(f, "{} {live}", kind.name())?; + write!(f, "{kind:?} {live}")?; first = false; } if first { diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index 85d8d389906..b6df1241845 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -254,7 +254,17 @@ impl Connection { /// protocol here sends a fixed batch per message type, so this is the /// shape every caller wants. pub fn recv_handles_exact(&self, kinds: [ObjectKind; N]) -> Option<[RawHandle; N]> { - recv_handles_exact(self.0.raw(), kinds) + const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; + let mut batch = [Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; syscall::MAX_TRANSFER_HANDLES]; + let n = self.recv_handles(&mut batch).ok()?; + let batch = &batch[..n]; + if !batch.iter().map(Received::kind).eq(kinds) { + for received in batch { + syscall::close(received.handle); + } + return None; + } + Some(core::array::from_fn(|i| batch[i].handle)) } pub fn recv_bytes(&self, header: &IpcHeader, buf: &mut [u8]) -> Result { @@ -330,25 +340,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, kinds: [ObjectKind; N]) -> Option<[RawHandle; N]> { - const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; - let mut batch = [Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; syscall::MAX_TRANSFER_HANDLES]; - let n = syscall::handle_recv(handle, &mut batch).ok()?; - let batch = &batch[..n]; - if !batch.iter().map(Received::kind).eq(kinds) { - for received in batch { - syscall::close(received.handle); - } - return None; - } - Some(core::array::from_fn(|i| batch[i].handle)) -} - /// [`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/log/stdio.rs b/sdk/toyos/src/log/stdio.rs index 30368d573e3..b8f3df6ddf6 100644 --- a/sdk/toyos/src/log/stdio.rs +++ b/sdk/toyos/src/log/stdio.rs @@ -237,11 +237,8 @@ mod target { let own = syscall::dup(slot).map_err(Why::Dup)?; // SAFETY: `own` is a live shared-memory handle this process holds and // never closes, so the mapping stays for the process's life. - let mapping = unsafe { syscall::shm_map(own) }.map_err(Why::Map)?; - if !mapping.is_ram() { - return Err(Why::NotRam); - } - let at = NonNull::new(mapping.ptr()).ok_or(Why::Null)?; + let at = unsafe { syscall::shm_map(own) }.map_err(Why::Map)?; + let at = NonNull::new(at).ok_or(Why::Null)?; // SAFETY: a fresh mapping of a region at least `RING_BYTES` long. if !unsafe { Ring::at(at) }.is_laid_out() { return Err(Why::NotARing); @@ -262,7 +259,6 @@ mod target { Dup(SyscallError), Map(SyscallError), Null, - NotRam, NotARing, } @@ -278,7 +274,6 @@ mod target { Self::Dup(e) => write!(f, "holds a ring that will not duplicate: {e:?}"), Self::Map(e) => write!(f, "holds a ring that will not map: {e:?}"), Self::Null => write!(f, "holds a ring the kernel mapped at null"), - Self::NotRam => write!(f, "holds a region that is not memory the kernel allocated"), Self::NotARing => write!(f, "holds a region that is not a log ring"), } } diff --git a/sdk/toyos/src/shm.rs b/sdk/toyos/src/shm.rs index e94b231480b..96e0bfd7bd1 100644 --- a/sdk/toyos/src/shm.rs +++ b/sdk/toyos/src/shm.rs @@ -13,14 +13,15 @@ //! which believes the kernel's answer about it and not the peer's: a handle //! of another kind is refused before this is reached //! ([`Connection::recv_handles_exact`](crate::ipc::Connection::recv_handles_exact)), -//! and a region that is not memory the kernel allocated, or not the length -//! the protocol says, is refused here. [`SharedMemory::adopt`] is for a region -//! the kernel itself answered: a device's window or a scanout. +//! and a region the kernel's [`fstat`](syscall::fstat) answers is not memory +//! it allocated, or not the length the protocol says, is refused here before +//! it is mapped. [`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, ShmMapping, SyscallError, SHM_PAGE}; +use toyos_abi::syscall::{self, FileType, Stat, SyscallError, SHM_PAGE}; use crate::{AsHandle, OwnedHandle, RawHandle}; @@ -43,29 +44,23 @@ impl SharedMemory { Self::adopt(syscall::shm_create(size)?, size) } - /// Map a region the kernel answered — a device's window, a scanout, a - /// DMA grant — and view its first `size` bytes. + /// Map a region the kernel itself handed this process — a device's + /// window, a DMA grant — and view its first `size` bytes. /// - /// Never for a region a peer sent, which is [`Self::lent`]'s. The kernel - /// is no peer, so a mapping shorter than `size` is this caller's bug. + /// Never for a region a peer sent, which is [`Self::lent`]'s. pub fn adopt(handle: RawHandle, size: usize) -> Result { - let (handle, mapping) = map(handle)?; - assert!( - size <= mapping.len as usize, - "shm: a {size}-byte view of a region the kernel maps at {} bytes", - mapping.len as usize - ); - Ok(Self { handle, ptr: mapping.ptr(), size }) + let handle = OwnedHandle(handle); + 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 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. + /// 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, mapping) = map(handle).map_err(Lent::Map)?; - judge(&mapping, bytes)?; - Ok(Self { handle, ptr: mapping.ptr(), size: bytes }) + 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. @@ -104,21 +99,20 @@ impl SharedMemory { } } -fn map(handle: RawHandle) -> Result<(OwnedHandle, ShmMapping), SyscallError> { - let handle = OwnedHandle(handle); +fn map(handle: &OwnedHandle) -> Result<*mut u8, SyscallError> { // SAFETY: the mapping lives as long as the handle, which the caller keeps beside it. - let mapping = unsafe { syscall::shm_map(handle.raw()) }?; - assert!(!mapping.ptr().is_null(), "shm_map answered null"); - Ok((handle, mapping)) + 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 map it. - Map(SyscallError), - /// It is not memory the kernel allocated: a device's registers or a - /// scanout, which a reader would read and write as a device. + /// The kernel would not describe or map it. + Kernel(SyscallError), + /// It is not memory the kernel allocated to a region, such as 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. @@ -128,7 +122,7 @@ pub enum Lent { impl fmt::Display for Lent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Map(e) => write!(f, "the kernel would not map it ({e:?})"), + 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"), } @@ -136,12 +130,12 @@ impl fmt::Display for Lent { } /// The kernel's answer about a lent region, held to the protocol's length. -fn judge(mapping: &ShmMapping, bytes: usize) -> Result<(), Lent> { - if !mapping.is_ram() { +fn judge(stat: &Stat, bytes: usize) -> Result<(), Lent> { + if stat.file_type != FileType::SharedMemory { return Err(Lent::NotRam); } - if Some(mapping.len as usize) != bytes.checked_next_multiple_of(SHM_PAGE) { - return Err(Lent::Length { is: mapping.len as usize, bytes }); + if Some(stat.size as usize) != bytes.checked_next_multiple_of(SHM_PAGE) { + return Err(Lent::Length { is: stat.size as usize, bytes }); } Ok(()) } @@ -156,25 +150,25 @@ impl AsHandle for SharedMemory { mod tests { use super::*; - fn answer(len: usize, ram: u32) -> ShmMapping { - ShmMapping { addr: 1 << 30, len: len as u64, ram, _pad: 0 } + 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, 1), SHM_PAGE), Ok(())); - assert_eq!(judge(&answer(2 * SHM_PAGE, 1), SHM_PAGE + 1), Ok(())); + 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, 0), SHM_PAGE), Err(Lent::NotRam)); + 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, 1), SHM_PAGE), + judge(&answer(2 * SHM_PAGE, FileType::SharedMemory), SHM_PAGE), Err(Lent::Length { is: 2 * SHM_PAGE, bytes: SHM_PAGE }) ); } @@ -182,7 +176,7 @@ mod tests { #[test] fn a_region_shorter_than_the_protocol_says_is_refused() { assert_eq!( - judge(&answer(SHM_PAGE, 1), SHM_PAGE + 1), + judge(&answer(SHM_PAGE, FileType::SharedMemory), SHM_PAGE + 1), Err(Lent::Length { is: SHM_PAGE, bytes: SHM_PAGE + 1 }) ); } @@ -190,7 +184,7 @@ mod tests { #[test] fn a_length_whose_rounding_overflows_is_refused() { assert_eq!( - judge(&answer(SHM_PAGE, 1), usize::MAX), + judge(&answer(SHM_PAGE, FileType::SharedMemory), usize::MAX), Err(Lent::Length { is: SHM_PAGE, bytes: usize::MAX }) ); } diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index 40e0913ba9f..275a871cf5d 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -594,7 +594,7 @@ impl Server { }; match SharedMemory::lent(lent, WINDOW_BYTES) { Ok(window) => client.window = Some(window), - Err(Lent::Map(_)) => 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"), } diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index 2db71327cd8..2948b804856 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -774,7 +774,9 @@ impl Sockets { /// What the kernel answered one of [`Self::watch`]'s watches, or `false` /// for a token that is none of them. An answer about a socket that is /// gone since, or a pipe end the node let go, says nothing: closing an - /// end ends its watch, and that end is an answer too. + /// end ends its watch, and that end is an answer too. A refused watch + /// ends what it watched for: an end of the right kind that its client + /// moved without `Rights::WAIT` is refused it. pub fn answered(&mut self, node: &mut Node, now: Instant, token: u64, answer: Result) -> bool { let socket_id = token as u32; match token & TOKEN_KIND { 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 index dd662873985..387e80e96cd 100644 --- a/tests/guest/rust-tests/src/bin/lent_window_refused.rs +++ b/tests/guest/rust-tests/src/bin/lent_window_refused.rs @@ -1,62 +1,25 @@ -//! A file server believes the kernel about the window a client lends it, never -//! the client: a hello whose window is no region, a region longer than -//! `WINDOW_BYTES`, or a device's registers is refused, and the server that -//! refused it answers the next client as the same process. -//! -//! The device is QEMU's virtio NIC, which no program of -//! `tests/images/testcases.toml` holds: its BAR is the one region a test can -//! lend that is not memory the kernel allocated. +//! 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::endow::Endowments; -use toyos::fs::{Reply, Request, HELLO, REPLY, WINDOW_BYTES}; -use toyos::ipc::IpcError; -use toyos::shm::SharedMemory; -use toyos::syscap::SysCap; -use toyos::{AsHandle, OwnedHandle, PciDev}; -use toyos_abi::syscall::{self, PciId, SyscallError, SYSCAP_LABEL}; +use toyos::fs::WINDOW_BYTES; +use toyos_abi::syscall::SyscallError; -const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; - -/// 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. -fn hello(window: OwnedHandle) -> Result { - let names = toyos::endow::namespace().expect("lent_window_refused: a namespace"); - let conn = names.open("fs:/home").expect("lent_window_refused: fs:/home"); - conn.send_handles([window]).expect("lent_window_refused: the window into the connection"); - conn.send(HELLO, &Request::new()).expect("lent_window_refused: 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!("lent_window_refused: the hello's answer: {e:?}"), - }; - assert_eq!(header.msg_type, REPLY, "lent_window_refused: a hello answered by a frame that is no reply"); - let reply: Reply = conn.recv_payload(&header).expect("lent_window_refused: the reply's words"); - assert_eq!(reply.status, 0, "lent_window_refused: a hello refused with a word, not let go"); - Ok(reply.value2) -} - -fn region(bytes: usize) -> OwnedHandle { - SharedMemory::create(bytes).expect("lent_window_refused: a region").share().expect("lent_window_refused: its handle") -} +#[path = "../lent_hello.rs"] +mod lent_hello; +use lent_hello::{hello, region, window}; fn main() { - let served = hello(region(WINDOW_BYTES)).expect("lent_window_refused: a window of WINDOW_BYTES is served"); - - let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("lent_window_refused: a device-minting capability"); - let nic: PciDev = cap.claim_pci(NIC).expect("lent_window_refused: the virtio NIC's claim"); - let info = nic.describe().expect("lent_window_refused: the claim's description"); - let bar = info.bar_bytes.iter().position(|&bytes| bytes != 0).expect("lent_window_refused: a memory BAR"); - let bar = syscall::device_bar_map(nic.as_handle(), bar as u32).expect("lent_window_refused: the BAR's region"); + 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))), - // SAFETY: the kernel installed the handle for this call, and nothing else answers for it. - ("a device's BAR", hello(unsafe { OwnedHandle::from_raw(bar) })), ]; - let again = hello(region(WINDOW_BYTES)).expect("lent_window_refused: a window after the refusals is served"); + 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:?}"); @@ -65,6 +28,5 @@ fn main() { 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 refused is not the one that answers"); - println!("lent_window_refused: the server that refused answers the next client, volume {served}"); + 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/netstack_socket_churn.rs b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs index a8e13db2051..3b805582411 100644 --- a/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs +++ b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs @@ -8,12 +8,13 @@ //! drops both pipe ends, and waits for netstack's own count of live piped //! connections to return. //! -//! A connect that moves netstack a handle that is no pipe end where its -//! receive end belongs is refused `InvalidInput` by the kind the kernel -//! answered for it, and netstack holds nothing for it. -//! //! argv[2] is the port of a second, which holds every connection it accepts -//! and reads nothing. On it, a client that shut its sending half down and left: +//! and reads nothing. On it, a client that moves netstack its receive end +//! without the right to wait on it, and keeps its send end: the kernel +//! refuses netstack's watch of it, and that refusal is all that ends the +//! connection. +//! +//! On the same server, a client that shut its sending half down and left: //! netstack let its send pipe go at the shutdown and its peer sends nothing, //! so nothing but the kernel's word on its receive pipe's other end can tell //! netstack it is gone. @@ -38,7 +39,8 @@ use std::time::{Duration, Instant}; use toyos::net::{ MsgType, NetError, NetstackConn, TcpConnectPipedRequest, TcpConnectResponse, UdpRecvFromRequest, UdpRecvResponse, }; -use toyos::OwnedHandle; +use toyos::{AsHandle, OwnedHandle}; +use toyos_abi::handle::Rights; use toyos_inspect::{Value, NET}; /// The host, as QEMU's user network names it to a guest. @@ -140,31 +142,33 @@ fn a_connect_past_the_places_is_refused(port: u16) { assert_eq!(count("net.places.held"), max, "every place is held where the connect was refused"); } -/// A connect whose client moved netstack an acceptor for the end netstack -/// writes the peer's bytes into: refused before netstack holds a place or a -/// stream for it. -fn a_receive_end_that_is_no_pipe_end_is_refused(port: u16) { - let (live, held) = (count("net.piped.live"), count("net.places.held")); - let (acceptor, _connector) = toyos::port::create().expect("a port"); - // SAFETY: the acceptor's one handle, which nothing else answers for. - let no_pipe_end = unsafe { OwnedHandle::from_raw(acceptor.into_raw()) }; +/// A connection the peer holds open, whose client keeps its send end and moved +/// netstack the end it writes the peer's bytes into without `Rights::WAIT`. +/// The peer sends none, so netstack never writes it: its watch is refused, and +/// the connection is reset and let go. +fn a_receive_end_netstack_may_not_wait_on_is_reset(port: u16) { + let live = count("net.piped.live"); + let (_reads, writes) = toyos::pipe_pair().expect("a pipe"); + let unwaitable = toyos_abi::syscall::dup_narrowed(writes.as_handle(), Rights::TRANSFER.union(Rights::WRITE)) + .expect("the receive end without the right to wait on it"); + // SAFETY: the duplicate's one handle, which nothing else answers for. + let unwaitable = unsafe { OwnedHandle::from_raw(unwaitable) }; let (from_client, kept) = toyos::pipe_pair().expect("a pipe"); - let connected = NetstackConn::connect() + let connected: TcpConnectResponse = NetstackConn::connect() .and_then(|netstack| { netstack.request_with_handles( - [no_pipe_end, OwnedHandle::from(from_client)], + [unwaitable, OwnedHandle::from(from_client)], MsgType::TcpConnectPiped, &TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: 0 }, ) }) - .and_then(|pending| pending.response::()); - assert_eq!( - connected.map(|c| c.socket_id).err(), - Some(NetError::InvalidInput), - "a connect with an acceptor for its receive end" + .and_then(|pending| pending.response()) + .unwrap_or_else(|e| panic!("the holding server, with a receive end it may not wait on: {e:?}")); + returns( + "net.piped.live", + live, + &format!("the kernel refused the watch of connection {}'s receive end, moved without WAIT", connected.socket_id), ); - assert_eq!(count("net.piped.live"), live, "netstack keeps a stream for a connect it refused"); - assert_eq!(count("net.places.held"), held, "netstack holds a place for a connect it refused"); drop(kept); } @@ -194,8 +198,8 @@ fn main() { ); assert_eq!(left, streams, "netstack keeps a stream for a connection it let go"); returns("net.places.held", held, "the last of the connections it let go ended on the wire"); - a_receive_end_that_is_no_pipe_end_is_refused(holding); - println!("netstack_socket_churn: a connection whose receive end is no pipe end was reset"); + a_receive_end_netstack_may_not_wait_on_is_reset(holding); + println!("netstack_socket_churn: a connection whose receive end netstack may not wait on was reset"); left_after_a_shutdown(holding); println!("netstack_socket_churn: a client that shut down and left was let go"); owners_that_left_are_let_go(); 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 5d7daf27aa7..4bb3bc94a32 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -194,8 +194,8 @@ const RUST_SKIP: &[&str] = &[ // runs it. "bar_map_again", // It lends fileserver the virtio NIC's BAR, which the T14 has none of: - // `lent_window_refused` runs it. - "lent_window_refused", + // `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", @@ -415,7 +415,7 @@ const MACHINE_TESTS: &[&str] = &[ // 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_window_refused", + "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", @@ -3922,7 +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_window_refused" => lent_window_refused(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), @@ -4759,16 +4759,16 @@ fn bar_map_again(test_config: &Path) -> Result<(), String> { Ok(()) } -/// fileserver refuses a hello whose window is a pipe end, a region longer than -/// `WINDOW_BYTES`, or the virtio NIC's BAR, each by the kernel's answer and by -/// name, and answers the next client as the process that refused them. -fn lent_window_refused(test_config: &Path) -> Result<(), String> { - const JOB: &str = "lent_window_refused"; +/// 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_window_refused", Duration::from_secs(60)); + 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)); } @@ -4776,10 +4776,8 @@ fn lent_window_refused(test_config: &Path) -> Result<(), String> { return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); } for said in [ - ": its hello carried no window", - ": its window is not WINDOW_BYTES long", ": its window is not memory the kernel allocated", - "lent_window_refused: the server that refused answers the next client", + "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)); From c85fbc77fe812b2b30c0218c0ef3e649e7c04d2d Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 10:17:20 +0200 Subject: [PATCH 3/4] The kernel answers no handle's kind: fstat already judges a lent region Round 3 of #867, on review round 2's BLOCKER and the orchestrator's ruling that accepts the narrowed design. SYS_FSTAT resolves a handle of any kind and needs no right, and only a region of memory the kernel allocated answers SharedMemory. So SharedMemory::lent, which reads fstat before it maps, already refuses every handle that is no such region, a pipe end included, and the kind SYS_HANDLE_RECV answered had no reader fstat did not serve. That is why the m3 mutant, which made recv_handles_exact ignore the kinds, survived on the T14. Every pipe site uses its ends generically, where a wrong kind answers PermissionDenied or NotSupported and never WrongType. Deleted: KObjectRef::object_kind, the Received write and type (handle_recv takes &mut [RawHandle] again), the kinds in recv_handles and recv_handles_exact and every caller's kind list, DATA_KINDS, STREAM_OPENED_KINDS, and the supervisor's Received batch. netstack's pipes.rs paragraph, Sockets::answered's doc and netstack_socket_churn are main's again, because main's text is true again. recv_handles_exact stays folded into the method, count-only. ObjectKind stays, for the census alone. The kernel's one new answer is fstat's DeviceMemory. issues/a-received-handle-has-no-knowable-type.md is restored and not closed: a received Process, Namespace, Connector, Inbox or SysCap answers fstat Unknown and an Acceptor answers Pipe, so a typed use of one is still a WrongType the sender can cause. It is narrowed to that, with an exit. shm.rs's contract now says why lent judges before it maps: mapping a handle that names no region ends the caller. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- .../a-received-handle-has-no-knowable-type.md | 76 +++++++++++++++++++ kernel/src/object/mod.rs | 7 -- kernel/src/syscall/dispatch.rs | 2 +- kernel/src/syscall/ipc.rs | 8 +- lib/logkeeper-api/src/lib.rs | 2 +- sdk/abi/src/handle.rs | 30 +------- sdk/abi/src/syscall.rs | 10 +-- sdk/toyos/src/audio.rs | 4 +- sdk/toyos/src/fs.rs | 2 +- sdk/toyos/src/ipc.rs | 34 ++++----- sdk/toyos/src/launch.rs | 2 +- sdk/toyos/src/lib.rs | 1 - sdk/toyos/src/shm.rs | 16 ++-- sdk/window/src/lib.rs | 9 +-- system/diskserver/src/main.rs | 2 +- system/fileserver/src/main.rs | 4 +- system/logkeeper/src/main.rs | 4 +- system/netstack/src/pipes.rs | 5 ++ system/netstack/src/serve.rs | 16 ++-- system/supervisor/src/main.rs | 5 +- .../rust-tests/src/bin/abuse_shared_grant.rs | 2 +- .../rust-tests/src/bin/fs_stream_offset.rs | 2 +- .../rust-tests/src/bin/handle_lifetime.rs | 3 +- tests/guest/rust-tests/src/bin/mutual_kill.rs | 4 +- .../src/bin/netstack_socket_churn.rs | 33 ++++---- .../rust-tests/src/bin/pipe_other_end_gone.rs | 2 +- .../guest/rust-tests/src/bin/process_tree.rs | 2 +- .../src/bin/shm_release_reclaims.rs | 5 +- .../rust-tests/src/bin/spawn_lands_claimed.rs | 2 +- 29 files changed, 161 insertions(+), 133 deletions(-) create mode 100644 issues/a-received-handle-has-no-knowable-type.md diff --git a/issues/a-received-handle-has-no-knowable-type.md b/issues/a-received-handle-has-no-knowable-type.md new file mode 100644 index 00000000000..3decfeb0487 --- /dev/null +++ b/issues/a-received-handle-has-no-knowable-type.md @@ -0,0 +1,76 @@ +--- +status: open +kind: defect +opened: 2026-08-10 +--- + +# 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*. `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` 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 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 + +- `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 + instead of bare handles. One ABI shape change, in-tree only — no fork names + `handle_recv` (the whole fork estate was swept for this branch and + `handle_recv` is not in it). The receiver then refuses by + name and the fail-fast policy is untouched. +2. **A syscall** answering the kind of a handle. Cheaper to write and + worse: it is a second place to ask, and it makes "what is this" a round trip + rather than part of the answer that produced it. +3. **`WrongType` stops being fatal.** Rejected: it is fatal for a reason, and + three quarters of its call sites really are a bug in the caller. + +(1) is the recommendation. It is not this review's to take — it is an ABI +change, and a new syscall is the owner's to approve. + +## Ruled not a merge blocker, 2026-08-10 + +Judged while clearing PR #22's blockers, with the reasoning written down because +the next reader will ask why a class this wide was left open. + +- **The fix is an ABI shape change and the ABI was the owner's to approve.** It + widens `SYS_HANDLE_RECV`'s answer from `n` to `n` pairs, which is a syscall + the owner approved changing shape after the fact. + +So it stays open, and what it is waiting on is a decision rather than an +instrument. + +`launcher_refusals` is deleted; `issues/parallel-tests-red-under-other-suites.md` records the commit that restores it. diff --git a/kernel/src/object/mod.rs b/kernel/src/object/mod.rs index 10067fe8838..bb532bc8f4f 100644 --- a/kernel/src/object/mod.rs +++ b/kernel/src/object/mod.rs @@ -164,13 +164,6 @@ macro_rules! kobject { } } - /// This object's type, as `SYS_HANDLE_RECV` answers it. - pub fn object_kind(&self) -> toyos_abi::handle::ObjectKind { - match self { - $(Self::$variant(_) => toyos_abi::handle::ObjectKind::$variant,)+ - } - } - fn run_zero_handles(&self) { match self { $(Self::$variant(o) => o.on_zero_handles(),)+ diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 6fa07c137a8..dc119ec77ed 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -508,7 +508,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> return SyscallError::InvalidArgument.to_u64(); } let Some(mut out) = (a3 as usize) - .checked_mul(core::mem::size_of::()) + .checked_mul(HANDLE_LEN) .and_then(|len| ctx.user_bytes_mut(UserAddr::new(a2), len as u64)) else { return bad_addr; diff --git a/kernel/src/syscall/ipc.rs b/kernel/src/syscall/ipc.rs index 678796eda23..b16b2760c70 100644 --- a/kernel/src/syscall/ipc.rs +++ b/kernel/src/syscall/ipc.rs @@ -408,7 +408,7 @@ pub(super) fn sys_handle_send(conn_h: RawHandle, handles: &crate::user_ptr::User } } -/// Take the oldest batch the peer sent, each handle with its kind. Never blocks; zero means none queued. +/// Take the oldest batch the peer sent. Never blocks; zero means none queued. pub(super) fn sys_handle_recv( conn_h: RawHandle, out: &mut crate::user_ptr::UserBytesMut, @@ -430,10 +430,8 @@ pub(super) fn sys_handle_recv( let Some(batch) = conn.recv_bounded(cap)? else { return Ok(0) }; let count = batch.len(); for (i, entry) in batch.into_iter().enumerate() { - let kind = entry.object().object_kind() as u32; - let handle = data.handles.install(entry).expect("room was asked for first"); - let received = toyos_abi::handle::Received { handle, kind }; - out.write_at(i * size_of::(), toyos_abi::usersafe::bytes(&received)); + let h = data.handles.install(entry).expect("room was asked for first"); + out.write_at(i * HANDLE_LEN, &h.0.to_ne_bytes()); } Ok::<_, crate::object::Refusal>(count as u64) }); diff --git a/lib/logkeeper-api/src/lib.rs b/lib/logkeeper-api/src/lib.rs index 72ee41eecd7..dc32932d5fe 100644 --- a/lib/logkeeper-api/src/lib.rs +++ b/lib/logkeeper-api/src/lib.rs @@ -24,7 +24,7 @@ pub fn read() -> Result { } let boot_so_far: u64 = conn.recv_payload(&header).map_err(|e| format!("logkeeper's answer is short: {e:?}"))?; - let [raw] = conn.recv_handles_exact([toyos::ObjectKind::PipeRead]).ok_or("logkeeper's answer carried no pipe")?; + let [raw] = conn.recv_handles_exact::<1>().ok_or("logkeeper's answer carried no pipe")?; // SAFETY: the kernel moved this handle into this table with the frame // that names it, and nothing else answers for it. let pipe = unsafe { Pipe::from_raw(raw) }; diff --git a/sdk/abi/src/handle.rs b/sdk/abi/src/handle.rs index 234d8971c36..019f5ecd16e 100644 --- a/sdk/abi/src/handle.rs +++ b/sdk/abi/src/handle.rs @@ -57,9 +57,8 @@ impl RawHandle { /// any table issues. pub const HANDLE_INVALID: RawHandle = RawHandle(u32::MAX); -/// Every kind of kernel object a handle can name: the kernel answers a -/// received handle's kind as this word, and -/// [`CENSUS_KIND`](crate::syscall::debug_action::CENSUS_KIND) takes one. +/// 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 { @@ -100,8 +99,8 @@ impl ObjectKind { } } -// `from_word` indexes `ALL` by the word: a row out of order would answer a -// received handle's kind as its neighbour. +// `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() { @@ -110,27 +109,6 @@ const _: () = { } }; -crate::user_safe! { - /// One handle [`handle_recv`](crate::syscall::handle_recv) installed, and - /// what it names: a receiver refuses a handle of the wrong kind by name, - /// where using it typed would end the receiver. - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - pub struct Received { - pub handle: RawHandle, - /// An [`ObjectKind`] word; [`Received::kind`] decodes it. - pub kind: u32, - } -} - -impl Received { - /// The kernel writes only [`ObjectKind`] words here, so any other is a - /// kernel and SDK that disagree on the ABI. - pub fn kind(&self) -> ObjectKind { - ObjectKind::from_word(self.kind) - .unwrap_or_else(|| panic!("handle_recv answered object kind {}, which the ABI has no kind for", self.kind)) - } -} - /// 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 10bf5d35a19..bc77b9c0f27 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -177,8 +177,7 @@ pub const SYS_NAMESPACE_OPEN: u64 = 102; /// **handles are sent before the frame that announces them** and a receiver /// that has the frame already has the handles. pub const SYS_HANDLE_SEND: u64 = 103; -/// Take the oldest batch of handles the peer sent, each with its kind. See -/// [`handle_recv`]. +/// 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 [`SHM_PAGE`]s. /// See [`shm_create`]. @@ -438,7 +437,7 @@ pub const MAX_LABELS_LEN: usize = 4096; /// Bytes of label blob one spawn may carry: the kernel adds [`SELF_LABEL`]'s. pub const MAX_SPAWN_LABELS_LEN: usize = MAX_LABELS_LEN - SELF_LABEL.len(); -use crate::handle::{Received, Rights}; +use crate::handle::Rights; use crate::pci::{DmaGrant, DmaMapping}; use crate::{Pid, RawHandle, HANDLE_INVALID}; @@ -1985,13 +1984,12 @@ pub fn handle_send(conn: RawHandle, handles: &[RawHandle]) -> Result<(), Syscall )) } -/// Take the oldest batch the peer sent, into `out`, each handle with the kind -/// of object it names. Answers how many it wrote. +/// Take the oldest batch the peer sent, into `out`. Answers how many it wrote. /// /// Never blocks: zero means nothing is queued right now. A batch larger than /// `out` is `InvalidArgument` and stays queued — `out` should be /// [`MAX_TRANSFER_HANDLES`] long. -pub fn handle_recv(conn: RawHandle, out: &mut [Received]) -> Result { +pub fn handle_recv(conn: RawHandle, out: &mut [RawHandle]) -> Result { check(syscall( SYS_HANDLE_RECV, conn.0 as u64, diff --git a/sdk/toyos/src/audio.rs b/sdk/toyos/src/audio.rs index a8f251ef9ee..614652b0283 100644 --- a/sdk/toyos/src/audio.rs +++ b/sdk/toyos/src/audio.rs @@ -48,8 +48,6 @@ crate::ipc_payload! { pub const STREAM_OPENED_HANDLES: usize = 2; pub const STREAM_OPENED_SHM: usize = 0; pub const STREAM_OPENED_SIGNAL: usize = 1; -pub const STREAM_OPENED_KINDS: [crate::ObjectKind; STREAM_OPENED_HANDLES] = - [crate::ObjectKind::SharedMem, crate::ObjectKind::PipeRead]; pub struct AudioSlotWriter { shm: SharedMemory, @@ -213,7 +211,7 @@ impl AudioStream { }; let batch = control - .recv_handles_exact(STREAM_OPENED_KINDS) + .recv_handles_exact::() .ok_or(AudioError::MissingHandles)?; let signal = crate::Pipe(crate::OwnedHandle(batch[STREAM_OPENED_SIGNAL])); diff --git a/sdk/toyos/src/fs.rs b/sdk/toyos/src/fs.rs index 2805243e7d9..23028af0585 100644 --- a/sdk/toyos/src/fs.rs +++ b/sdk/toyos/src/fs.rs @@ -476,7 +476,7 @@ impl Dir { pub fn stream(&mut self, fid: u64, generation: u64, offset: u64) -> Result { self.fid_call(STREAM, generation, &Request { fid, offset, ..Request::new() })?; let conn = self.conn.as_ref().ok_or(SyscallError::Gone)?; - let [end] = conn.recv_handles_exact([crate::ObjectKind::PipeWrite]).ok_or(SyscallError::Unknown)?; + let [end] = conn.recv_handles_exact::<1>().ok_or(SyscallError::Unknown)?; Ok(Pipe(crate::OwnedHandle(end))) } diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index b6df1241845..01832e09886 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -3,7 +3,6 @@ //! Wire format: `[u32 msg_type][u32 len][len bytes payload]`. use toyos_abi::RawHandle; -use toyos_abi::handle::{ObjectKind, Received}; use toyos_abi::syscall::{self, SyscallError}; use crate::{AsHandle, OwnedHandle}; @@ -230,41 +229,36 @@ impl Connection { move_batch(handles, |batch| syscall::handle_send(self.0.raw(), batch)) } - /// Take the batch the peer sent with the frame just received, each handle - /// with the kind of object it names. + /// Take the batch the peer sent with the frame just received. /// /// Never blocks. An empty answer for a frame that promised handles is a /// peer that sent them out of order, which is a protocol error and not /// something to wait for. pub fn recv_handles( &self, - out: &mut [Received; syscall::MAX_TRANSFER_HANDLES], + out: &mut [RawHandle; syscall::MAX_TRANSFER_HANDLES], ) -> Result { syscall::handle_recv(self.0.raw(), out) } - /// The handles of the kinds this frame's message type says travel with - /// it, in that order. + /// The `N` handles this frame's message type says travel with it. /// - /// `None` for any other count or any other kind, with whatever did arrive - /// closed rather than leaked: a short batch is a peer that sent its frame - /// first, a long one or a handle of another kind is a protocol this - /// endpoint does not speak — and using a handle of the wrong kind typed - /// would end this process — and none is something to wait for. Every - /// protocol here sends a fixed batch per message type, so this is the - /// shape every caller wants. - pub fn recv_handles_exact(&self, kinds: [ObjectKind; N]) -> Option<[RawHandle; N]> { + /// `None` for any other count, with whatever did arrive closed rather than + /// leaked: a short batch is a peer that sent its frame first, a long one is + /// a protocol this endpoint does not speak, and neither is something to + /// 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]> { const { assert!(N <= syscall::MAX_TRANSFER_HANDLES) }; - let mut batch = [Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; syscall::MAX_TRANSFER_HANDLES]; + let mut batch = [toyos_abi::HANDLE_INVALID; syscall::MAX_TRANSFER_HANDLES]; let n = self.recv_handles(&mut batch).ok()?; - let batch = &batch[..n]; - if !batch.iter().map(Received::kind).eq(kinds) { - for received in batch { - syscall::close(received.handle); + if n != N { + for h in &batch[..n] { + syscall::close(*h); } return None; } - Some(core::array::from_fn(|i| batch[i].handle)) + Some(core::array::from_fn(|i| batch[i])) } pub fn recv_bytes(&self, header: &IpcHeader, buf: &mut [u8]) -> Result { diff --git a/sdk/toyos/src/launch.rs b/sdk/toyos/src/launch.rs index aa61f37148a..86d740daafc 100644 --- a/sdk/toyos/src/launch.rs +++ b/sdk/toyos/src/launch.rs @@ -373,7 +373,7 @@ pub fn launch<'a>( conn.send_bytes(MSG_LAUNCH, &buf[..len]).map_err(LaunchError::Sent)?; let header = conn.recv_header().map_err(LaunchError::Sent)?; match header.msg_type { - MSG_LAUNCHED => match conn.recv_handles_exact([crate::ObjectKind::Process]) { + MSG_LAUNCHED => match conn.recv_handles_exact::<1>() { Some([process]) => Ok(Outcome::Started(process)), None => Err(LaunchError::Sent(IpcError::Malformed)), }, diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 953f80d8460..1a72dfcdb64 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -40,7 +40,6 @@ pub use device::{ }; pub use toyos_abi::RawHandle; -pub use toyos_abi::handle::ObjectKind; pub use toyos_abi::ioport; /// Trait for types that wrap a kernel handle. diff --git a/sdk/toyos/src/shm.rs b/sdk/toyos/src/shm.rs index 96e0bfd7bd1..c408db6fb16 100644 --- a/sdk/toyos/src/shm.rs +++ b/sdk/toyos/src/shm.rs @@ -11,12 +11,11 @@ //! //! **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 -//! of another kind is refused before this is reached -//! ([`Connection::recv_handles_exact`](crate::ipc::Connection::recv_handles_exact)), -//! and a region the kernel's [`fstat`](syscall::fstat) answers is not memory -//! it allocated, or not the length the protocol says, is refused here before -//! it is mapped. [`SharedMemory::adopt`] is for a region the kernel itself -//! handed this process. +//! 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; @@ -111,8 +110,9 @@ fn map(handle: &OwnedHandle) -> Result<*mut u8, SyscallError> { pub enum Lent { /// The kernel would not describe or map it. Kernel(SyscallError), - /// It is not memory the kernel allocated to a region, such as a device's - /// registers, which a reader would read and write as a device. + /// 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. diff --git a/sdk/window/src/lib.rs b/sdk/window/src/lib.rs index e13f56fd5e8..8447d8bc717 100644 --- a/sdk/window/src/lib.rs +++ b/sdk/window/src/lib.rs @@ -15,7 +15,6 @@ use toyos::endow::{self, EndowError}; use toyos::surface; use toyos::Connection; use toyos::shm::{Lent, SharedMemory}; -use toyos::ObjectKind; /// 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. @@ -466,7 +465,7 @@ fn copy(bytes: &[u8]) -> Result<(), CreateError> { if header.msg_type != MSG_COPY_REGION || header.len() != 0 { return Err(CreateError::Protocol(header.msg_type)); } - let [region] = conn.recv_handles_exact([ObjectKind::SharedMem]).ok_or(CreateError::NoHandle)?; + let [region] = conn.recv_handles_exact::<1>().ok_or(CreateError::NoHandle)?; 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)?) @@ -552,7 +551,7 @@ impl Window { // The buffer crossed ahead of the frame. A compositor that announced a // window and sent nothing with it is not serving this client, whatever // else it is doing. - let [buffer] = conn.recv_handles_exact([ObjectKind::SharedMem]).ok_or(CreateError::NoHandle)?; + let [buffer] = conn.recv_handles_exact::<1>().ok_or(CreateError::NoHandle)?; let shm = SharedMemory::lent(buffer, buf_size).map_err(CreateError::Lent)?; let poller = Poller::new(1); @@ -653,7 +652,7 @@ impl Window { // The old buffer is already gone on the compositor's side, so // a window that cannot map the new one has nothing to draw // into and its session is over. - let Some([buffer]) = self.conn.recv_handles_exact([ObjectKind::SharedMem]) else { + let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { return Event::Close; }; let Ok(shm) = SharedMemory::lent(buffer, buf_size) else { @@ -676,7 +675,7 @@ impl Window { let Ok(info) = self.conn.recv_payload::(header) else { return Event::Close; }; - let Some([buffer]) = self.conn.recv_handles_exact([ObjectKind::SharedMem]) else { + let Some([buffer]) = self.conn.recv_handles_exact::<1>() else { return Event::ClipboardPaste(Vec::new()); }; let Ok(shm) = SharedMemory::lent(buffer, info.len as usize) else { diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index b9d5d9c4388..4978fc349ff 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -758,7 +758,7 @@ fn handshake(service: &mut Service, acceptor: &toyos::port::Acceptor, p: Pending return; } let guid = wire::guid(p.rx.payload(payload_len)); - let handles = p.conn.recv_handles_exact([toyos::ObjectKind::SharedMem]); + let handles = p.conn.recv_handles_exact::<1>(); let (Some(guid), Some([region]), wire::MSG_OPEN) = (guid, handles, msg_type) else { if let Some([region]) = handles { toyos_abi::syscall::close(region); diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index 275a871cf5d..e3dcaf14849 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -58,7 +58,7 @@ use toyos::poller::{Poller, READABLE}; use toyos::port::Acceptor; use toyos::shm::{Lent, SharedMemory}; use toyos::volatile::Window; -use toyos::{ObjectKind, Pipe}; +use toyos::Pipe; use toyos_abi::syscall::{SyscallError, DEV_PREFIX, MAX_BADGE, SERVE_PREFIX}; /// Clients served at once, machine-wide: one connection per directory per @@ -589,7 +589,7 @@ impl Server { let why = format!("it is refused, since its share is served {SERVED_SHARE} clients already"); return Answer::Refuse(SyscallError::ResourceExhausted, why); } - let Some([lent]) = client.conn.recv_handles_exact([ObjectKind::SharedMem]) else { + let Some([lent]) = client.conn.recv_handles_exact::<1>() else { return Answer::Drop("its hello carried no window"); }; match SharedMemory::lent(lent, WINDOW_BYTES) { diff --git a/system/logkeeper/src/main.rs b/system/logkeeper/src/main.rs index 58793d00bac..d1bfcbdbb2f 100644 --- a/system/logkeeper/src/main.rs +++ b/system/logkeeper/src/main.rs @@ -87,7 +87,7 @@ use toyos::poller::{Poller, READABLE, WRITABLE}; use toyos::port::Acceptor; use toyos::say; use toyos::syscap::SysCap; -use toyos::{Console, ObjectKind, Pipe}; +use toyos::{Console, Pipe}; use toyos_abi::syscall::SyscallError; use toyos_logstream::{ ProgramLine, Registration, Tag, CONSOLE, FLUSH, FLUSHED, LOG_OPENED, MAX_TAG, ORIGINS, REGISTER, RESUME, @@ -320,7 +320,7 @@ impl Log { panic!("logkeeper: the supervisor registered a ring under no name"); }; let name = registration.tag.as_str(); - let Some([ring, alive]) = self.from_supervisor.recv_handles_exact([ObjectKind::SharedMem, ObjectKind::PipeRead]) else { + let Some([ring, alive]) = self.from_supervisor.recv_handles_exact::<2>() else { panic!("logkeeper: the supervisor registered {name:?} with no ring"); }; // SAFETY: the kernel moved this handle into this table with diff --git a/system/netstack/src/pipes.rs b/system/netstack/src/pipes.rs index f3dd2d9443c..5aa1b4cc44a 100644 --- a/system/netstack/src/pipes.rs +++ b/system/netstack/src/pipes.rs @@ -7,6 +7,11 @@ //! keeps: the handle closes when the node drops its half, which is how a //! client reads the end of its stream or finds its writes refused, and from //! then the watched half names nothing. +//! +//! **Untrusted input.** An end is whatever handle its client moved, and +//! nothing checks its kind at intake: one that is no pipe end, or the wrong +//! end, answers a refusal here that is neither a full pipe nor a vanished +//! reader, and the node ends that client's stream or listener for it. use std::rc::{Rc, Weak}; diff --git a/system/netstack/src/serve.rs b/system/netstack/src/serve.rs index 2948b804856..7a95dbf023e 100644 --- a/system/netstack/src/serve.rs +++ b/system/netstack/src/serve.rs @@ -29,7 +29,7 @@ use std::time::Duration; use toyos::net::*; use toyos::poller::{OTHER_END_GONE, READABLE, WRITABLE, Poller}; -use toyos::{ipc, say, AsHandle, ObjectKind, Pipe}; +use toyos::{ipc, say, AsHandle, Pipe}; use toyos_abi::syscall::SyscallError; use toyos_inspect::Snapshot; use toyos_net_node::{ @@ -119,14 +119,10 @@ pub struct Sockets { readable: Vec, } -/// The ends a client moves for a stream or a datagram socket, in -/// [`DATA_TO_CLIENT`], [`DATA_FROM_CLIENT`] order: a reversed pair is refused. -const DATA_KINDS: [ObjectKind; DATA_HANDLES] = [ObjectKind::PipeWrite, ObjectKind::PipeRead]; - /// The two ends a connect or an accept moves, each split into the node's half /// and the watched one. fn data_pipes(client: &Client) -> Option<(Pipes, Watched, Watched)> { - let [to_client, from_client] = client.conn.recv_handles_exact(DATA_KINDS)?; + let [to_client, from_client] = client.conn.recv_handles_exact::<{ DATA_HANDLES }>()?; // SAFETY: the kernel moved both handles into this process with the frame // just read, and nothing else answers for either. let (to_client, from_client) = unsafe { (Pipe::from_raw(to_client), Pipe::from_raw(from_client)) }; @@ -418,7 +414,7 @@ impl Sockets { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some([wakes]) = msg.client.conn.recv_handles_exact([ObjectKind::PipeWrite]) else { + let Some([wakes]) = msg.client.conn.recv_handles_exact::<{ NOTIFY_HANDLES }>() else { msg.client.error(ERR_INVALID_INPUT); return; }; @@ -475,7 +471,7 @@ impl Sockets { msg.client.error(ERR_INVALID_INPUT); return; }; - let Some([to_client, from_client]) = msg.client.conn.recv_handles_exact(DATA_KINDS) else { + let Some([to_client, from_client]) = msg.client.conn.recv_handles_exact::<{ DATA_HANDLES }>() else { msg.client.error(ERR_INVALID_INPUT); return; }; @@ -774,9 +770,7 @@ impl Sockets { /// What the kernel answered one of [`Self::watch`]'s watches, or `false` /// for a token that is none of them. An answer about a socket that is /// gone since, or a pipe end the node let go, says nothing: closing an - /// end ends its watch, and that end is an answer too. A refused watch - /// ends what it watched for: an end of the right kind that its client - /// moved without `Rights::WAIT` is refused it. + /// end ends its watch, and that end is an answer too. pub fn answered(&mut self, node: &mut Node, now: Instant, token: u64, answer: Result) -> bool { let socket_id = token as u32; match token & TOKEN_KIND { diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 5ae46a31bec..3bcec5fa71b 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -1754,8 +1754,7 @@ impl Supervisor<'_> { if msg_type != launch::MSG_LAUNCH { return; } - let mut batch = [toyos_abi::handle::Received { handle: toyos_abi::HANDLE_INVALID, kind: 0 }; - toyos_abi::syscall::MAX_TRANSFER_HANDLES]; + let mut batch = [toyos::RawHandle(0); toyos_abi::syscall::MAX_TRANSFER_HANDLES]; let received = conn.recv_handles(&mut batch).unwrap_or(0); // **Owned on the statement after they arrive, and before anything can @@ -1763,7 +1762,7 @@ impl Supervisor<'_> { // here releases them — a launcher that leaked a handle per refused launch // would exhaust the one table the machine cannot do without, and a client // picks which refusal it takes. - let mut held = Moved(batch[..received].iter().map(|r| r.handle).collect()); + let mut held = Moved(batch[..received].to_vec()); let Some(request) = Request::decode(payload) else { return }; // `extra_names` drops an empty or non-UTF-8 name, so its count is what 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 fa2b145781f..0456d92f61f 100644 --- a/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs +++ b/tests/guest/rust-tests/src/bin/abuse_shared_grant.rs @@ -147,7 +147,7 @@ fn invited() { let conn = toyos::endow::service(SERVICE).expect("the invited peer holds a connector"); // 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([toyos::ObjectKind::SharedMem]).expect("the region the owner sent"); + let [handle] = conn.recv_handles_exact::<1>().expect("the region the owner sent"); 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/fs_stream_offset.rs b/tests/guest/rust-tests/src/bin/fs_stream_offset.rs index 9a8f204e627..6f6bfb5737c 100644 --- a/tests/guest/rust-tests/src/bin/fs_stream_offset.rs +++ b/tests/guest/rust-tests/src/bin/fs_stream_offset.rs @@ -48,7 +48,7 @@ fn main() { if reply.status == 0 { // What the server's end would have been: the pipe's bytes drained // at an offset that overflows. - if let Some([end]) = conn.recv_handles_exact([toyos::ObjectKind::PipeWrite]) { + if let Some([end]) = conn.recv_handles_exact::<1>() { let _ = toyos_abi::syscall::write(end, &[0x5A; 16]); toyos_abi::syscall::close(end); } diff --git a/tests/guest/rust-tests/src/bin/handle_lifetime.rs b/tests/guest/rust-tests/src/bin/handle_lifetime.rs index af383bb75ff..a87580dfa70 100644 --- a/tests/guest/rust-tests/src/bin/handle_lifetime.rs +++ b/tests/guest/rust-tests/src/bin/handle_lifetime.rs @@ -27,7 +27,8 @@ use std::os::toyos::process::CommandExt; use std::process::{Child, ChildStdout, Command, Stdio}; use toyos::census::Census; -use toyos::{namespace, port, AsHandle, ObjectKind}; +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}; diff --git a/tests/guest/rust-tests/src/bin/mutual_kill.rs b/tests/guest/rust-tests/src/bin/mutual_kill.rs index c6bbe288330..4a8d52477be 100644 --- a/tests/guest/rust-tests/src/bin/mutual_kill.rs +++ b/tests/guest/rust-tests/src/bin/mutual_kill.rs @@ -120,9 +120,7 @@ fn killer() -> ! { let conn = ns.open(SERVICE).expect("connect through the endowed connector"); 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([toyos::ObjectKind::SharedMem, toyos::ObjectKind::Process]) - .expect("the word and the victim"); + let [word_handle, victim] = conn.recv_handles_exact::<2>().expect("the word and the victim"); 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/netstack_socket_churn.rs b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs index 3b805582411..0534d66d57a 100644 --- a/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs +++ b/tests/guest/rust-tests/src/bin/netstack_socket_churn.rs @@ -9,8 +9,8 @@ //! connections to return. //! //! argv[2] is the port of a second, which holds every connection it accepts -//! and reads nothing. On it, a client that moves netstack its receive end -//! without the right to wait on it, and keeps its send end: the kernel +//! and reads nothing. On it, a client that moves netstack a handle that is no +//! pipe end where its receive end belongs, and keeps its send end: the kernel //! refuses netstack's watch of it, and that refusal is all that ends the //! connection. //! @@ -39,8 +39,7 @@ use std::time::{Duration, Instant}; use toyos::net::{ MsgType, NetError, NetstackConn, TcpConnectPipedRequest, TcpConnectResponse, UdpRecvFromRequest, UdpRecvResponse, }; -use toyos::{AsHandle, OwnedHandle}; -use toyos_abi::handle::Rights; +use toyos::OwnedHandle; use toyos_inspect::{Value, NET}; /// The host, as QEMU's user network names it to a guest. @@ -143,31 +142,29 @@ fn a_connect_past_the_places_is_refused(port: u16) { } /// A connection the peer holds open, whose client keeps its send end and moved -/// netstack the end it writes the peer's bytes into without `Rights::WAIT`. -/// The peer sends none, so netstack never writes it: its watch is refused, and -/// the connection is reset and let go. -fn a_receive_end_netstack_may_not_wait_on_is_reset(port: u16) { +/// netstack an acceptor for the end netstack writes the peer's bytes into. The +/// peer sends none, so netstack never writes it: its watch is refused, and the +/// connection is reset and let go. +fn a_receive_end_that_is_no_pipe_end_is_refused(port: u16) { let live = count("net.piped.live"); - let (_reads, writes) = toyos::pipe_pair().expect("a pipe"); - let unwaitable = toyos_abi::syscall::dup_narrowed(writes.as_handle(), Rights::TRANSFER.union(Rights::WRITE)) - .expect("the receive end without the right to wait on it"); - // SAFETY: the duplicate's one handle, which nothing else answers for. - let unwaitable = unsafe { OwnedHandle::from_raw(unwaitable) }; + let (acceptor, _connector) = toyos::port::create().expect("a port"); + // SAFETY: the acceptor's one handle, which nothing else answers for. + let no_pipe_end = unsafe { OwnedHandle::from_raw(acceptor.into_raw()) }; let (from_client, kept) = toyos::pipe_pair().expect("a pipe"); let connected: TcpConnectResponse = NetstackConn::connect() .and_then(|netstack| { netstack.request_with_handles( - [unwaitable, OwnedHandle::from(from_client)], + [no_pipe_end, OwnedHandle::from(from_client)], MsgType::TcpConnectPiped, &TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: 0 }, ) }) .and_then(|pending| pending.response()) - .unwrap_or_else(|e| panic!("the holding server, with a receive end it may not wait on: {e:?}")); + .unwrap_or_else(|e| panic!("the holding server, with an acceptor for a receive end: {e:?}")); returns( "net.piped.live", live, - &format!("the kernel refused the watch of connection {}'s receive end, moved without WAIT", connected.socket_id), + &format!("the kernel refused the watch of connection {}'s receive end, which is no pipe end", connected.socket_id), ); drop(kept); } @@ -198,8 +195,8 @@ fn main() { ); assert_eq!(left, streams, "netstack keeps a stream for a connection it let go"); returns("net.places.held", held, "the last of the connections it let go ended on the wire"); - a_receive_end_netstack_may_not_wait_on_is_reset(holding); - println!("netstack_socket_churn: a connection whose receive end netstack may not wait on was reset"); + a_receive_end_that_is_no_pipe_end_is_refused(holding); + println!("netstack_socket_churn: a connection whose receive end is no pipe end was reset"); left_after_a_shutdown(holding); println!("netstack_socket_churn: a client that shut down and left was let go"); owners_that_left_are_let_go(); diff --git a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs index 785233ee320..f67c2e683ec 100644 --- a/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs +++ b/tests/guest/rust-tests/src/bin/pipe_other_end_gone.rs @@ -85,7 +85,7 @@ fn test() { let (reader, writer) = pipe(""); client.send_handles([OwnedHandle::from(writer)]).expect("the write end into the connection's queue"); held(&reader, "its handle sits unreceived in a connection's queue"); - let [received] = server.recv_handles_exact([toyos::ObjectKind::PipeWrite]).expect("the handle the queue held"); + let [received] = server.recv_handles_exact::<1>().expect("the handle the queue held"); held(&reader, "its handle was received"); syscall::close(received); assert_eq!(at_last(&reader), Ok(OTHER_END_GONE), "the received handle closed"); diff --git a/tests/guest/rust-tests/src/bin/process_tree.rs b/tests/guest/rust-tests/src/bin/process_tree.rs index 9025523b91a..d2ab4685c15 100644 --- a/tests/guest/rust-tests/src/bin/process_tree.rs +++ b/tests/guest/rust-tests/src/bin/process_tree.rs @@ -161,7 +161,7 @@ fn grow() -> Grown { let conn = acceptor.accept().expect("B connected back"); let header = conn.recv_header().expect("B's word that its subtree is grown"); assert_eq!(header.msg_type, MSG_GROWN, "B said something else"); - let [c, d, b_self, e] = conn.recv_handles_exact([toyos::ObjectKind::Process; 4]).expect("B sent four handles"); + let [c, d, b_self, e] = conn.recv_handles_exact::<4>().expect("B sent four handles"); // SAFETY: B moved each into this table with the frame, and nothing else // answers for them. let (c, d, e) = unsafe { (Process::from_raw(c), Process::from_raw(d), Process::from_raw(e)) }; 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 a38c482ce38..11fd6b3b3c6 100644 --- a/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs +++ b/tests/guest/rust-tests/src/bin/shm_release_reclaims.rs @@ -18,7 +18,8 @@ use std::process::{Command, Stdio}; use toyos::census::Census; use toyos::shm::SharedMemory; -use toyos::{namespace, port, ObjectKind}; +use toyos::{namespace, port}; +use toyos_abi::handle::ObjectKind; use toyos_abi::syscall::SVC_LABEL; #[path = "../census_wait.rs"] @@ -84,7 +85,7 @@ fn main() { let conn = acceptor.accept().expect("the donor connects"); conn.recv_header().expect("the donor announces its region"); let [sent] = conn - .recv_handles_exact([ObjectKind::SharedMem]) + .recv_handles_exact::<1>() .expect("the donor sent the region ahead of the frame"); // Only now, after the donor has let go — its line says so. diff --git a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs index 21ee12db088..fcb71ec8ee1 100644 --- a/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs +++ b/tests/guest/rust-tests/src/bin/spawn_lands_claimed.rs @@ -51,7 +51,7 @@ fn test() { let conn = acceptor.accept().expect("the place connected back"); let header = conn.recv_header().expect("the place's word"); assert_eq!(header.msg_type, MSG_SELF, "the place said something else"); - let [place_self] = conn.recv_handles_exact([toyos::ObjectKind::Process]).expect("the place sent its self"); + let [place_self] = conn.recv_handles_exact::<1>().expect("the place sent its self"); assert!(place.try_wait().expect("ask after the place").is_none(), "the place ended before the marked spawn"); assert_eq!( From f875327288d767d2abe1784ad8c5bf9e3cd96903 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 12:19:13 +0200 Subject: [PATCH 4/4] The compositor's window budget reads SHM_PAGE, not a private PAGE_2M `toyos-desktop`'s `budget.rs` kept its own `PAGE_2M` for what the kernel rounds a shared region up to. That fact is now declared once in the ABI as `SHM_PAGE`, which the kernel const-asserts equals its page size, so the private copy is deleted and `window_bytes` and its test read the ABI's. Review round 3's BLOCKER on #867. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/compositor/desktop/src/budget.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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]