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: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2733,14 +2733,17 @@ mod tests {
/// Features of a host-tested member that choose a build's world rather
/// than revert a decision: loom's atomics, a crate's `std` and its default
/// set, the toolchain's `core` and `rustc-dep-of-std`, libc's `std-runtime`,
/// the signer's `sign`, `flaws`, which compiles what a simulator's
/// libc's `fd-owner` and `toyos`'s `fd-table`, which pick the copy that
/// owns the descriptor table, the signer's `sign`, `flaws`, which compiles what a simulator's
/// negative gates select at run time, `toyos-crypto`'s `portable`, `wpa2`
/// and `rustls-provider`, which `--ci host` tests as forms of its own, and
/// of the guest libraries, `toyos-font`'s `ttf`, its TrueType rendering,
/// and `toyos-pci-claim`'s `host-grant`, which netstack's host tests build.
const NOT_A_CONTROL: &[&str] = &[
"core",
"default",
"fd-owner",
"fd-table",
"flaws",
"host-grant",
"loom",
Expand Down
6 changes: 6 additions & 0 deletions build/clippy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ const SHAPES: &[Shape] = &[
before: &["-p", "toyos", "--all-targets"],
after: &["$ADOPTED", "-D", "warnings"],
},
// The copy that owns the descriptor table, as std's and C-only libc's
// build it: `fd-table`, which no other shape turns on.
Shape {
before: &["-p", "toyos", "--lib", "--features", "fd-table"],
after: &["$ADOPTED", "-D", "warnings"],
},
// Its own target directory: cargo keeps one check of a unit, so a unit
// linted under two sets of lints is checked again by each, every run, and
// so is every crate depending on it.
Expand Down
2 changes: 1 addition & 1 deletion build/libc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub const FEATURES: &str = "std-runtime";
pub(crate) const BUILD: [&str; 5] = ["build", "--release", "--features", FEATURES, "--message-format=json"];

/// What [`build_c`] asks cargo for, as [`BUILD`] is [`build`]'s.
pub(crate) const BUILD_C: [&str; 4] = ["rustc", "--release", "--crate-type", "staticlib"];
pub(crate) const BUILD_C: [&str; 6] = ["rustc", "--release", "--features", "fd-owner", "--crate-type", "staticlib"];

/// Build toyos-libc against the toolchain at `toolchain`, in `target_dir`, and
/// install it there as `libtoyos_c.a`. Part of making a sysroot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ directory is `issues/every-program-sees-only-the-files-it-was-given.md`'s.
`posix_spawnp` with file actions and attributes start a child by
`toyos::spawn`, std's launch-or-spawn rule, which std's `Command` now uses
too. A C child starts with descriptors 0–2 and exactly what its file
actions name, a stated departure from POSIX, and a spawned child's table
keeps slots 0–2 for them at its spawn (`HandleTable::for_child`): an
endowment that filled an empty one read as the child's stdio. A stdio
slot a process was started without is no descriptor of its children
(`toyos::spawn::inherited`), and std's stdio answers its errors rather
than reading every one as a closed stream (`is_ebadf`). libc
keeps close-on-exec marks (`fdreq`); `waitpid`,
actions name, a stated departure from POSIX, each endowed as
`fd:<n>`, which the child's descriptor table installs at its number
(`toyos::fd`). A descriptor a process was started without is no
descriptor of its children (`toyos::fd::copy`), and std's stdio answers
its errors rather than reading every one as a closed stream
(`is_ebadf`). libc's close-on-exec flag is the table's per-descriptor
inheritable flag; `waitpid`,
`wait` and `wait4` answer each child once, by pid, group or any, with
`<sys/wait.h>`'s status for its stage 2 end (`spawnreq`); `environ`,
`setenv` and `unsetenv`; `kill` with `SIGKILL` for a child or a group of
Expand All @@ -95,7 +95,7 @@ directory is `issues/every-program-sees-only-the-files-it-was-given.md`'s.
0); the parent's stdout marked `FD_CLOEXEC`, a 0 that `addopen` opened
`O_CLOEXEC`, and a descriptor that `adddup2` made and `addclose` closed,
each absent in the child (`F_SETFD`, `O_CLOEXEC` or `addclose` ignored,
and the child's table without its stdio slots kept); the 256th unwaited
and the child's stdio not taken from its endowments); the 256th unwaited
child refused `EAGAIN` beside a `poll` of 255 descriptors answering (no
capacity check); a negative descriptor beside a pipe, the pipe answering
(passed to the kernel, which ends the caller).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
status: open
kind: defect
opened: 2026-10-21
---

# A stdio sink cached in one copy outlives a redirect in another

`toyos::log::stdio` asks once what descriptors 1 and 2 name and caches the
answer, a log ring's mapping or the descriptor itself, in a static of each
copy of `toyos` linked into a program (`SINKS`, `sdk/toyos/src/log/stdio.rs`).
The descriptor table is one per process (`toyos::fd`), but the cache is not:
libc's `dup2` onto 1 or 2 calls `forget` only in libc's own copy, so in a
program that mixes Rust and C, std's copy keeps writing to the ring it mapped
before the redirect. Std's own `toyos::fd::dup2` forgets nothing. A
descriptor sink is asked on every write and is unaffected; only a cached ring
goes stale. Read from the code; no guest case has shown it.

**Exit**: the sink cache lives with the table's owner, or every change of
descriptor 1 or 2 is seen by every copy, and a guest case redirects a ring
stdout from C and sees std's next line land at the new target.
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 @@ -28,8 +28,8 @@ netstack would drain what the child writes as doorbells, with no error on
either side. Stage 3 makes std refuse both loudly instead.
`File::from_raw_fd` of a stream's handle panics with `toyos::net`'s refusal.
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
refusal before anything moves: a spawn's stdio descriptors,
`CommandExt::inherit_handle` as any descriptor, `CommandExt::endow` and
`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
Expand Down
20 changes: 0 additions & 20 deletions issues/libc-close-of-a-socket-ends-the-process.md

This file was deleted.

17 changes: 17 additions & 0 deletions issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
status: open
kind: defect
opened: 2026-10-01
---

# libc's close of a socket is unproven on a connected socket

A socket's descriptor names an object of libc's own in the process's one
descriptor table (`toyos::fd::Object::opaque`, `sdk/libc/src/socket.rs`), so
`close` (`sdk/libc/src/posix_io.rs`) releases it like any descriptor, and its
release closes the socket's pipes and stream and tells netstack. Before the
table, a socket's number was `1024` plus its index, `close` passed it to
`SYS_CLOSE`, and the kernel ended the caller for naming a handle it did not
hold. `close_socket` is gone. Read from the code; no guest case has run it.

**Exit**: a guest C case closes a connected socket and goes on.
21 changes: 11 additions & 10 deletions issues/the-capability-end-state-is-twelve-answers.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,18 +192,19 @@ delegation and not a widening.

## 7. What authority is implicit in process creation? — COMMITTED

A child starts with an empty `HandleTable` and gets exactly two vectors:
`slot_map` *duplicates* (the parent keeps its copy) and `endow` *moves*
(`sdk/abi/src/syscall.rs:283`). Both are all-or-nothing and refuse by name — a
slot-map pair naming a handle the parent does not hold ends the parent
(`kernel/src/loader/start.rs:308`), an endowed handle without `TRANSFER` refuses
the spawn before anything leaves the table (`:206`), and the move is the last
thing a spawn does (`kernel/src/loader/mod.rs:649`). **Two things are implicit
A child starts with an empty `HandleTable` and gets exactly one vector,
`endow`, which *moves* (`sdk/abi/src/syscall.rs`, `SpawnArgs`): its stdio
and every other descriptor are endowments labelled `fd:<n>`, which its
userland table numbers, and a console among them is minted for the child. It
is all-or-nothing and refuses by name — an endowed handle without `TRANSFER`
refuses the spawn before anything leaves the table
(`kernel/src/loader/start.rs`, `PendingHandles::commit`), and the move is the
last thing a spawn does (`kernel/src/loader/mod.rs`). **Two things are implicit
and nothing else is:**

1. a `Console` slot is *minted fresh* rather than duplicated, at the parent's
rights, because the object is the line buffer
(`kernel/src/loader/start.rs:325`);
1. an endowed `Console` is *minted fresh* rather than moved, at the moved
handle's rights, because the object is the line buffer
(`kernel/src/loader/start.rs`, `PendingHandles::commit`);
2. the spawner is handed a `Process` handle carrying
`DUP|TRANSFER|WAIT|READ|MANAGE` by type (`kernel/src/object/ops.rs:74`), and
must dup-narrow and close if it wants to hold less.
Expand Down
2 changes: 1 addition & 1 deletion issues/toyos-builds-itself.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ libc's state it is `userland/libc` at `15625e0cb`.
(`issues/the-kernel-resolves-a-path-by-rules-of-its-own.md`); and socket
descriptors that are pollable and
can be non-blocking
(`issues/libc-close-of-a-socket-ends-the-process.md`). Read from each
(`issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md`). Read from each
library's source and not run: SQLite's default VFS takes `fcntl` record
locks and keys them on `st_dev` and `st_ino`; libgit2 opens every repository
through `realpath` and reads packs through a file `mmap`; curl and libssh2
Expand Down
30 changes: 16 additions & 14 deletions kernel/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
mod start;
mod tls;

pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN};
pub use start::{build_child_handles, PendingHandles};
pub(crate) use start::{alloc_kernel_stack, KernelStack, Start};
pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT};

Expand Down Expand Up @@ -439,16 +439,18 @@ pub const SUPERVISOR_PATH: &str = "/system/bin/supervisor";
/// Panics on failure: a boot that cannot start the supervisor has nowhere to report to.
pub fn spawn_supervisor() -> Pid {
let mut handles = HandleTable::new();
let mut entries = Vec::new();
let mut labels = Vec::new();
// One console, the supervisor's descriptors 0 to 2.
let console = KObjectRef::Console(crate::object::device::ConsoleObject::new());
for slot in 0..3 {
let entry = crate::object::HandleEntry::new(
console.clone(),
ops::initial_rights(&console),
);
let (_, displaced) = handles
.install_at(slot, entry)
.expect("spawn_supervisor: three slots cannot exhaust an empty table");
assert!(displaced.is_none(), "an empty table had something at slot {slot}");
for fd in ["0", "1", "2"] {
let entry = crate::object::HandleEntry::new(console.clone(), ops::initial_rights(&console));
let handle = handles.install(entry).expect("spawn_supervisor: three handles cannot exhaust an empty table");
let label_off = labels.len() as u32;
labels.extend_from_slice(toyos_abi::syscall::FD_PREFIX.as_bytes());
labels.extend_from_slice(fd.as_bytes());
let label_len = labels.len() as u32 - label_off;
entries.push(toyos_abi::syscall::EndowEntry { label_off, label_len, handle, _pad: 0 });
}
let cap = KObjectRef::SysCap(crate::object::syscap::SysCap::new());
// Every machine-wide authority the system has: rights only shrink from
Expand All @@ -470,13 +472,13 @@ pub fn spawn_supervisor() -> Pid {
.install(crate::object::HandleEntry::new(cap, rights))
.expect("spawn_supervisor: an empty table refused the system capability");
let label = toyos_abi::syscall::SYSCAP_LABEL;
let mut entries = alloc::vec![toyos_abi::syscall::EndowEntry {
label_off: 0,
entries.push(toyos_abi::syscall::EndowEntry {
label_off: labels.len() as u32,
label_len: label.len() as u32,
handle: cap_handle,
_pad: 0,
}];
let mut labels = label.as_bytes().to_vec();
});
labels.extend_from_slice(label.as_bytes());
// Built by the kernel and owing nobody anything: no table but the supervisor's own holds it.
let commit = |own| {
start::endow_self(&mut handles, &mut entries, &mut labels, own);
Expand Down
Loading