Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions issues/a-file-server-maps-a-lent-window-at-the-size-it-expects.md

This file was deleted.

51 changes: 26 additions & 25 deletions issues/a-received-handle-has-no-knowable-type.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,47 @@ kind: defect
opened: 2026-08-10
---

# A process cannot ask what it received, and using it wrongly is fatal
# A process cannot ask what most handles it received are, and using one wrongly is fatal

`SYS_HANDLE_RECV` installs a peer's handles in the receiver's table and answers
only *how many*. There is no syscall that says what any of them names, and
`KObjectRef`'s kind is not reported anywhere.
only *how many*. `SYS_FSTAT` answers what any handle is, needing no right, and
settles two kinds: only a region of memory the kernel allocated answers
`SharedMemory`, which is how `SharedMemory::lent` refuses every other handle
before it maps one (`sdk/toyos/src/shm.rs`), and only a `Connection` answers
`Socket`. It settles no other. A `Process`, a `Namespace`, a `Connector`, an
`Inbox` and a `SysCap` all answer `Unknown`, and an `Acceptor` answers `Pipe`,
as a pipe end does (`fstat` in `kernel/src/object/ops.rs`).

Every typed use of a handle resolves through `HandleTable::get::<T>`, and
`HandleError::WrongType` is one of the three kinds `HandleError::refuse` treats
as a bug in the caller: it ends the process, exit 139
(`kernel/src/object/handle.rs`). The doc argues that *"asking a pipe to accept a
connection is not something a correct program can do"* — which is true for a
handle the process made or was endowed, and **false for one a peer sent**. A
`HandleError::WrongType` ends the caller, exit 139, everywhere but
`HandleTable::get_sent` (`kernel/src/object/handle.rs`). That is right for a
handle the process made or was endowed, and wrong for one a peer sent: a
correct program cannot know.

So: **any process that receives a handle over a connection and then uses it
typed can be ended by whoever sent it.** The sender needs nothing but
So: **any process that receives one of those kinds over a connection and then
uses it typed can be ended by whoever sent it.** The sender needs nothing but
`Rights::TRANSFER`, which every handle it made carries.

## Where it is reachable today

- `/system/bin/init`'s launcher takes `extra` connectors from a client and hands them
to `SYS_NAMESPACE_BUILD`. That one is closed by making that call's `add` arm
answer `InvalidArgument` rather than fault, and `launcher_refusals` gates it —
but that is one call site, not the class.
- An audio client receives two handles from soundd and calls `SYS_SHM_MAP` on
the first (`sdk/toyos/src/audio.rs`). A hostile *server* ends every client.
- A window client receives its buffer the same way (`sdk/window/src/lib.rs`).
- blockd maps the region a client sends with its open (`Region::adopt` in
`userland/blockd/src/region.rs`), so a *client* holding its connector ends it.
- netstack adopts as pipes the two handles a client sends with a piped socket
(`data_pipes`) and the one with a piped bind (`Sockets::listen`), both in
`system/netstack/src/serve.rs`: a handle that is no pipe end answers a
refusal the node ends that client's stream or listener for
(`system/netstack/src/pipes.rs`), and a datagram socket's are netstack's
own to read and write.
- `toyos::launch` answers a started launch with the `Process` handle the
supervisor sent (`sdk/toyos/src/launch.rs`), and its caller uses it typed.

A generic use is not reached: a read or a write a handle cannot do answers
`PermissionDenied`, and a watch `NotSupported`, never `WrongType`, which is how
netstack's piped ends are refused (`system/netstack/src/pipes.rs`). The
supervisor hands what a launch's client sent to `SYS_SPAWN` and
`SYS_NAMESPACE_BUILD`, which answer a handle of the wrong kind
`InvalidArgument`.

Nothing in the tree is hostile today, so nothing fails. The property the
architecture claims — that a process cannot be harmed by what it was not given —
does not hold across a transfer.

**Exit**: a process that receives a handle of one of those kinds refuses one of
another kind by name before it uses it typed, and a guest test that sends one
in its place sees it refused while the receiver lives.

## Three ways out, and none is free

1. **Report the kind.** `SYS_HANDLE_RECV` writes `(RawHandle, kind)` pairs
Expand Down
11 changes: 6 additions & 5 deletions kernel/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,14 @@ macro_rules! kobject {
pub(super) static $variant: AtomicU64 = AtomicU64::new(0);
)+

