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
10 changes: 5 additions & 5 deletions issues/a-moved-handle-is-always-re-movable.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ opened: 2026-08-10

# A handle that can be moved can always be moved on

Both move paths — `SpawnArgs::endow` and `SYS_HANDLE_SEND` — require
Both move paths — `SpawnArgs::endow` and `SYS_MESSAGE_SEND` — require
`Rights::TRANSFER` on the handle being moved, and both carry the source's rights
into the destination unchanged. So every handle that arrives by a move arrives
carrying `TRANSFER`, and its new holder can move it on. There is no way to
Expand All @@ -33,7 +33,7 @@ recording.
## What would fix it

A rights word per moved handle, on **both** paths: `EndowEntry` gains one and
`SYS_HANDLE_SEND` takes `[TransferEntry { handle, rights }]` instead of
`SYS_MESSAGE_SEND` takes `[TransferEntry { handle, rights }]` instead of
`[RawHandle]`, with `RIGHTS_UNCHANGED` as the wire encoding
`SYS_HANDLE_DUP` already uses. The endowment design argues against a rights word
on the endow path — "a second place to shrink rights and the first one already
Expand All @@ -51,8 +51,8 @@ trigger: *"with `grant` owner-only, the set that can ever map is exactly the set
the owner named, so revocation has no caller today … It stops being sound the
moment the reachable set is no longer exactly what the owner named — if
delegation or re-grant is reintroduced, or when `SYS_HANDLE_SEND` makes a grant
transferable."* `SYS_HANDLE_SEND` exists, so the trigger has fired: the reachable
set is no longer what the owner named.
transferable."* Handles move by `SYS_MESSAGE_SEND`, so the trigger has fired: the
reachable set is no longer what the owner named.

That leaves two answers and the rights word above is the cheaper one. **Revocation
proper is rejected by name** in the capability-handle design — shm revoke,
Expand All @@ -70,5 +70,5 @@ folded). The rights model cannot spell "you may map this and nobody else may",
so the property the capability design claims is not the property the code has —
soundd's per-client ring is re-sendable by its client. Owed by whoever next
opens the handle ABI: a rights word on `EndowEntry` **and** on
`SYS_HANDLE_SEND`, on one ABI-only PR, since doing either alone makes the two
`SYS_MESSAGE_SEND`, on one ABI-only PR, since doing either alone makes the two
move verbs disagree.
46 changes: 46 additions & 0 deletions issues/a-pipe-write-of-a-few-bytes-can-tear-between-two-writers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
status: owner
kind: question
opened: 2026-10-21
---

# A pipe write of a few bytes can tear between two writers

`pipe::try_write` (`kernel/src/pipe.rs`) writes `min(len, free space)` and
`sys_write` (`kernel/src/syscall/io.rs`) returns that short count. It waits
only while the ring is full, and the caller's write loop sends the rest later.
So whenever the reader lags and the ring has less free space than a write,
even a 40-byte line is split. Another writer's bytes can land between the two
parts. POSIX promises a pipe write of at most `PIPE_BUF` bytes is never
interleaved with another's, and `sdk/libc/include/limits.h` declares no
`PIPE_BUF` at all.

Every multi-writer pipe has this gap: `{ a & b; } | c`, `make -j | tee log`,
and a stdout stream several children share, whose lines tear in one log
while the file server behind it is busy. A one-byte write, such as a
jobserver token, cannot tear. Record boundaries matter to any
multi-writer pipe, so the fix is native, not POSIX's alone.

Only the pipe can keep such a write whole: its writers are separate
processes holding one pipe and sharing no lock, so no userland library can
order them.

The change is one branch in the pipe's write path, plus libc's constant, and
it costs two semantics. A write of at most `PIPE_BUF` waits for room for the
whole write (`pipe::has_space(id, len)`) where today it takes part of the
ring, and a non-blocking one (`sys_write_nonblock`,
`kernel/src/syscall/io.rs`) answers `WouldBlock` where today it answers a
short count, as POSIX has `O_NONBLOCK` do. Writes longer than `PIPE_BUF`
behave as today.

Owner: the kernel's pipe. It is the owner's call because it changes the
kernel's write semantics (ruling M: no kernel change for POSIX, so the case
for it is the native one above).

## Exit

