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
21 changes: 11 additions & 10 deletions build/bootlog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,15 @@ pub fn kernel_records(log: &str) -> String {
/// trailing ` online` as a separate word: the same head carries the failure.
pub const AP_BRINGUP: &str = "SMP: AP cpu";

/// `kernel/src/process.rs`'s `THREAD_NAME_LEN`, one byte of which is the
/// terminator `make_name` leaves.
const NAME_LEN: usize = 28;

/// A process's name as the kernel records it: the path's last component,
/// truncated to what [`NAME_LEN`] holds.
/// The roster's name field, one byte of which is the name's terminator.
const NAME_LEN: usize = toyos_abi::inventory::NAME_BYTES;

/// The name the kernel records a process run as `binary` under: what its
/// parent's SDK states at the spawn (`toyos::spawn::name` in
/// `sdk/toyos/src/spawn.rs`), which this agrees with for every name of
/// ASCII letters, digits and punctuation: the last component, cut to what
/// [`NAME_LEN`] holds. That function cuts at a character and writes `_` for
/// white space and a control character, which this does not.
///
/// **A predicate looking for the whole name finds nothing on a good boot**:
/// `test_rs_lent_device_memory_refused` is `test_rs_lent_device_memory_` on the wire.
Expand Down Expand Up @@ -499,16 +502,14 @@ mod tests {
assert_eq!(stop_marked(""), Err(Unfit::NoStopMark));
}

/// The two kernel spellings a metal readback is judged on, and the length
/// it truncates a name to — held to the kernel's own source, because
/// nothing links this crate to it either.
/// The kernel spellings a metal readback is judged on, held to the
/// kernel's own source, because nothing links this crate to it.
#[test]
fn the_kernel_writes_the_records_the_host_reads() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for (file, needle) in [
("kernel/src/process.rs", format!("log!(\"{EXIT}{{name}} pid=")),
("kernel/src/arch/x86_64/smp.rs", format!("log!(\"{AP_BRINGUP}")),
("kernel/src/process.rs", format!("THREAD_NAME_LEN: usize = {NAME_LEN}")),
("kernel/src/deadline.rs", format!("\"{DEADLINE_ARMED}{{ms}} ms")),
] {
let at = root.join(file);
Expand Down
4 changes: 2 additions & 2 deletions build/kimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! The kernel is ours and linked by this build, so anything here that it
//! could not be is this build's bug, and panics naming it.

use toyos::elf::program::{lay_out, BASE};
use toyos::elf::program::lay_out;
use toyos::elf::rela::{self, Op, RelaTable, Rules};
use toyos::elf::section::{symbol_tables, SectionTable};
use toyos::elf::sym::{SymTab, STT_FUNC};
Expand Down Expand Up @@ -80,7 +80,7 @@ pub fn section(kernel: &[u8], supervisor: &[u8], arch: Arch) -> Vec<u8> {
}
}

let program = lay_out(supervisor, machine, SUPERVISOR_OBJECT, BASE).unwrap_or_else(|why| panic!("supervisor: {why}"));
let program = lay_out(supervisor, machine, SUPERVISOR_OBJECT).unwrap_or_else(|why| panic!("supervisor: {why}"));
Input {
machine: e_machine,
entry: min + layout.entry().get(),
Expand Down
9 changes: 6 additions & 3 deletions docs/src/architecture/kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ The kernel is small, fast and safe, and does what userland cannot:

It holds no filesystem, no storage, input, network, audio or USB driver, no
network protocol, no audio mixing, no login and no naming policy, and parses
nothing one program writes for another. The firmware's AML is interpreted by a
userland ACPI service, which tells the kernel the little it needs, such as how
to power off.
nothing one program writes for another. What a parent tells the child it
starts, its arguments and environment, is memory the parent lays out in the
child: the kernel keeps none of it. It knows a process by a short name its
parent gives it, a label that carries no authority. The firmware's AML is
interpreted by a userland ACPI service, which tells the kernel the little it
needs, such as how to power off.

## Interrupts post, threads wait

Expand Down
13 changes: 11 additions & 2 deletions docs/src/using/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Every system call is one of these families:
| Own address space | Reserve, commit, map, protect and release memory |
| Handles | Duplicate with narrower rights, close, move |
| Waiting | Watch any set of objects for readiness, and wait |
| Processes | Spawn from an image with endowments; wait for and stop a child |
| Processes | Spawn from memory ranges the parent lays out, with endowments; wait for and stop a child |
| Communication | Pipes, service ports, connections carrying bytes and handles, inboxes |
| Shared and paged memory | Create, map and lend memory; supply pages as a pager |
| Devices | Claim through the system capability, map device memory, grant DMA, wait for interrupts |
Expand Down Expand Up @@ -62,9 +62,18 @@ userland library over the SDK.
endowments ──> standard streams (descriptors 0, 1, 2 and those named)
──> a namespace of services it may connect to
──> directory capabilities: its view
──> environment: HOME, the starting directory, ...
──> anything else its row or its parent gave it

its memory ──> its program, in the ranges its parent laid out
──> a startup block below it: its arguments and its
environment (HOME, the starting directory, ...)
```

A parent reads the child's program and lays out the child's memory. What it
tells the child goes in the startup block, which the kernel copies as it
copies the program and never reads; the SDK, std and libc read it where it
lies. A child whose parent wrote none starts with no argument and an empty
environment.

A program holds nothing it was not given. It asks for more only by connecting
to a service in its namespace, whose server decides.

This file was deleted.

22 changes: 11 additions & 11 deletions issues/a-spawn-drops-an-empty-argument.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@ kind: defect
opened: 2026-10-20
---

# A spawn drops an empty argument and refuses one that is not UTF-8
# A spawn drops an empty argument

`SYS_SPAWN` reads `argv` as one `NUL`-separated string with `user_str`, which
refuses bytes that are not UTF-8 (`InvalidArgument`), and splits it with
`text.split('\0').filter(|s| !s.is_empty())` (`kernel/src/syscall/dispatch.rs`).
So a child is never handed an empty argument, and a program asked to run
`cmd "" x` sees `cmd x`; and an argument C or std carries as bytes, which POSIX
and `OsStr` allow, refuses the spawn. Read from the code while building
A spawn's `argv` is one `NUL`-separated run of bytes (`toyos::spawn::Spawn`),
which the startup block's writer splits skipping every empty string
(`toyos::startup::Block::new`), as the kernel did while it copied `argv`. So a
child is never handed an empty argument, and a program asked to run
`cmd "" x` sees `cmd x`. An argument that is not UTF-8 reaches the block and a
C child as given, and a Rust child's `std::env::args_os` as an empty one
(`sdk/std/sys/args.rs`). Read from the code while building
`posix_spawn` (stage 3 of
`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`), not
reproduced: `tests/guest/libc` passes `""` only where the child reads no
argument. Build tools pass empty arguments (`make`'s recipes, `sh -c ''`).

A launch through the supervisor carries the same blob (`toyos::launch`'s
request names it "exactly the blobs `SYS_SPAWN` takes") and loses the same
arguments before any spawn: `serve_launch` (`system/supervisor/src/main.rs`)
A launch through the supervisor carries the same blob (`toyos::launch`) and
loses the same arguments before any spawn: `serve_launch` (`system/supervisor/src/main.rs`)
splits `request.argv` on `NUL`, filters out every empty argument, and skips an
argument that is not UTF-8 without refusing the launch, so a launched child
sees neither.

*Exit*: a child spawned with `argv` `["a", "", "b"]` and with a non-UTF-8
argument reads both as given, in a guest test, red with the kernel's filter put
argument reads both as given, in a guest test, red with the writer's filter put
back; and a host test of the supervisor's decode of a launch request's `argv`
keeps an empty and a non-UTF-8 argument, red with its filter put back.
The blob needs a representation that says where an argument ends without
Expand Down
59 changes: 59 additions & 0 deletions issues/a-spawns-record-quotes-the-path-its-caller-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
status: open
kind: defect
opened: 2026-11-11
---

# A spawn's record quotes the path its caller states

`SYS_SPAWN` takes the program's path as a label, any UTF-8 up to 64 KiB, which
nothing opens and nothing checks but for UTF-8 (`SpawnArgs::path_ptr`, read by
`user_str` in `kernel/src/syscall/dispatch.rs`). The kernel writes it into its
own records as it came: `spawn: {path} pid=N (total=Nms)` for a spawn that
lands and `spawn: {path}: …` for each one it refuses
(`kernel/src/loader/mod.rs`), so the spawn need not succeed. A crash frame
escapes the same text (`toyos_symbols::frame`); these records do not.

A program that may call `SYS_SPAWN` can therefore have the kernel write, three
ways:

1. **A whole forged line.** Nothing between the call and `/log` touches a
control character: `Message::write_str` (`kernel/src/log/mod.rs`) copies
the text, `LogRecord`'s `Display` (`sdk/abi/src/log.rs`) writes the message
raw, and `logkeeper` writes that (`system/logkeeper/src/main.rs`). A path of
`x`, a newline and `[2026-11-11 10:00:00 1.000 cpu0 kernel] exit:
test_rs_job pid=1 code=0 cpu=0ms` puts a line in `/log` that
`toyos_logstream::parse` reads as a kernel record. `Readback::exit_code`
(`tests/common/metal.rs`) takes the first `pid=` and `code=` after the
job's name and keeps the record with the lowest pid: the forged line is a
test's verdict. The same line can wear any program's head.
2. **A record that reads as another program's start**, by stating that
program's path. A child labelled `/system/bin/logkeeper` is recorded
`spawn: /system/bin/logkeeper pid=N`, the head `bootlog::LOGKEEPER_SPAWN`
and the audio judges' `spawn: /system/bin/{job} ` (`tests/common/audio.rs`)
look for. The label holds nothing to escape.
3. **A record carrying another record's words on its own line**: a space,
`pid=7`, or a second `spawn: `, which a judge that searches a line for a
head finds (`tests/toyos.rs`, `tests/common/claims.rs`,
`tests/common/qemu.rs`).

By reading, not run. The name beside the path is refused at spawn unless it is
one bounded token (`toyos_abi::syscall::process_name`), which a record's head
never fitted in; the path has no such rule. Every reader of these records is a
judge in `tests/` or `build/`, or a person: nothing in `system/`, `apps/`,
`sdk/` or `lib/` decides by one.

Owner: the kernel track
(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), whose
record names the path as userland text the kernel still keeps.

**Exit**, the orchestrator's decision (2026-11-11, on #1200), in two parts:

- (a) The spawn boundary refuses a path label carrying a control character,
the rule the name has, and `abuse_spawn_path_and_name` holds it: a path
with a newline is `InvalidArgument`, red on the kernel that takes it. That
closes way 1.
- (b) No judge in `tests/` or `build/` decides by a `spawn:` record's path,
since it is its caller's label: each reads what the kernel itself knows of
the child, or the program's own lines under the head the supervisor
registered. That closes ways 2 and 3.
8 changes: 8 additions & 0 deletions issues/no-user-address-is-drawn-per-spawn.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ draws 32 bits for the image and the mmap base (`CONFIG_ARCH_MMAP_RND_BITS`,
three bases is set 88 to 168 times, and each base claims Linux's bits or its
shortfall is filed as a defect. **Mutation**: `BASE + n * 2 MiB`; a constant
seed. **Oracle**: Linux's bits.

A fourth address is constant since #1200: the startup block
(`toyos::startup::AT`), below the image. It is the only one of the four whose
bytes another party chooses: writable, up to 2 MiB, written by the parent,
and in a launched program holding strings of a caller with less authority than
the row. **Exit**: the block has no address of its own. It moves with the
image, found by every SDK copy through the process's module list
(`toyos::tls`), and the exit above counts its base with the other three.

This file was deleted.

Loading
Loading