/// `(type name, live count)` per kind, in declaration order — a
/// summed total would hide one kind's leak in another's churn.
/// Live objects of one kind — a summed total would hide one
/// kind's leak in another's churn.
#[cfg(feature = "test-actuators")]
pub fn live() -> impl Iterator<Item = (&'static str, u64)> {
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),)+
}
}
}
};
Expand All @@ -240,7 +242,6 @@ kobject! {
deferred Connection => service::ConnectionEnd,
deferred Device => device::DeviceClaim,
deferred Acceptor => port::Acceptor,
// Kind name is the ABI's; `CENSUS_KIND` asserts the two lists agree.
deferred Inbox => inbox::InboxObject,
// The flush that makes releasing it safe waits for every CPU, so it runs from the queue, never inline.
deferred SharedMem => shm::SharedMemObject,
Expand Down
3 changes: 2 additions & 1 deletion kernel/src/object/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions kernel/src/object/shm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 4 additions & 14 deletions kernel/src/syscall/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,20 +608,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) ->
// The two leaks this object model accepts — a stranded Arc, the cross-pair
// connection cycle — are visible in no other way than this census.
// Per kind, not total: a total would hide one kind's leak behind another's churn.
// Names are checked against OBJECT_KINDS rather than order assumed: this is
// the one place the two declarations meet.
DA::CENSUS_KIND => {
assert!(
crate::object::census::live()
.map(|(kind, _)| kind)
.eq(toyos_abi::syscall::OBJECT_KINDS.iter().copied()),
"kobject! and toyos_abi::syscall::OBJECT_KINDS declare different objects",
);
match crate::object::census::live().nth(a2 as usize) {
Some((_, live)) => live,
None => SyscallError::InvalidArgument.to_u64(),
}
}
DA::CENSUS_KIND => match u32::try_from(a2).ok().and_then(toyos_abi::handle::ObjectKind::from_word) {
Some(kind) => crate::object::census::live(kind),
None => SyscallError::InvalidArgument.to_u64(),
},
// Armed, not #[cfg]'d, so it doesn't ship in every kernel this suite boots:
// the real bound is unreachable (no guest makes 65,536 threads).
DA::LOWER_SYSINFO_BOUND => {
Expand Down
52 changes: 52 additions & 0 deletions sdk/abi/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,58 @@ impl RawHandle {
/// any table issues.
pub const HANDLE_INVALID: RawHandle = RawHandle(u32::MAX);

/// Every kind of kernel object a handle can name, as the word
/// [`CENSUS_KIND`](crate::syscall::debug_action::CENSUS_KIND) takes.
#[repr(u32)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ObjectKind {
PipeRead = 0,
PipeWrite = 1,
Connection = 2,
Device = 3,
Acceptor = 4,
Inbox = 5,
SharedMem = 6,
Connector = 7,
Namespace = 8,
File = 9,
Console = 10,
SysCap = 11,
Process = 12,
}

impl ObjectKind {
pub const ALL: [ObjectKind; 13] = [
Self::PipeRead,
Self::PipeWrite,
Self::Connection,
Self::Device,
Self::Acceptor,
Self::Inbox,
Self::SharedMem,
Self::Connector,
Self::Namespace,
Self::File,
Self::Console,
Self::SysCap,
Self::Process,
];

pub const fn from_word(word: u32) -> Option<Self> {
if (word as usize) < Self::ALL.len() { Some(Self::ALL[word as usize]) } else { None }
}
}

// `from_word` indexes `ALL` by the word: a row out of order would count a
// kind's neighbour.
const _: () = {
let mut i = 0;
while i < ObjectKind::ALL.len() {
assert!(ObjectKind::ALL[i] as usize == i, "ObjectKind::ALL is out of word order");
i += 1;
}
};

/// What the holder of a handle may do with the object behind it.
///
/// Rights only ever shrink: a duplicate may ask for a subset and nothing
Expand Down
41 changes: 12 additions & 29 deletions sdk/abi/src/syscall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,16 @@ pub const SYS_NAMESPACE_OPEN: u64 = 102;
pub const SYS_HANDLE_SEND: u64 = 103;
/// Take the oldest batch of handles the peer sent. See [`handle_recv`].
pub const SYS_HANDLE_RECV: u64 = 104;
/// Make a shared-memory region, sized up to whole 2 MiB pages.
/// Make a shared-memory region, sized up to whole [`SHM_PAGE`]s.
/// See [`shm_create`].
pub const SYS_SHM_CREATE: u64 = 105;
/// Map a region into the caller. Idempotent: a second call answers the first
/// call's address. See [`shm_map`].
pub const SYS_SHM_MAP: u64 = 106;

/// What [`SYS_SHM_CREATE`] rounds a length up to a whole number of.
pub const SHM_PAGE: usize = 2 << 20;

/// Wait for the process a handle names and take its exit code, gated by
/// [`Rights::WAIT`]. See [`process_wait`].
///
Expand Down Expand Up @@ -554,13 +557,17 @@ pub enum FileType {
// 9 was `Nic`, the type a claim on the kernel's own NIC driver reported.
// A claim on a PCI function reports `Unknown`: what it is, is the driver's
// to know, and this kernel does not know it.
/// A shared-memory region; `size` is its length. A program's stdout and
/// stderr are one when they are its log ring (`toyos::log`).
/// A shared-memory region of memory the kernel allocated to it and maps
/// write-back, as [`shm_create`] makes; `size` is its length. A program's
/// stdout and stderr are one when they are its log ring (`toyos::log`).
SharedMemory = 10,
/// A directory, which only [`stat`] answers.
Directory = 11,
/// A symbolic link, which only [`stat`] answers; `size` is its target's length.
Symlink = 12,
/// Any other shared-memory region, such as a device's registers; `size`
/// is its length. A peer's buffer is never one.
DeviceMemory = 13,
}

impl FileType {
Expand All @@ -578,6 +585,7 @@ impl FileType {
10 => Some(Self::SharedMemory),
11 => Some(Self::Directory),
12 => Some(Self::Symlink),
13 => Some(Self::DeviceMemory),
_ => None,
}
}
Expand Down Expand Up @@ -815,7 +823,7 @@ pub mod debug_action {
pub const TLB_ACK_DELAY_ARM: u64 = 12;
pub const TLB_ACK_DELAY_DISARM: u64 = 13;
/// How many kernel objects of one kind are alive right now. The argument is
/// an [`OBJECT_KINDS`](super::OBJECT_KINDS) index.
/// an [`ObjectKind`](crate::handle::ObjectKind) word.
///
/// **Per kind and not a total**: an object of one kind that is never
/// released is invisible behind ordinary churn in another.
Expand Down Expand Up @@ -886,31 +894,6 @@ pub mod debug_action {
pub const RING0_FIRE_OTHER_SPAN: u64 = 2;
}

/// Every kind of kernel object, in the order the kernel's own `kobject!`
/// declares them — so an index into this is the index
/// [`debug_action::CENSUS_KIND`] takes.
///
/// In the ABI rather than in the kernel alone, because a census nobody can read
/// per kind is a total. The kernel checks this list against its own declaration
/// order when the action is called, so a row added to `kobject!` without a row
/// here is a named refusal rather than an index that quietly names its
/// neighbour.
pub const OBJECT_KINDS: &[&str] = &[
"PipeRead",
"PipeWrite",
"Connection",
"Device",
"Acceptor",
"Inbox",
"SharedMem",
"Connector",
"Namespace",
"File",
"Console",
"SysCap",
"Process",
];

/// Create a pipe. Returns the read and write ends.
///
/// **Fallible, because `sys_pipe` is.** It answers `ResourceExhausted` on three
Expand Down
5 changes: 3 additions & 2 deletions sdk/toyos/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ pub enum AudioError {
/// pipe with it. Handles cross before the frame that names them, so a
/// short batch is a protocol violation and never something to wait for.
MissingHandles,
/// The slot ring soundserver sent is refused ([`SharedMemory::lent`]).
Lent(crate::shm::Lent),
Ipc(IpcError),
Protocol(u32),
}
Expand Down Expand Up @@ -215,8 +217,7 @@ impl AudioStream {

let slot_count = resp.slot_count as u32;
let shm_size = AudioSlotHeader::SIZE + slot_count as usize * resp.client_period_bytes as usize;
let shm = SharedMemory::adopt(batch[STREAM_OPENED_SHM], shm_size)
.map_err(|e| AudioError::Ipc(IpcError::Syscall(e)))?;
let shm = SharedMemory::lent(batch[STREAM_OPENED_SHM], shm_size).map_err(AudioError::Lent)?;
let slot_writer = AudioSlotWriter::new(shm, resp.client_period_bytes, slot_count);

Ok(Self {
Expand Down
Loading