A pipe write of at most `PIPE_BUF` bytes lands whole or waits for room, a
non-blocking one lands whole or answers `WouldBlock`, libc declares
`PIPE_BUF`, and a host test of the pipe shows two writers' writes of at most
`PIPE_BUF` never interleave while a reader lags. Longer writes may still be
split.
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ while the parent still holds the handle.
whose exit ends a mapping at the close of its process's last handle and so
reaches this one wherever the process closed that handle, before it ended or
in its teardown. It does not reach a process that mapped the region and moved
its handle on by `SYS_HANDLE_SEND` (`sys_handle_send`,
its handle on by `SYS_MESSAGE_SEND` (`sys_message_send`,
`kernel/src/syscall/ipc.rs`), which is no close.
4 changes: 2 additions & 2 deletions issues/a-stream-cannot-move-between-processes.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ either side. Stage 3 makes std refuse both loudly instead.
Every path std offers to move a raw handle into another process answers that
refusal before anything moves: a spawn's stdio slots,
`CommandExt::inherit_handle` into any slot, `CommandExt::endow` and
`Command::provide`, each starting no child. `toyos::ipc`'s `send_handles`
`Command::provide`, each starting no child. `toyos::ipc`'s `send_with`
takes only `OwnedHandle`s, and a stream's handle becomes one only through
`OwnedHandle::from_raw`, whose contract stage 3 says excludes a handle
`toyos::net` answers for: the type refuses it. And `toyos::net` refuses by
Expand All @@ -40,7 +40,7 @@ as an inetd-style server does through its child's stdio, then fails loudly
on ToyOS where today it works, and one that takes a moved stream back as a
`TcpStream`, as socket activation does and as no ToyOS std builds today, is
refused, though root `CLAUDE.md` says existing Rust just works. The bare
`dup`, `SYS_HANDLE_SEND` and `SYS_SPAWN` syscalls are not checked.
`dup`, `SYS_MESSAGE_SEND` and `SYS_SPAWN` syscalls are not checked.

Owner: the orchestrator, who stages
`issues/netstack-moves-every-byte-through-two-kernel-pipes.md`.
Expand Down
11 changes: 11 additions & 0 deletions issues/four-shares-take-a-file-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ never had one start past 3 served connections, 1 waiting on its hello or 0
streams, nor more than 5 of DATA's connections served at once. A parallel
build (cargo with rustc as its direct children) is the unmeasured load.

Each writer streaming to a file at once holds one of its share's 16 streams,
so a `make -j16` whose jobs each stream to a log of their own holds all 16,
and separate stdout and stderr files per job need 32. Inferred, not
measured: the share's 32 served connections, spent by every process of the
session at one per directory, are probably the first bound such a build
reaches. Raising `STREAM_SHARE` doubles the uncharged rings a share can pin,
2 MiB each (`issues/nothing-charges-kernel-memory-to-a-process.md`), so it
waits for one of two exits: a measured parallel build on ToyOS shows which
bound it reaches and by how much, or kernel memory is charged and a share's
streams are bounded by bytes rather than by count.

Owner: the file server.

## Exit condition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ opened: 2026-10-05
# std says a launch moves its handles to the launcher, even when the move is refused

`toyos::launch::launch` consumes every handle a `Launch` names: the kernel
moves them to the launcher, or refuses the move and `Connection::send_handles`
moves them to the launcher, or refuses the move and `Connection::send_with`
closes them in the caller (`sdk/toyos/src/ipc.rs`, `sdk/toyos/src/launch.rs`). The
prose of std's ToyOS backend about the same handles says something else:

Expand Down
2 changes: 1 addition & 1 deletion issues/the-capability-end-state-is-twelve-answers.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ process has.
is created with `Rights::NONE` there because "every bit on a `SysCap` is an
authority init decides per program" — the machine's only one is minted with
`TRANSFER` at `kernel/src/loader/mod.rs:946`. The mechanisms are
`SYS_HANDLE_SEND`/`SYS_HANDLE_RECV` over a `Connection`, where the connection
`SYS_MESSAGE_SEND`/`SYS_HANDLE_RECV` over a `Connection`, where the connection
and every handle must carry `TRANSFER`, no handle may be named twice, and the
connection may not be sent over itself (`4a98107f^:kernel/src/arch/syscall.rs:2086`,
`:2090`); and `SpawnArgs::endow`. Nothing else crosses a process boundary — the
Expand Down
1 change: 1 addition & 0 deletions kernel/pure/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod clock;
pub mod cpu;
pub mod dirnodes;
pub mod iova;
pub mod message;
pub mod microcode;
pub mod mtrr;
pub mod owners;
Expand Down
Loading