Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
672f04b
A parent's end takes its children down
Japabu Oct 1, 2026
b04de48
process_tree judges sshd's end by the kernel's exit line
Japabu Oct 1, 2026
6c38904
Merge origin/main (#635, #652) into wt/toyos-proclife4
Japabu Oct 1, 2026
81ff550
build: the boot-config gates cover tests/proctreecase
Japabu Oct 1, 2026
90a9937
Merge remote-tracking branch 'origin/main' into wt/toyos-proclife4
Japabu Oct 1, 2026
2d53ac3
Answer the first review of #659: a spawn past its admission lands, an…
Japabu Oct 1, 2026
6cb8071
process_tree: B faults by a volatile write to address 0
Japabu Oct 1, 2026
80be946
Merge origin/main (#639) into wt/toyos-proclife4
Japabu Oct 1, 2026
cc49e13
A refused spawn spends no pid, the last pid is refused by name, and a…
Japabu Oct 1, 2026
11e57e2
Guest arms for the endowment vector's refusals and a launch's place, …
Japabu Oct 1, 2026
f3b6d22
toyos-proclife: the crate doc names pids among its questions
Japabu Oct 1, 2026
d69b80a
kernelkeys: the process table's own HashMap is declared, keyed by ker…
Japabu Oct 1, 2026
d62803b
abuse_handle_table closes what dup2 answered, not the bare slot index
Japabu Oct 2, 2026
0ee4b44
spawn_lands_claimed: a guest test reaches the kernel's claimed-as-it-…
Japabu Oct 2, 2026
ad21ebf
pkg_launch_gbae asks init to be gbae's parent
Japabu Oct 2, 2026
54c375a
Merge origin/main (de5f63cc2) into wt/toyos-proclife4
Japabu Oct 2, 2026
94d22fe
Merge origin/main (46af79d5d: #650, #653) and pin std over the commit…
Japabu Oct 2, 2026
1e9cb8c
issues: pkg_install_gbae's client is recorded in a file of its own
Japabu Oct 2, 2026
6b483b7
Pid is no IdKey and adds to nothing
Japabu Oct 2, 2026
757fe9f
tests: process_reopen's doc heads process_reopen again, and the proct…
Japabu Oct 2, 2026
517d8c1
issues: a launch past one frame becomes a direct spawn, and the shell…
Japabu Oct 2, 2026
3fd1419
Merge origin/main (80bc97b41: #643) into wt/toyos-proclife4
Japabu Oct 2, 2026
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
25 changes: 25 additions & 0 deletions issues/build/a-metal-judge-takes-a-names-lowest-pid-for-the-job.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
status: open
kind: tooling
opened: 2026-10-02
---

# A metal judge takes a name's lowest pid for the job

`Readback::exit_code` (`tests/common/metal.rs`) reads a job's exit off the
record with the lowest pid among those of its name: a binary that re-executes
itself leaves one record per child under that name, and the runner spawned the
job before the job spawned anything.

The kernel does not issue pids in landing order. A spawn refused after its
admission gives its pid back, and the next admission takes it
(`toyos_proclife::pids`). A spawn admitted before a job and refused after the
job landed hands a pid below the job's to the next spawn, and if that is the
job's child of the same name the judge reads the child's exit as the job's.

Evidence: by reading. No boot has shown it: the runner starts one job at a
time, so it takes another process inside `SYS_SPAWN` across a job's start.

Exit: the judge finds the job's record by something only the job has (the pid
the runner records for it), and a host test over a log in which a child holds
the lower pid reds the lowest-pid rule. Owner: the harness.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
status: open
kind: tooling
opened: 2026-10-02
---

# pkg_install_gbae's launch client exits over a child that ends with it

`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` brings
`pkg_install_gbae` back as a metal row, from `main` before the cut. Its client
there, `pkg_launch_gbae`, launches gbae and exits at once. A launched program
ends with its launcher, so gbae ends as killed before the compositor counts its
window.

Evidence: under QEMU before the cut, `cargo test --test toyos-build --
pkg_install_gbae` exited 1 with the kernel's `exit: gbae pid=21 code=137`
beside the client's own exit, and 0 with the client's launch made
`Command::new(PROGRAM).under_init()` (commit ad21ebf3a).

Exit: the row's client starts gbae under init, and the row is green on the T14.
Owner: the guest-suite track's stage D.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-10-02
---

# The shell says "not found" for every spawn error

`execute_simple` and `execute_pipeline` (`userland/shell/src/main.rs`) match
the error of `Command::spawn` and `Command::status` as `Err(_)` and print
`<program>: not found`. A spawn refused for its depth, one under a place being
torn down and one refused `PermissionDenied` all read to the user as a program
that is not there.

Evidence: the T14's `proctreecase` boot at 1e9cb8c77. The kernel recorded
`spawn: refused under pid 79 at depth 65, more than 64 below init`, and the
shell at that depth printed `/system/bin/test_rs_process_tree: not found`.
`process_tree`'s chain (`tests/toyos-rust-tests/src/bin/process_tree.rs`)
accepts that line as its stop, and the row's judge reads the kernel's record
for the depth.

Exit: the shell prints the error the spawn answered, as its `detach` does, and
says `not found` for `ErrorKind::NotFound` alone; `process_tree`'s chain no
longer accepts `: not found` as its stop. Owner: the shell.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
status: open
kind: defect
opened: 2026-10-02
---

# A launch too large for one frame becomes a direct spawn

`toyos::launch::launch` (`toyos/src/launch.rs`) answers `LaunchError::NotSent`
when `Launch::encode` refuses the request, and std's `Command::launch`
(`rust/library/std/src/sys/process/toyos.rs`) answers `NotSent` with the direct
spawn. `encode` refuses a request whose header, program, argv, environment,
working directory and connector names do not fit `MAX_FRAME_LEN`
(`toyos/src/ipc.rs`). Its other refusal, too many connectors or slots, std
never reaches: it refuses the connectors itself, and a launch carries stdio
alone.

So a declared program started with an argv and environment past one frame is
not launched. Its caller spawns it: it holds its caller's namespace and not its
manifest row, a connector the caller `provide`d does not reach it, and it
carries no `HOME` unless its caller named one. Neither side is told. A child
asked of init (`under_init`) is refused `PermissionDenied` instead.

Evidence: by reading. No test sends a launch past one frame.

Exit: a launch `encode` refuses answers its caller an error and starts nothing,
or the wire carries every argv and environment `SYS_SPAWN` takes; a test
launching a declared program with an environment past one frame reads that
error, or finds the child holding its row. Owner: the std lane, with
`toyos/src/launch.rs`.
Original file line number Diff line number Diff line change
Expand Up @@ -117,57 +117,6 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md
the pipe answering (passed to the kernel, which ends the caller). Negative
control: today's libc, against which the corpus does not link. Oracle:
POSIX's `posix_spawn`, `waitpid`, `poll` and `SA_RESTART`, and signal(7).
4. **A parent takes its children down**. A spawn's parent is its
spawner. A launch names its parent in its request, by one of two words: the
caller, whose place is a copy of the handle to itself every process starts
holding under the label `self` (`WRITE`, `DUP`, `TRANSFER`), carried as one
of the batch's five extras (`toyos/src/launch.rs`); or init, the one way to
outlive a starter, which std's ToyOS `CommandExt` asks for and the shell
gains a way to use; std answers a request for init it cannot launch
`PermissionDenied`, as it answers a `provide` with no launcher, and spawns
nothing. init spawns under the place, which `SpawnArgs` gains (112 bytes to
120). The place and `SYS_NAMESPACE_BUILD`'s connector are each a handle a
peer sent, and one lookup resolves both, answering a wrong type
`InvalidArgument` rather than ending the caller: `HandleError::WrongType`
declares that exception once, for that lookup, and the connector's own match
in `kernel/src/syscall/ipc.rs` goes. The kernel keeps each process's parent,
children and depth on its table entry, answered by no syscall. A spawn that
would put a process more than `toyos_proclife::MAX_DEPTH` (64) below init,
counted from the place, is refused `ResourceExhausted`, and the kernel logs
a depth refusal naming that depth. Every end — exit, kill, CPU fault,
handle fault — closes admission at its top and claims its subtree on the
ending or killing thread, one process per table-lock hold, calling
`scheduler::yield_now` between two claims, with nothing held, whenever a
reschedule is owed. An end is published once its teardown is done and every
child's end is, by a count admission raises and a child's publication or
refused insert lowers, so the climb from the last teardown, run with
preemption off, is at most `MAX_DEPTH` publications. Decided in
`toyos-proclife`; init's part goes where launch resolution is by then
(`issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md` stage
2). *Exit*: host — `toyos-proclife`'s interleavings of a spawn racing its
place's kill land nothing under it, end all under it and publish it in every
one; a spawn under an unrelated process lands between two claims of one
walk; a chain is refused at `MAX_DEPTH` + 1 below init. Guest — A starts B,
B starts C and launches D: killing B ends all three, read as killed, and A's
watch on B completes after C's and D's; so do B's exit, a CPU fault and a
handle fault in B; a spawn or launch under B after its kill answers `Gone`;
a chain alternating spawn and launch, each process starting the next, stops
where the kernel's depth refusal names `MAX_DEPTH` + 1, and dies whole with
its first; a launch that neither carries a place nor asks for init is
refused and nothing starts, and so is a request for init to start an
undeclared program or carrying an extra slot; a launch whose place is a
pipe is refused and init answers the next; a `MANAGE`-only place answers
`PermissionDenied`; sshd launched from a shell dies with it, and with init
as its parent outlives it. Negative control: the stage reverted whole, where
C and D outlive B. Mutations: a walk that skips a launched child (D); a
publication before the children's (A's watch); a walk from `sys_exit` alone
(the faults); the walk in one lock hold (the interleaved spawn); the count
kept on a refused insert (B unpublished); the place looked up as any handle
(init ends); no depth check, and a depth counted from the spawner rather
than the place (the chain, which no depth refusal stops); a launch with no
place spawned under init (it starts); std's `NotDeclared` fallback, or its
early return for an endowment or extra slot, kept for init (it starts).
Oracle: cgroup v2's `cgroup.kill` and `cgroup.max.depth`.
5. **A login is a session under init**. Per login init starts a session
program that logout ends and that only parents what its user starts — the
desktop's at boot, one per SSH connection at sshd's request. As it starts
Expand All @@ -182,7 +131,7 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md
6. **A program is asked to quit**. `SYS_PROCESS_QUIT` (124, never
assigned), `(process, reason)`, needs `MANAGE` as the kill does, and the
reason is interrupt, hang-up or terminate. A quit reaches the process's
subtree by stage 4's walk. Each process is started holding its notice, a new
subtree. Each process is started holding its notice, a new
object kind under the label `quit`, `READABLE` while a reason is asked and
not yet read, whose read takes it. A quit kills a process that has never
watched its notice or whose notice still holds a reason, whoever asks, in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
status: open
kind: defect
opened: 2026-10-01
---

# A process that starts four billion threads panics the kernel

`process::spawn_thread` takes a thread's id from its process's
`IdMap<Tid, ThreadEntry>` (`kernel/src/id_map.rs`), whose `insert` steps its
counter with `Tid + Tid` (`toyos-abi/src/lib.rs`). The kernel builds with
`overflow-checks = true` (`kernel/Cargo.toml`), so the 2^32nd thread a single
process starts panics the kernel, and it does so under `PROCESS_TABLE`, which
hangs the machine instead of reporting. The thread before it is issued
`Tid(u32::MAX)`, which is the per-CPU word for no thread
(`arch::x86_64::percpu::current_tid`), so `handle_page_fault` refuses every
fault that thread takes.

Userland reaches it by starting and joining threads in a loop; only a
successful start spends an id, so it costs 2^32 thread starts, never measured.
A pid had the same shape until `toyos_proclife::pids` (stops below `Pid::MAX`
and refuses the next spawn by name); a thread id needs the same: never
`Tid::MAX`, and a refused start once the ids are spent.

*Exit*: a host test of the thread-id allocator answering a refusal after
`Tid(u32::MAX - 1)`, and `SYS_THREAD_SPAWN` answering it as a refusal.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-10-01
---

# A spawn refused for its caller's full table has already moved its endowments

`sys_spawn` (`kernel/src/syscall/proc.rs`) installs the new child's `Process`
handle in its caller's table after `loader::spawn` returns, which is after
`PendingHandles::commit` (`kernel/src/loader/start.rs`) moved the endowed
handles out of that table and the child landed. When the install finds the
table full, the spawn kills the child and answers `ResourceExhausted`: a
refusal after the caller's handles left, and a child that may have run user
code before its retire landed. Every other refusal of a spawn leaves the
caller's table as it was.

The moves free one slot per endowment, so with endowments it takes another
thread of the caller filling the table between the commit and the install;
with none, a caller whose table is full when it spawns. Neither is measured.

*Exit*: the child's handle goes into the caller's table in the commit's hold,
with the room checked before anything moves; a guest arm spawning from a
full table reads `ResourceExhausted` and no child started.
4 changes: 0 additions & 4 deletions kernel/src/id_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ pub trait IdKey: Copy + Eq + Hash + Ord + Add<Output = Self> {
}

// No impl for u32/u64/usize: that would let a bare integer key an IdMap.
impl IdKey for toyos_abi::Pid {
const ZERO: Self = Self(0);
const ONE: Self = Self(1);
}
impl IdKey for toyos_abi::Tid {
const ZERO: Self = Self(0);
const ONE: Self = Self(1);
Expand Down
83 changes: 47 additions & 36 deletions kernel/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::object::{ops, HandleTable, KObjectRef};
use crate::mm::policy::{CachePolicy, Prot};
use crate::mm::{PAGE_2M, PAGE_BYTES};
use crate::process::{
ElfInfo, Endowments, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Pid,
Admission, ElfInfo, OwnedAlloc, PageAlloc, PageFaultTrace, PageTables, Parent, Pid,
ProcessAccounting, ProcessData, ProcessEntry, ThreadData, ThreadEntry, UserStack,
PROCESS_TABLE,
};
Expand Down Expand Up @@ -335,11 +335,8 @@ fn rela_dyn_from_sections(
}
}

/// Load a program and place its main thread, answering the object a handle to
/// the new process names.
///
/// No parent argument: a process has no parent, and the only thing the caller
/// contributes beyond its endowment is the working directory it passes in.
/// Load a program and place its main thread under `parent`, answering the object
/// a handle to the new process names.
///
/// `image` is the program's bytes when the caller read them itself, and then
/// `argv[0]` is only its name: nothing opens it, and its libraries come from
Expand All @@ -355,11 +352,14 @@ pub fn spawn(
cwd: String,
env: Vec<u8>,
image: Option<Arc<dyn crate::file_backing::FileBacking>>,
parent: Parent,
) -> Result<Arc<crate::object::process::ProcessObject>, crate::object::Refusal> {
// An argv of only separators survives sys_spawn's split as an empty slice.
let Some(&path) = argv.first() else {
return Err(SyscallError::InvalidArgument.into());
};
// Before anything is built; dropped on every way out below but the insert.
let admission = Admission::ask(parent)?;
let t0 = crate::clock::nanos_since_boot();

let from_image = image.is_some();
Expand Down Expand Up @@ -586,10 +586,11 @@ pub fn spawn(


let NeededLibs { libs: loaded_libs, paths: lib_paths } = loaded_libs;
let pid = admission.pid();
let object = crate::object::process::ProcessObject::new(pid);
// The point of no return: every failure above answers the caller with its
// table untouched. `commit`'s own `?` is different — reachable only if the
// caller raced its own spawn, and fatal to it, not a refusal.
let (handles, endowments) = pending.commit()?;
// table untouched.
let (handles, endowments) = pending.commit(KObjectRef::Process(Arc::clone(&object)))?;
let proc_data = Arc::new(Lock::new(ProcessData {
handles,
cwd,
Expand Down Expand Up @@ -636,30 +637,39 @@ pub fn spawn(
// One table, two holders: cloned so a crash report on this thread reads names without the process table.
let syms = Arc::new(syms);

#[cfg(feature = "test-actuators")]
crate::process::debug_kill_marked_place(parent);

let mut guard = PROCESS_TABLE.lock();
let table = guard.as_mut().unwrap();
let pid = table.insert_with(|pid| ProcessEntry::new(
pid,
start::make_name(path),
proc_data,
Arc::clone(&syms),
ThreadEntry::new(thread_data),
));
let tid = table.get(pid).unwrap().main_tid();
let object = Arc::clone(table.get(pid).unwrap().object());

// Placed while still holding the table lock: kill_process claims teardown
// under it, so a retire sweep can never see the pid before its thread is scheduled.
let (sched, dst) = scheduler::enqueue_new(
scheduler::TaskId(pid, tid),
ks_alloc,
ks_sp,
child_pt.clone(),
thread_pointer,
syms,
);
table.get_mut(pid).unwrap().threads_mut().get_mut(tid).unwrap().set_sched(sched);
let ((tid, dst), retire) = admission.land(guard.as_mut().unwrap(), |table, node| {
table.insert(ProcessEntry::new(
Arc::clone(&object),
start::make_name(path),
proc_data,
Arc::clone(&syms),
ThreadEntry::new(thread_data),
node,
));
let tid = table.get(pid).unwrap().main_tid();
// Placed while still holding the table lock: kill_process claims teardown
// under it, so a retire sweep can never see the pid before its thread is scheduled.
let (sched, dst) = scheduler::enqueue_new(
scheduler::TaskId(pid, tid),
ks_alloc,
ks_sp,
child_pt.clone(),
thread_pointer,
syms,
);
table.get_mut(pid).unwrap().threads_mut().get_mut(tid).unwrap().set_sched(sched);
(tid, dst)
});
drop(guard);
// Its parent was claimed while it was built, and its walk has passed: the
// child is ended as that walk would have ended it, and the spawn answers it.
for sched in &retire {
scheduler::post_retire(sched);
}

let t3 = crate::clock::nanos_since_boot();
log!("spawn: {} pid={} tid={} dst={} base={:#x} entry={:#x} root={:#x} symbols={}KiB (layout={}ms relocs={}ms deps={}ms tls={}ms total={}ms)",
Expand Down Expand Up @@ -876,16 +886,17 @@ pub fn spawn_init() -> Pid {
.install(crate::object::HandleEntry::new(cap, rights))
.expect("spawn_init: an empty table refused the system capability");
let label = toyos_abi::syscall::SYSCAP_LABEL;
let endowments = Endowments::new(
alloc::vec![toyos_abi::syscall::EndowEntry {
let pending = PendingHandles::Ready {
table: handles,
entries: alloc::vec![toyos_abi::syscall::EndowEntry {
label_off: 0,
label_len: label.len() as u32,
handle: cap_handle,
_pad: 0,
}],
label.as_bytes().to_vec(),
);
match spawn(&[INIT_PATH], PendingHandles::Ready(handles, endowments), String::from("/"), Vec::new(), None) {
labels: label.as_bytes().to_vec(),
};
match spawn(&[INIT_PATH], pending, String::from("/"), Vec::new(), None, Parent::Root) {
Ok(object) => object.pid(),
Err(crate::object::Refusal::Error(e)) => panic!("spawn_init: failed to spawn: {e:?}"),
Err(crate::object::Refusal::Handle(e)) => panic!("spawn_init: {e}"),
Expand Down
Loading
Loading