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
5 changes: 5 additions & 0 deletions docs/src/architecture/authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@

- A connection carries bounded, whole messages, each holding bytes and handles
together, so a message and the authority it passes arrive atomically.
- A received handle arrives with the kind of object it names, as the kernel
read it when it delivered the handle. What the sender's protocol says the
handle is remains the sender's word: a receiver takes it as a kind only where
the kernel reported that kind, and refuses any other, including a kind it
does not know.
- Notifications coalesce, and polling for readiness does not consume one;
signalling and consuming each need their own right.
- Pipes remain for real byte streams; bulk data stays in shared memory.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
status: open
kind: finding
opened: 2026-11-11
---

# A connection call on a bare handle of another kind ends its caller

The SDK's connection calls take any `AsHandle`: `FrameRx::pump` and `recv`,
and `ipc::send`, `signal` and their `try_` forms (`sdk/toyos/src/ipc.rs`). The
message syscalls resolve that handle with `HandleTable::get`, which ends the
caller for a handle of another kind. So a received handle that a process keeps
bare (`OwnedHandle::from(received)`) and then uses as a connection can end it,
which `ReceivedHandle::typed` exists to prevent. No receive site does this
today: every connection a peer sends is made a `Connection` through `typed`
before any connection call. Found in review of #1239, by reading.

**Exit**: the SDK's connection calls take a `Connection` rather than any
`AsHandle`, so a bare handle cannot reach a message syscall.

- **Owed on metal:** the receive that reports each handle's kind (#1239),
end to end. Its guest binaries are shared-boot members, which no guest row
runs.
- Rows: the `testcases`, `testcases-watchdog` and `shared-debug` boots of
main, whole, and the `proctreecase` boot (`process_tree`).
- Bench: the T14.
- Main must show `messages_are_whole: each handle arrived with its kind, and
another kind was refused by name`, then `messages_are_whole: ok`.
`mutual_kill`, `moved_rights` and `served_file_offset` pass. `process_tree`
passes, with `a_pipe_is_no_place` refused by the supervisor by name.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ soundserver sent with its acceptance, unchecked (`offered` and
pipe end or a connection there ends hda rather than being refused. A disk driver
is not reached, because `Region::lent` refuses what `SYS_FSTAT` does not answer
as a region before the session lends it (`Sessions::open`,
`lib/blockserve/src/sessions.rs`). A region's kind is one `SYS_FSTAT` settles,
so this is not `issues/a-received-handle-has-no-knowable-type.md`'s case.
`lib/blockserve/src/sessions.rs`).

