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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: tooling
opened: 2026-09-28
---

# `a_key_being_built_is_waited_for_and_another_key_is_not` reds under host load

`src/buildlock.rs`'s last assertion, `keyed_idle(&root, Keyed::Sysroot,
"k1").is_some()` after the `using` guard drops, failed once under `cargo test
--lib` on a host running several other agents' builds concurrently
(`wt/toyos-desk1` at `e039fe6b`). Run alone straight after, the same test
passed in 0.59 s. The test spawns real child processes and times state
transitions against wall-clock sleeps and deadlines (`appeared`,
`keyed_idle`'s own polling), so host contention can move an event past a
window the test assumed was empty.

**Exit**: reproduce under synthetic host load (parallel `cargo build`s pinned
to the same cores) to find which wait the contention defeats, then replace
the polled read with the event itself.

## Owner

Held by the orchestrator.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# A client waits on the compositor's answer with no bound

`window::clipboard_set` (`userland/toyos-window/src/lib.rs`) blocks in
`recv_header` for `MSG_COPY_REGION` on a copy past `MAX_INLINE_PAYLOAD`, and
`Window::create` blocks the same way for `MSG_WINDOW_CREATED`. Neither wait has
a deadline, so a compositor that is alive and not answering wedges every
terminal or editor that copies, and every program that asks for a window.

Owner: `toyos-window`.

**Exit**: each wait has a bound, and a missed one is a `CreateError` of its own.
19 changes: 19 additions & 0 deletions issues/design-debt/decode-payload-accepts-bytes-past-its-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# `ipc::decode_payload` accepts bytes past the type it decodes

`toyos::ipc::decode_payload` refuses a payload shorter than its `T` and ignores
whatever follows one. Each caller decides for itself whether trailing bytes are
out of protocol, and the compositor's client frames
(`userland/compositor/src/session.rs`) disagree: `copy_begin` refuses them with
a length check of its own, and `MSG_PRESENT`'s `Rect`, `MSG_SET_CURSOR`'s
style, `ResolutionRequest` and `CreateWindowRequest` accept them.

Owner: `toyos::ipc`.

**Exit**: one trailing-bytes rule in `toyos::ipc` that every `decode_payload`
caller gets, and `copy_begin`'s own check deleted.
17 changes: 5 additions & 12 deletions issues/isolation/a-received-handle-has-no-knowable-type.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ typed can be ended by whoever sent it.** The sender needs nothing but
- An audio client receives two handles from soundd and calls `SYS_SHM_MAP` on
the first (`toyos/src/audio.rs`). A hostile *server* ends every client.
- A window client receives its buffer the same way (`userland/toyos-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.
- netd adopts as pipes the two handles a client sends with a piped socket
(`DataPipes::take`) and the one with a piped bind (`handle_tcp_bind_piped`),
both in `userland/netd/src/main.rs`: a *client* ends it the same way.

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 —
Expand All @@ -57,18 +62,6 @@ change, and a new syscall is the owner's to approve.
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 one instance a hostile *client* can reach is closed.** `/system/bin/init`'s
launcher takes `extra` connectors from anybody holding a `launcher` connector
and hands them to `SYS_NAMESPACE_BUILD`, and that call answers
`InvalidArgument` for a wrong type rather than ending the caller. It is the
one handle argument in the ABI that routinely crosses a trust boundary,
`kernel/CLAUDE.md` says so where the policy is stated, and `launcher_refusals`
gates it.
- **Every other instance needs a hostile *server*** — soundd sending an audio
client its region, the compositor sending a window its buffer. A client whose
server is hostile has already lost: that server chooses what the client sees,
when it is answered, and whether it is answered at all. Ending it with a
`WrongType` is not a new capability.
- **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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
status: open
kind: defect
opened: 2026-09-27
---

# A refused handle move leaves the compositor holding what it meant to send

`deliver_with_handles` (`userland/compositor/src/client.rs`) sends through
`Connection::try_send_with_handles`, which calls `syscall::handle_send` and
then the frame. When `handle_send` itself is refused, the kernel restores every
handle at its own number (`sys_handle_send` in `kernel/src/syscall/ipc.rs`).
The compositor drops the client, but it still holds the handle and never
closes it.

Each refusal keeps one handle slot and one region. Three sites are affected:

- `create_window`'s `MSG_WINDOW_CREATED`: a client that closes before the
answer arrives.
- `paste`'s `MSG_CLIPBOARD_PASTE_SHM`.
- `rebuffer`'s `MSG_WINDOW_RESIZED`, reached by any app through
`MSG_SET_RESOLUTION`, which reallocates every window's buffer. A window that
never takes its handles fills its `MAX_QUEUED_BATCHES` queue, and the next
move is refused.

A client can repeat this, and nothing bounds it before the handle table or
memory runs out.

Owner: the compositor's client delivery, `deliver_with_handles` in
`userland/compositor/src/client.rs`.

**Exit**: `deliver_with_handles` calls `syscall::handle_send` on its own and
closes the handles if that is refused, then sends the frame. It never closes a
handle after a successful move. `copy_begin` in
`userland/compositor/src/session.rs` already has this shape.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
status: open
kind: defect
opened: 2026-09-27
---

# A window's buffer is read while its client writes it

Each window has one region, which the client draws into and the compositor
blits from (`render::draw_window` in `userland/compositor/src/render.rs`). The
compositor reads it as a `&[u8]` while the client may be writing it: a data
race in Rust's model, and on the panel a frame whose pixels come from two of
the client's frames.

Owner: the compositor's window blit, `render::draw_window` in
`userland/compositor/src/render.rs`.

**Exit**: the compositor makes two buffers per window, a client hands one over
with its present and gets it back on release, and the compositor never reads a
buffer the client holds.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# `SharedMemory`'s safe slices rest on a size nobody checks

`SharedMemory::as_slice`, `as_mut_slice` and `as_atomic` (`toyos/src/shm.rs`)
are safe functions that make a slice of `size` bytes over the mapping. The type
enforces neither premise that makes this sound:

- `adopt` is safe and takes `size` from its caller without checking it against
the region the kernel mapped. A server that adopts a peer's region at the
peer's declared length reads and writes past it in safe code.
- `shm_map` is idempotent, so `share()` followed by `adopt` gives two
`SharedMemory` values over one mapping in one process. `as_mut_slice` on one
then aliases `as_slice` or `as_atomic` on the other.

`#![forbid(unsafe_code)]` on the compositor (`userland/compositor/src/main.rs`)
leans on both. The compositor meets them, since it adopts only regions it
created and the kernel's framebuffer and cursor, but the compiler does not know
that.

Owner: `toyos::shm`.

**Exit**: `adopt` is an `unsafe fn` whose contract is both premises, or it
checks `size` against the kernel's region and refuses a second mapping of one
region.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: defect
opened: 2026-09-27
---

# The compositor ignores a message type it does not know

`Session::dispatch` (`userland/compositor/src/session.rs`) ends its match with
`_ => {}`: a frame of a type no protocol here defines is read, framed, and
dropped without a word, on a window's connection and on a fresh one alike. The
one retired type, `window::MSG_RETIRED_CLIPBOARD_SET_SHM`, is refused by name;
every other unknown type is accepted and silently discarded.

`compositor_stall`'s streaming case depends on it: its window sends
`UNKNOWN_MSG` on every pass as load with nothing to draw
(`tests/toyos-rust-tests/src/bin/compositor_stall.rs`). A refusal there would
drop the window after its first frame and leave the case passing with no load.

Owner: `Session::dispatch` in `userland/compositor/src/session.rs`.

**Exit**: an unknown type drops its client with `DropReason::OutOfProtocol`,
and the stall's stream is a type the compositor serves without drawing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-09-28
---

# The compositor keeps a committed region's mapping for as long as the client does

`CopyRegion::take` (`userland/compositor/src/client.rs`) drops the
compositor's own `SharedMemory`, closing its handle to the region. That does
not unmap it: `SharedMemObject::on_zero_handles` (`kernel/src/object/shm.rs`)
tears down every process's mapping together, only once every handle to the
object is gone anywhere — `unmap_from` exists to drop one process's own
mapping on its own, but nothing outside `kernel/src/inbox/mod.rs` calls it. A
client that keeps the handle its copy answered with keeps the compositor's own
2 MiB mapping alive too, for as long as it likes, and a client that copies
repeatedly and keeps every handle leaves one such mapping per copy — bounded
only by its own handle table, never by the compositor's need for the memory.

Owner: `kernel::object::shm`'s handle-driven mapping teardown.

**Exit**: closing a process's last handle to a shared-memory object unmaps
that process's own view at once, through `unmap_from`, independent of whether
another process still holds a handle to the same object.
Loading
Loading