The kernel owns the fix rather than each lender: what a holder lends is a
peer's by `lend`'s contract, and the kernel already answers a peer's handle of
Expand Down
97 changes: 0 additions & 97 deletions issues/a-received-handle-has-no-knowable-type.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,8 @@ By mechanisms eliminated:
3. What the loader named, independent of the others: `SYS_BOOT_SOURCES`,
the inventory's partition records, the device-path parser. **Done**
(#1210).
4. The TTY mark, with or after 2: `SYS_MARK_TTY`. It meets
`issues/a-received-handle-has-no-knowable-type.md`, which wants
`SYS_FSTAT` to answer more, so the two are designed together.
4. The TTY mark, with or after 2: `SYS_MARK_TTY`. A receive reports each
handle's kind and a TTY is no kind, so `SYS_FSTAT` still answers it.
5. The main thread's stack, after #1200: `SYS_STACK_INFO` and the kernel's
two stack constants.

Expand Down
10 changes: 6 additions & 4 deletions kernel/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use alloc::vec::Vec;
use kernel::message::{Head, Queue, Untaken};
use kernel::pipe::{End, Holders, Released};
use kernel::sched::task::WaitClass;
use toyos_abi::handle::RawHandle;
use toyos_abi::handle::ReceivedEntry;
use toyos_abi::syscall::{MAX_MESSAGE_LEN, MAX_QUEUED_HANDLES, MAX_QUEUED_MESSAGES, MAX_TRANSFER_HANDLES};

use crate::frame_bytes::{self, Bounce, Fill, FrameBytes, Spares};
Expand Down Expand Up @@ -162,7 +162,8 @@ pub enum Unsent {
/// lock let go. Spares past its batch stay listed for the next take.
pub struct Received {
pub head: Head,
pub installed: [RawHandle; MAX_TRANSFER_HANDLES],
/// Each handle and its object's kind, read as it was installed.
pub installed: [ReceivedEntry; MAX_TRANSFER_HANDLES],
_spares: Spares,
boost: bool,
}
Expand Down Expand Up @@ -293,13 +294,14 @@ impl QueueRef {
if !table.has_room(head.handles) {
return Err(Unclaimed::Room(head.handles));
}
let mut installed = [RawHandle(0); MAX_TRANSFER_HANDLES];
let mut installed = [ReceivedEntry::NONE; MAX_TRANSFER_HANDLES];
let Some(Ok((message, handles))) = state.queue.pop(head.len, head.handles, |_| true) else {
unreachable!("message: a front its reader holds left the queue or was refused");
};
assert_eq!(message, head, "message: the front moved under its lock");
for (slot, entry) in installed.iter_mut().zip(handles) {
*slot = table.install(entry).expect("the table had room");
let kind = entry.object().object_kind() as u32;
*slot = ReceivedEntry { handle: table.install(entry).expect("the table had room"), kind };
}
let spares = state.bytes.take(head.len, into);
let boost = core::mem::take(&mut state.rt_boost_pending);
Expand Down
7 changes: 7 additions & 0 deletions kernel/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ macro_rules! kobject {
}
}

/// The word a receive reports this object as.
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(),)+
Expand Down
4 changes: 2 additions & 2 deletions kernel/src/syscall/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use toyos_abi::handle::{RawHandle, Rights};
use toyos_abi::syscall::debug_action as DA;
use toyos_abi::syscall::*;

use super::{HANDLE_LEN, TRANSFER_ENTRY_LEN};
use super::{RECEIVED_ENTRY_LEN, TRANSFER_ENTRY_LEN};
#[cfg(feature = "test-actuators")]
use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_NONCE, LOCK_ACROSS_SWITCH};
use super::device::{
Expand Down Expand Up @@ -612,7 +612,7 @@ fn incoming<'a>(ctx: &SyscallContext<'a>, args: &MessageRecv) -> Result<Option<I
Some(Incoming {
head: ctx.user_bytes_mut(UserAddr::new(args.head), head_len)?,
bytes: ctx.user_bytes_mut(UserAddr::new(args.bytes), len)?,
handles: ctx.user_bytes_mut(UserAddr::new(args.handles), (cap * HANDLE_LEN) as u64)?,
handles: ctx.user_bytes_mut(UserAddr::new(args.handles), (cap * RECEIVED_ENTRY_LEN) as u64)?,
cap,
})
})())
Expand Down
17 changes: 10 additions & 7 deletions kernel/src/syscall/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ use crate::user_ptr::SyscallContext;
use crate::UserAddr;
use crate::{message, pipe, process};

use toyos_abi::handle::{NotRights, RawHandle, Rights, TransferEntry};
use toyos_abi::handle::{NotRights, RawHandle, ReceivedEntry, Rights, TransferEntry};
use toyos_abi::syscall::*;
use kernel::sched::task::WaitClass;
use kernel::vm::Prot;

use super::{cancelled, HANDLE_LEN, TRANSFER_ENTRY_LEN};
use super::{cancelled, RECEIVED_ENTRY_LEN, TRANSFER_ENTRY_LEN};
use super::handles::handle_result;

pub(super) fn sys_pipe() -> u64 {
Expand Down Expand Up @@ -627,10 +627,13 @@ fn recv(conn_h: RawHandle, into: &mut Incoming<'_>, nonblock: bool) -> u64 {
}

/// A message is taken, its bytes in the reader's buffers: answer its
/// handles' numbers and wake the sender, for its room.
fn delivered(rx: &QueueRef, into: &mut Incoming<'_>, head: kernel::message::Head, installed: &[RawHandle]) -> u64 {
/// handles' numbers and kinds and wake the sender, for its room.
fn delivered(rx: &QueueRef, into: &mut Incoming<'_>, head: kernel::message::Head, installed: &[ReceivedEntry]) -> u64 {
let installed = &installed[..head.handles];
let told = installed.iter().enumerate().try_for_each(|(i, h)| into.handles.write_at(i * HANDLE_LEN, &h.0.to_ne_bytes()));
let told = installed
.iter()
.enumerate()
.try_for_each(|(i, entry)| into.handles.write_at(i * RECEIVED_ENTRY_LEN, toyos_abi::usersafe::bytes(entry)));
if let Err(faulted) = told {
return undelivered(rx, installed, faulted);
}
Expand All @@ -640,9 +643,9 @@ fn delivered(rx: &QueueRef, into: &mut Incoming<'_>, head: kernel::message::Head

/// A message taken whose reader's buffers faulted: its bytes are lost, and
/// its handles, installed at numbers the reader was never told, are closed.
fn undelivered(rx: &QueueRef, installed: &[RawHandle], faulted: crate::user_ptr::Faulted) -> u64 {
fn undelivered(rx: &QueueRef, installed: &[ReceivedEntry], faulted: crate::user_ptr::Faulted) -> u64 {
process::with_handles(|handles| {
for &h in installed {
for ReceivedEntry { handle: h, .. } in installed.iter().copied() {
// A sibling may have closed the number since, or reused it for a
// handle of its own, which this then closes: the race is the
// misbehaving process's own, and stays inside it.
Expand Down
6 changes: 3 additions & 3 deletions kernel/src/syscall/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ mod paged;
mod proc;
pub(crate) mod vm;

use toyos_abi::handle::RawHandle;
use toyos_abi::handle::ReceivedEntry;
use toyos_abi::syscall::SyscallError;

use crate::process;

/// Byte width of one [`RawHandle`] on the wire.
const HANDLE_LEN: usize = core::mem::size_of::<RawHandle>();
/// Byte width of one handle a receive answers.
const RECEIVED_ENTRY_LEN: usize = core::mem::size_of::<ReceivedEntry>();

/// Byte width of one handle a send moves.
const TRANSFER_ENTRY_LEN: usize = core::mem::size_of::<toyos_abi::handle::TransferEntry>();
Expand Down
14 changes: 7 additions & 7 deletions lib/blockserve/src/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,21 @@ impl Link {
RxStep::Malformed => "a frame this protocol does not make",
RxStep::Frame { msg_type: disk::MSG_SESSION, payload_len, .. } => {
let session = disk::Session::decode(self.rx.payload(payload_len));
let handles = self.rx.take_handles::<2>();
let handles = self.rx.take_handles::<2>().map(|[conn, region]| (conn.typed::<Connection>(), region));
match (session, handles) {
(Some(session), Some([conn, region])) => {
let conn = Connection::received(conn);
handed.push(Handed::Session { session, conn, region });
(Some(session), Some((Ok(conn), region))) => {
handed.push(Handed::Session { session, conn, region: region.into() });
continue;
}
(_, Some((Err(_), _))) => "a session whose connection is no connection",
(_, Some(_)) => "a session frame this protocol does not make",
(_, None) => "a session without its two handles",
}
}
RxStep::Frame { msg_type: msg_type @ (disk::MSG_DESCRIBE | disk::MSG_DESIGNATE), payload_len, .. } => {
let payload = self.rx.payload(payload_len).to_vec();
match self.rx.take_handles::<1>() {
Some([conn]) => {
let conn = Connection::received(conn);
match self.rx.take_handles::<1>().map(|[conn]| conn.typed::<Connection>()) {
Some(Ok(conn)) => {
let asked = match msg_type {
disk::MSG_DESCRIBE => disk::disk(&payload).map(|disk| Handed::Describe { disk, conn }),
_ => disk::Designation::decode(&payload).map(|order| Handed::Designate { order, conn }),
Expand All @@ -184,6 +183,7 @@ impl Link {
None => "a description or designation frame this protocol does not make",
}
}
Some(Err(_)) => "a description or designation whose connection is no connection",
None => "a description or designation without the client's connection",
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/blockserve/src/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Region {

/// A region a client sent, refused unless it is memory the kernel
/// allocated and exactly a session long.
pub fn lent(handle: toyos::OwnedHandle) -> Result<Self, Lent> {
pub fn lent(handle: impl Into<toyos::OwnedHandle>) -> Result<Self, Lent> {
Ok(Self { memory: SharedMemory::lent(handle, SESSION_BYTES)? })
}

Expand Down
5 changes: 4 additions & 1 deletion lib/link/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,10 @@ impl<'a> Session<'a> {
RxStep::Eof => Some(Ended::HungUp("its process ended before it answered")),
RxStep::Frame { msg_type: wire::MSG_OPENED, payload_len: 0, handles: wire::OPENED_HANDLES } => {
let [ring, rung] = self.rx.take_handles::<{ wire::OPENED_HANDLES }>().expect("counted above");
self.bells = Some((Waker::from(ring), Bell::from(rung)));
let (Ok(ring), Ok(rung)) = (ring.typed::<Waker>(), rung.typed::<Bell>()) else {
panic!("{}: the stack opened this link with handles that are not its bells", self.label);
};
self.bells = Some((ring, rung));
// Its work is done: the bells carry the link from here.
self.conn = None;
say!("{}: the link is open", self.label);
Expand Down
2 changes: 1 addition & 1 deletion lib/link/src/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl Region {

/// A region a driver sent, refused unless it is memory the kernel
/// allocated and exactly a region long.
pub fn lent(handle: toyos::OwnedHandle) -> Result<Self, Lent> {
pub fn lent(handle: impl Into<toyos::OwnedHandle>) -> Result<Self, Lent> {
Ok(Self { memory: SharedMemory::lent(handle, REGION_BYTES)? })
}

Expand Down
2 changes: 1 addition & 1 deletion lib/logkeeper-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fn answer(conn: &Connection) -> Result<Served, String> {
let boot_so_far: u64 = toyos::ipc::decode_payload(rx.payload(frame.payload_len))
.map_err(|e| format!("logkeeper's answer is not one `u64`: {e:?}"))?;
let [pipe] = rx.take_handles::<1>().ok_or("logkeeper's answer carried no pipe")?;
let pipe = Pipe::from(pipe);
let pipe = pipe.typed::<Pipe>().map_err(|why| format!("logkeeper's answer carried no pipe: {why}"))?;
Ok(Served { pipe, boot_so_far })
}

Expand Down
8 changes: 6 additions & 2 deletions lib/session/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,13 @@ pub fn open(
return Err(format!("the supervisor refused a session: {why}"));
}
let [launcher, controls] = rx.take_handles::<2>().ok_or("the session came without its handles")?;
let namespace = |handle: toyos::ipc::ReceivedHandle| {
handle.typed::<toyos::namespace::Namespace>().map_err(|why| format!("the session's handles are not namespaces: {why}"))
};
let (launcher, controls) = (namespace(launcher)?, namespace(controls)?);
asks += 1;
match toyos::namespace::Namespace::received(controls).open(CONTROL) {
Ok(control) => return Ok((toyos::namespace::Namespace::received(launcher), control)),
match controls.open(CONTROL) {
Ok(control) => return Ok((launcher, control)),
Err(toyos_abi::syscall::SyscallError::Gone) if began.elapsed() < ENDED_UNREAD => {}
Err(toyos_abi::syscall::SyscallError::Gone) => {
return Err(format!(
Expand Down
Loading
Loading