Repository navigation
SYS_PROCESS_OPEN is deleted, 110 is free, and a spawn mints its child's own handle before its caller's - #642
Conversation
Stage 0 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md, ruled by the owner: the `Process` handle is the only control, so the one call that turned a pid into one is deleted with every name only it reached. - `sys_process_open`, its dispatch arm, `toyos_abi::syscall::process_open` and `SysCap::open_process` go. 110 enters `retired_syscalls!`, so a call of it answers `NotSupported` and the kernel logs "syscall 110 is retired (formerly SYS_PROCESS_OPEN)"; the ABI keeps the number as a comment, as it does 8, 85, 87 and 107. - `Rights::MANAGE` leaves init's `SysCap`: on a `SysCap` it opened a process by pid and nothing else. It stays a `Process` handle's right to kill. - The `kobject!` `sealed`/`reopenable` column goes. `Process` was its one `reopenable` row, there only because 110 could mint a handle after the last one had gone; with nothing left that installs a `Process` object but the spawn that made it, every row retires on its last handle and `HandleEntry` is byte-identical to its form before 1ee9ec9. - `process::process_object`, `object::process::reopen_selftest`, `sched::kthread::open_selftest`, the `process-reopen-selftest` actuator, its `process_reopen_selftest` machine test and judge, its metal row, its `FLASHABLE` row and its recorded duration go. The two in-kernel controls measured `process_object`'s answers, and nothing asks that question now. - `process_lifecycle`'s pid arm, the only caller, now asks 110 with the arguments it took (this process's capability and its own pid) and asserts `NotSupported`; `check_process_lifecycle` reads the kernel's retired record. `wait_raw` becomes `raw(num, a1, a2)`, which both raw arms share. - `issues/kernel/the-capability-end-state-is-twelve-answers.md` (questions 3 and 10, and the enforced-rulings paragraph) and `issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md` (stage 2) no longer argue from 110, and the track's stage 0 is deleted. `git grep` for sys_process_open, process_open, open_process, reopenable, process_object, reopen_selftest, open_selftest, process-reopen, process_reopen and process-open-kthread finds only `toyos-symbols/tests/fixtures/input-test.bin`, whose symbols are frozen test data; `SYS_PROCESS_OPEN` is left only as the retired table's name for 110. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Orchestrator runs at a8acd2a (logs
|
|
Review of #642, round 1, at a8acd2a Gate. CI Size. +103 −297 in all. Production ( The brief's questions
BLOCKER
NOTE
REMOVE
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The owner ruled that the ABI is completely unstable: no stable ABI exists yet, and toyos-abi and the SDK take breaking changes freely until ToyOS is adopted. A deleted syscall is deleted and its number is free, so nothing here retires 110. - 110 leaves `retired_syscalls!` and the ABI's retired-number comment goes. The table itself stays; removing it tree-wide is a separate change. - process_lifecycle's pid arm is deleted. A call that no longer exists needs no runtime test: every caller of the deleted names fails to compile. The other arms stay. `wait_raw` is its base form again, so no second raw-syscall entry is left in the binary (review B1). - check_process_lifecycle, its record constant and its check_for row are deleted (review B2). No run had seen the record half red, and the record could reach the wire after the runner's window had closed. - Review NOTEs and REMOVEs: the trailing blank line at the end of kernel/src/object/process.rs goes. In the-capability-end-state-is-twelve-answers.md, the paragraph on which rulings are enforced in code goes, and so does question 3's sentence on retired pid-addressed numbers. The test loses its count of the arms that take the cap, the arm's history comment, and the clauses of the module doc and the closing line that spoke for the deleted arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…d the defects the cut's tests found - `.claude/agents/reviewer.md`: **Growth** said tests are cut only when they test nothing, which sends back every cut the ladder makes. It now reads "a test is cut only when it tests nothing, or as **Guest tests** says", and **Guest tests** says when: a cheaper tier already holds the behaviour, named in the pull request body, or a stage of a track names it in the same diff with an exit a build or test can fail. A guest test re-argues the tiers when it is new or its behaviour changes, not on every edit. - The track carries owed work only: the verdict counts, the keep list and both cut lists go to this pull request's body, and stage M goes, its shard, duration and phase half done here and the rest naming nothing. It names its owner. The tests main redlists are not its items: each is red and its issue holds it. `late_storage_connect` stays metal, staged by its actuator rather than a plug; `https_tls13_e1000e` and `blackbox_early_panic_sealed_muted` join the rows that cover them; `metal_sim_input` and `virtio_used_ring` become host items and `query_pci_agreement` a metal one against Ubuntu's `lspci`; FPU isolation goes first in stage C; and each item whose arm needs a deleted feature or file says so. - Defects #654 recorded on main, carried whether or not their test survives: `a-ready-marker-read-off-the-16550-file-can-end-the-boot-wait-mid-line.md` (`root_candidate_malformed`), `a-test-asserts-a-daemons-line-off-a-boot-log-that-ends-before-it.md` (`lan_dhcp_lease` and `iommu_virtio_platform`, one mechanism), `an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md` (`virt_smp`), `blockd_serves_partitions` folded into `qemu-drops-console-output-the-harness-is-slow-to-read.md`, and `process_stats`'s two assertions with an exit a test can fail. `redirty_mid_flush`'s red was #642's own and is not carried. - `sys-debug-actions-and-two-loader-words-that-nothing-calls.md` records the ABI names the cut left with no caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Six conflicts, each resolved by taking main's side and applying this branch's deletion to it again: - kernel/src/actuator.rs: main deleted the rows this branch kept around `process-reopen-selftest` and kept that row; the row goes. - kernel/src/syscall/dispatch.rs: main's import list gained `spawn_place`; `sys_process_open` leaves it. - src/metal.rs: `FLASHABLE` is a list of names on main; the `process-reopen-selftest` name goes. - tests/toyos.rs: main moved the QEMU machine test out, so what is left to delete is the metal row, the `SELFTESTS` arm, the `process_reopen` judge and the two counts of that image's actuators. - tests/test-durations: main deleted the file; this branch's one hunk removed a row of it, and goes with it. - the track file: main reworded stage 0 and stage 1; stage 0 is deleted and stage 1 is main's. kernel/src/object/handle.rs and kernel/src/object/mod.rs merge clean, so the `Process` row is sealed again over #659's spawn, whose two installs on a child's object are not ordered for that. The commits after this one make that red and then remove it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…s for it `debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS` (23) marks the caller's next spawn whose child lands: its thread parks in `loader::spawn`, after the landing and its retires, until the child's exit is published. `spawn_child_ends_first` spawns a child that exits at once under that hold and reads the child's code off the handle the spawn answers. This is the window the merge before this commit left open: #659 installs the child's own `self` at the commit, schedules the child, and installs its spawner's handle only once `loader::spawn` has returned. A child whose table closes in between takes its object's handle count to zero and back, which `HandleEntry::new` asserts against on every row now that none is `reopenable`. The binary is the control for the commit that removes the window. `ProcessEntry::object` goes: `process::process_object` was its last caller on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ld lands
Measured first, on the commit before this one: `spawn_child_ends_first`
on the test kernel under QEMU ends in
PANIC: panicked at src/object/handle.rs:108:9:
a handle to a retired Process (koid 162)
kernel::object::ops::install
kernel::syscall::proc::sys_spawn
The child's table had closed, taking its `self` and the object's count to
zero, before `sys_spawn` installed the spawner's handle.
`PendingHandles::commit` now installs the caller's handle in the hold
that moves the endowments, before the child's own `self` and before the
landing, and answers it; `sys_spawn` returns that handle and installs
nothing. The room for it is checked with the child's table's, before
anything moves, so the refusal a full table draws leaves the caller's
table as it was and no child exists: the kill of a child that had landed
and could not be named goes.
`loader::spawn` takes the commit as a closure and answers what it left
its caller holding beside the pid, so the boot's init, which no table but
its own holds, answers `()` and the syscall a `RawHandle`: `PendingHandles`
loses its `Ready` variant and is the caller's request alone.
A handle the commit installed resolves before the syscall that will
answer it has returned. Only a caller that guessed its number can name
it there, and what it gets is what a process not yet in the table gives:
a wait that parks, `NotFound` for its accounting, `Gone` for a spawn
under it, and a kill that finds nothing to claim.
toyos-proclife: the model's spawn mints both handles in the section that
lands the child, a teardown closes its process's table, and L13 refuses a
handle minted on an object whose last had gone.
`mutate-spawner-handle-after-the-landing` restores the old order and reds
`a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it`
and `a_spawn_racing_the_kill_of_its_own_spawner`: the child lands claimed,
is retired and closes its table, and then its spawner's handle is minted.
Guest arms: `abuse_handle_table` spawns from its full table naming an
endowment, and is refused with the endowment still its own; under the
hold, `spawn_child_ends_first` spawns from a full table a child that
would speak into a pipe, and the pipe ends empty.
Closes issues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md:
its exit was the handle in the commit's hold with the room checked before
anything moves, and a guest arm reading `ResourceExhausted` with no child
started.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Under the control that lets a spawn from a full table succeed, `abuse_handle_table` ended with exit 134 and no message: its `expect_err` panicked with every slot of the table taken. Both arms now take the spawn's answer, close what filled the table, and assert after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Runs at 35e5338 (implementer; QEMU on this host, 14 cores, nothing else of this worktree running). Each row is one run of the probe: every patch applied as a checked patch (
The 78 shipping members: The hazard, at 7fe8c56: The old order restored, at 35e5338: Slot not taken, test kernel: Slot not taken, shipping kernel:
The model at 35e5338: T14, staged and not run: probe.patch — the machine test every row at 35e5338 runsdiff --git a/tests/toyos.rs b/tests/toyos.rs
index 8fa631772..837b0c620 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -170,6 +170,10 @@ const MACHINE_TESTS: &[&str] = &[
// The nested-NMI report is a raw write to the 16550, which the T14 does not
// have.
"nested_nmi_is_loud",
+ // PROBE, not for landing: the shared-boot binaries `TOYOS_PROBE` names, on
+ // one QEMU boot of tests/testcases; `TOYOS_PROBE_DEBUG` picks the kernel
+ // that carries `SYS_DEBUG`.
+ "shared_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2024,6 +2028,39 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "shared_probe" => {
+ let names = std::env::var("TOYOS_PROBE").expect("TOYOS_PROBE names the binaries to run");
+ let kernel_features: &'static [&'static str] =
+ if std::env::var_os("TOYOS_PROBE_DEBUG").is_some() { ACTUATOR_KERNEL } else { &[] };
+ let rust_bins = qemu::build_toyos_bins(
+ &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests"),
+ );
+ let mut qemu = QemuInstance::boot_with_options(
+ test_config,
+ &[],
+ &rust_bins,
+ BootOptions { kernel_features, ..Default::default() },
+ );
+ let mut failed = Vec::new();
+ for name in names.split(',') {
+ let result = qemu.run_test(&format!("test_rs_{name}"), Duration::from_secs(180));
+ eprintln!(" [probe] {name}: exit {:?}", result.exit_code);
+ if result.exit_code != Some(0) {
+ let died = result.exit_code.is_none();
+ failed.push(format!(
+ "{name}: exit {:?}\n{}\n--- stdout ---\n{}",
+ result.exit_code,
+ result.error.map(|e| e.to_string()).unwrap_or_default(),
+ result.stdout,
+ ));
+ // No exit code is a machine that is gone: nothing after it runs.
+ if died {
+ break;
+ }
+ }
+ }
+ if failed.is_empty() { Ok(()) } else { Err(failed.join("\n")) }
+ }
other => Err(format!("unknown machine test {other}")),
}
}probe-v1.patch — the probe as the hazard row ran it, which stops at the first member that does not exit 0diff --git a/tests/toyos.rs b/tests/toyos.rs
index 8fa631772..5eca9a086 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -170,6 +170,10 @@ const MACHINE_TESTS: &[&str] = &[
// The nested-NMI report is a raw write to the 16550, which the T14 does not
// have.
"nested_nmi_is_loud",
+ // PROBE, not for landing: the shared-boot binaries `TOYOS_PROBE` names, on
+ // one QEMU boot of tests/testcases; `TOYOS_PROBE_DEBUG` picks the kernel
+ // that carries `SYS_DEBUG`.
+ "shared_probe",
];
/// **The metal profile**: which registrations run on the ThinkPad T14, what
@@ -2024,6 +2028,33 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> {
match name {
"iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config),
"nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config),
+ "shared_probe" => {
+ let names = std::env::var("TOYOS_PROBE").expect("TOYOS_PROBE names the binaries to run");
+ let kernel_features: &'static [&'static str] =
+ if std::env::var_os("TOYOS_PROBE_DEBUG").is_some() { ACTUATOR_KERNEL } else { &[] };
+ let rust_bins = qemu::build_toyos_bins(
+ &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/toyos-rust-tests"),
+ );
+ let mut qemu = QemuInstance::boot_with_options(
+ test_config,
+ &[],
+ &rust_bins,
+ BootOptions { kernel_features, ..Default::default() },
+ );
+ for name in names.split(',') {
+ let result = qemu.run_test(&format!("test_rs_{name}"), Duration::from_secs(180));
+ eprintln!(" [probe] {name}: exit {:?}", result.exit_code);
+ if result.exit_code != Some(0) {
+ return Err(format!(
+ "{name}: exit {:?}\n{}\n--- stdout ---\n{}",
+ result.exit_code,
+ result.error.map(|e| e.to_string()).unwrap_or_default(),
+ result.stdout,
+ ));
+ }
+ }
+ Ok(())
+ }
other => Err(format!("unknown machine test {other}")),
}
}probe-110.patchdiff --git a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
index 5a63f02dd..95110d3fb 100644
--- a/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
+++ b/tests/toyos-rust-tests/src/bin/process_lifecycle.rs
@@ -35,6 +35,8 @@ use toyos_abi::RawHandle;
#[path = "../roster.rs"]
mod roster;
+#[path = "../arch/mod.rs"]
+mod arch;
const SELF_PATH: &str = "/system/bin/test_rs_process_lifecycle";
@@ -62,6 +64,15 @@ fn test() {
a_kill_publishes_like_an_exit();
a_handle_is_the_whole_of_the_right();
an_undefined_wait_flag_bit_is_refused();
+ // PROBE, not for landing: 110 answers as a number nothing serves.
+ // SAFETY: an unassigned number, refused without reading an argument.
+ let answer = unsafe { arch::bare_syscall(110) };
+ assert_eq!(
+ SyscallError::from_u64(answer),
+ Some(SyscallError::InvalidArgument),
+ "syscall 110 answered {answer:#x}, not as an unassigned number"
+ );
+ println!(" [probe] syscall 110 is unassigned");
println!("a process is a handle: the code is read, not claimed");
}
mut-old-order.patchdiff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
index d74b0930d..40cd17d49 100644
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -4,7 +4,7 @@
use alloc::vec::Vec;
-use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
+use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal};
use crate::process::{
process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
};
@@ -53,7 +53,7 @@ pub struct PendingHandles {
impl PendingHandles {
/// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
/// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
- pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+ pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, KObjectRef), Refusal> {
let Self { mut table, endow, mut labels } = self;
let data_arc = process_data();
let mut data = data_arc.lock();
@@ -103,8 +103,7 @@ impl PendingHandles {
entries.push(entry);
}
// In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
- let held = ops::install(&mut data.handles, own.clone())
- .expect("a caller's table with verified room refused its child");
+ let held = own.clone();
drop(data);
endow_self(&mut table, &mut entries, &mut labels, own);
Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
index c54120404..4e0163efd 100644
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,12 @@ pub(super) fn sys_spawn(
) -> u64 {
// Nothing to clean up: spawn's frame owns the child's resources on error.
match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
- Ok((_, handle)) => u64::from(handle.0),
+ Ok((_, object)) => {
+ match process::with_process_data(|data| crate::object::ops::install(&mut data.handles, object)) {
+ Ok(handle) => u64::from(handle.0),
+ Err(e) => e.to_u64(),
+ }
+ }
Err(e) => e.refuse(),
}
}mut-slot-not-taken.patchdiff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
index d74b0930d..51bcc7ade 100644
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -53,7 +53,7 @@ pub struct PendingHandles {
impl PendingHandles {
/// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
/// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
- pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+ pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, HandleEntry), Refusal> {
let Self { mut table, endow, mut labels } = self;
let data_arc = process_data();
let mut data = data_arc.lock();
@@ -87,7 +87,7 @@ impl PendingHandles {
moving.push((EndowEntry { label_off, label_len, handle, _pad: 0 }, handle));
}
// Checked before any removal, so a failed install can't strand a handle out of a table that never spawned.
- if !table.has_room(moving.len() + 1) || !data.handles.has_room(1) {
+ if !table.has_room(moving.len() + 1) {
return Err(SyscallError::ResourceExhausted.into());
}
@@ -103,8 +103,7 @@ impl PendingHandles {
entries.push(entry);
}
// In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
- let held = ops::install(&mut data.handles, own.clone())
- .expect("a caller's table with verified room refused its child");
+ let held = HandleEntry::new(own.clone(), ops::initial_rights(&own));
drop(data);
endow_self(&mut table, &mut entries, &mut labels, own);
Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
index c54120404..648d2bf29 100644
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,10 @@ pub(super) fn sys_spawn(
) -> u64 {
// Nothing to clean up: spawn's frame owns the child's resources on error.
match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
- Ok((_, handle)) => u64::from(handle.0),
+ Ok((_, entry)) => match process::with_process_data(|data| data.handles.install(entry)) {
+ Ok(handle) => u64::from(handle.0),
+ Err(crate::object::handle::TableFull) => SyscallError::ResourceExhausted.to_u64(),
+ },
Err(e) => e.refuse(),
}
}
+// CONTROL, not for landing: the ABI at this head no longer declares it.
+const SYS_PROCESS_OPEN: u64 = 110;
+ |
|
Answers to the review at a8acd2a (#642 (comment)), at 35e5338. BLOCKER
NOTE
REMOVE
|
|
T14 evidence at
|
|
Review of #642, round 2, at 35e5338 Earlier BLOCKERs
What this verdict rests on. I read, and ran nothing: Size. +359 −393 in 27 files: production ( The probe. Sound for what it measured. It applies to this head's BLOCKER
NOTE
The five left to the orchestrator
REMOVE
SEND BACK |
… and a sibling's close is a step
`PendingHandles::commit` installs the caller's handle to the child in the
caller's live table, drops that table's lock, and only then mints the child's
own `self`. The model minted both in the section that lands the child and had
no close but a teardown's, so it could not see a thread of the spawner closing
that handle in the gap.
The spawn's section is now two: the commit under the caller's lock, and the
child's own handle with the landing once that lock is given up, in the order
the kernel has them at this commit. `Op::Close` is `sys_close` by another
thread of the spawner on the handle the spawn will answer.
This commit is red, and is the measurement: `cargo test -p toyos-proclife`
exits 101 on `a_sibling_closing_a_spawns_handle_before_the_spawn_returns`,
pid 2: a handle to it was minted after its last one had gone
schedule: spawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0
which is `HandleEntry::new`'s assert in `endow_self`. The next commit changes
the kernel's order and the model's with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…takes it At 35e5338 `PendingHandles::commit` installed the caller's handle to the child in the caller's live table, released that table's lock, and minted the child's `self` after it. In between the object had one handle, in a table every other thread of the caller reaches, and `LockGuard::drop` is a preemption point: a close or a `dup2` over it there retired the object, and `endow_self` then panicked in `HandleEntry::new`. 40d7842 shows it in the model. `loader::spawn` now mints the child's `self` as it makes the object (`start::own_handle`) and `commit` takes that `HandleEntry`, not the object. The caller's handle is minted from the entry's object while the entry is held, and the entry then sits in the child's table, which no thread reaches until the child lands. `commit` has no object to mint from but the one inside a handle it holds, so the order that crossed zero is not one it can be written in. The model's commit section mints both, the child's own first. `mutate-spawner-handle-before-the-childs-own` restores 35e5338's order and `src/ci.rs`'s `CONTROLS` runs it: `a_sibling_closing_a_spawns_handle_before_the_spawn_returns` fails on L13. `stats_of`'s doc loses the sentence that named one window in which it answers `None`: a handle that resolves before its process lands is a second. `toyos_abi::syscall::spawn`'s doc says the room for the answer is counted before, and regardless of, what the endowments free. A kill on the handle before the child lands still claims nothing; that is filed as issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Runs at c837976 (implementer; QEMU on this host, 14 cores, nothing else of this worktree running). Logs: The model, on the host.
At 40d7842 and under the first feature alike: The probe, under QEMU. Each row is one run of the probe of #642 (comment) (
The 5
Old order, Slot not taken, test kernel, Slot not taken, shipping kernel,
The kernel's size. The shipping x86-64 kernel
T14, the previous head's boots judged. T14, staged at c837976 and not run: mut-old-order.patchdiff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -7,7 +7,7 @@
use alloc::vec::Vec;
use crate::object::process::ProcessObject;
-use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
+use crate::object::{HandleEntry, HandleTable, KObjectRef, Refusal};
use crate::process::{
process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
};
@@ -56,7 +56,7 @@
impl PendingHandles {
/// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
/// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
- pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+ pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, KObjectRef), Refusal> {
let Self { mut table, endow, mut labels } = self;
let data_arc = process_data();
let mut data = data_arc.lock();
@@ -106,8 +106,7 @@
entries.push(entry);
}
// Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
- let held = ops::install(&mut data.handles, own.object().clone())
- .expect("a caller's table with verified room refused its child");
+ let held = own.object().clone();
drop(data);
endow_self(&mut table, &mut entries, &mut labels, own);
Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,12 @@
) -> u64 {
// Nothing to clean up: spawn's frame owns the child's resources on error.
match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
- Ok((_, handle)) => u64::from(handle.0),
+ Ok((_, object)) => {
+ match process::with_process_data(|data| crate::object::ops::install(&mut data.handles, object)) {
+ Ok(handle) => u64::from(handle.0),
+ Err(e) => e.to_u64(),
+ }
+ }
Err(e) => e.refuse(),
}
}mut-slot-not-taken.patchdiff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -56,7 +56,7 @@
impl PendingHandles {
/// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
/// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
- pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+ pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, HandleEntry), Refusal> {
let Self { mut table, endow, mut labels } = self;
let data_arc = process_data();
let mut data = data_arc.lock();
@@ -90,7 +90,7 @@
moving.push((EndowEntry { label_off, label_len, handle, _pad: 0 }, handle));
}
// Checked before any removal, so a failed install can't strand a handle out of a table that never spawned.
- if !table.has_room(moving.len() + 1) || !data.handles.has_room(1) {
+ if !table.has_room(moving.len() + 1) {
return Err(SyscallError::ResourceExhausted.into());
}
@@ -106,8 +106,7 @@
entries.push(entry);
}
// Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
- let held = ops::install(&mut data.handles, own.object().clone())
- .expect("a caller's table with verified room refused its child");
+ let held = HandleEntry::new(own.object().clone(), ops::initial_rights(own.object()));
drop(data);
endow_self(&mut table, &mut entries, &mut labels, own);
Ok((table, Endowments::new(entries, labels), held))
diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs
--- a/kernel/src/syscall/proc.rs
+++ b/kernel/src/syscall/proc.rs
@@ -53,7 +53,10 @@
) -> u64 {
// Nothing to clean up: spawn's frame owns the child's resources on error.
match process::spawn(args, |own| pending.commit(own), cwd, env, image, parent) {
- Ok((_, handle)) => u64::from(handle.0),
+ Ok((_, entry)) => match process::with_process_data(|data| data.handles.install(entry)) {
+ Ok(handle) => u64::from(handle.0),
+ Err(crate::object::handle::TableFull) => SyscallError::ResourceExhausted.to_u64(),
+ },
Err(e) => e.refuse(),
}
}mut-head-order.patch — the kernel half of c837976 reverteddiff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs
--- a/kernel/src/loader/mod.rs
+++ b/kernel/src/loader/mod.rs
@@ -337,8 +337,8 @@
/// Load a program and place its main thread under `parent`, answering its pid
/// and what `commit` left its caller holding of it. `commit` builds the
-/// child's handle table around the child's handle to itself, once nothing is
-/// left to refuse.
+/// child's handle table around the child's own object, once nothing is left
+/// to refuse.
///
/// `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
@@ -350,7 +350,7 @@
/// unwinds, so the error must travel out as a value rather than strand it.
pub fn spawn<H>(
argv: &[&str],
- commit: impl FnOnce(crate::object::HandleEntry) -> Result<(HandleTable, Endowments, H), crate::object::Refusal>,
+ commit: impl FnOnce(KObjectRef) -> Result<(HandleTable, Endowments, H), crate::object::Refusal>,
cwd: String,
env: Vec<u8>,
image: Option<Arc<dyn crate::file_backing::FileBacking>>,
@@ -592,7 +592,7 @@
let object = crate::object::process::ProcessObject::new(pid);
// The point of no return: every failure above answers the caller with its
// table untouched.
- let (handles, endowments, held) = commit(start::own_handle(&object))?;
+ let (handles, endowments, held) = commit(KObjectRef::Process(Arc::clone(&object)))?;
let proc_data = Arc::new(Lock::new(ProcessData {
handles,
cwd,
diff --git a/kernel/src/loader/start.rs b/kernel/src/loader/start.rs
--- a/kernel/src/loader/start.rs
+++ b/kernel/src/loader/start.rs
@@ -3,10 +3,8 @@
//! stack starts from and the trampolines it returns into are the
//! architecture's (`arch::entry`).
-use alloc::sync::Arc;
use alloc::vec::Vec;
-use crate::object::process::ProcessObject;
use crate::object::{ops, HandleEntry, HandleTable, KObjectRef, Refusal};
use crate::process::{
process_data, Endowments, OwnedAlloc, ENDOW_ENTRY_LEN, KERNEL_STACK_SIZE,
@@ -54,9 +52,9 @@
}
impl PendingHandles {
- /// Take the endowed handles out of the caller's table and put its handle to the child in it, all under one lock hold: a refusal leaves the table unchanged.
- /// `own` is the child's handle to itself, which its table holds under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
- pub fn commit(self, own: HandleEntry) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
+ /// Take the endowed handles out of the caller's table and put its handle to `own`, the child, in it, all under one lock hold: a refusal leaves the table unchanged.
+ /// The child's table holds `own` under [`SELF_LABEL`] beside the endowments, and the caller's handle is the third answer.
+ pub fn commit(self, own: KObjectRef) -> Result<(HandleTable, Endowments, RawHandle), Refusal> {
let Self { mut table, endow, mut labels } = self;
let data_arc = process_data();
let mut data = data_arc.lock();
@@ -105,8 +103,8 @@
.expect("a child table with verified room refused an endowment");
entries.push(entry);
}
- // Minted while `own` is held, and `own` goes into a table no thread reaches until the child lands: another thread of the caller closing this handle never closes the object's last.
- let held = ops::install(&mut data.handles, own.object().clone())
+ // In this hold, before the child can run: its object's handle count never reaches zero while the spawn is in flight.
+ let held = ops::install(&mut data.handles, own.clone())
.expect("a caller's table with verified room refused its child");
drop(data);
endow_self(&mut table, &mut entries, &mut labels, own);
@@ -114,16 +112,11 @@
}
}
-/// A new process's handle to itself, the first its object has: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on.
-pub(super) fn own_handle(object: &Arc<ProcessObject>) -> HandleEntry {
+/// Install `own` in its own table under [`SELF_LABEL`]: `WRITE` to be named a spawn's place, `DUP` and `TRANSFER` to hand that on. Its caller verified the room.
+pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec<EndowEntry>, labels: &mut Vec<u8>, own: KObjectRef) {
let rights = Rights::WRITE.union(Rights::DUP).union(Rights::TRANSFER);
- HandleEntry::new(KObjectRef::Process(Arc::clone(object)), rights)
-}
-
-/// Install `own` in its own table under [`SELF_LABEL`]. Its caller verified the room.
-pub(super) fn endow_self(table: &mut HandleTable, entries: &mut Vec<EndowEntry>, labels: &mut Vec<u8>, own: HandleEntry) {
let handle = table
- .install(own)
+ .install(HandleEntry::new(own, rights))
.expect("a child table with verified room refused its own handle");
entries.push(EndowEntry {
label_off: labels.len() as u32,
+// CONTROL, not for landing: the ABI at this head no longer declares it.
+const SYS_PROCESS_OPEN: u64 = 110;
+ |
|
Answers to the review at 35e5338 (#642 (comment)), at c837976. Measurements: #642 (comment). BLOCKER
NOTE
The five
REMOVE
|
|
T14 evidence at
|
|
Review of #642, round 3, at c837976 Earlier BLOCKER
The six points, each read against the code
Reachable in the window and not on the walk, none of them a gap: a wait parks or answers Earlier NOTEs
Earlier REMOVEs — all four are gone from the tree and the body. What this verdict rests on. I read, and ran nothing. Logs:
Size. +473 −400 in 28 files: production ( The two judgments the brief asks for
BLOCKER None. NOTE
REMOVE
LAND AFTER NAMED CHANGES |
No conflict. `src/ci.rs` is the one file changed on both sides, in separate hunks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
#647, #642 and #636's follow-up landed since the last merge. One conflict, in tests/toyos.rs: #647 made `boot_virt_smp` take a `BootOptions` and `judge_virt_job` borrow its guest, and this branch had added `virt_mask_windows` on the old shape. Main's shape is kept whole; `virt_mask_windows` names its kernel build in the options and lends its guest like the others. `tests/virtsmpcase` now ends in `shutdown`, which this test does not wait for: it judges once `unmap_touch` has ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Three modify/delete conflicts, each hunk of this branch's side accounted for: - tests/toyos-rust-tests/src/bin/log_hold.rs, deleted by 520c0d1: the one hunk moved its 192 records from syscall 26 to `SYS_DEBUG` `LOG_PATTERNED`. The binary and `log_program_line_after_its_records` are gone, so it goes. - tests/common/origin.rs, deleted by 520c0d1: `staged_job`, `one_job` on it, `PATTERNED` in `RETIRED`'s place and `after_records` on the test kernel with its per-index count all served that one test. They go. - issues/build/no-device-class-answers-for-a-block-device.md, deleted on main: the one hunk dropped "(3 and 4 are retired.)". It goes. Content conflicts: - tests/common/logstream.rs and tests/common/qemu.rs are main's: this branch's `stage_on_test_kernel`, `write_staged` and `build_test_kernel_image` had `origin::after_records` as their only caller. - issues/build/the-boot-census-guesses-a-staged-images-kernel.md, which this branch filed against `build_test_kernel_image` and a staged `BootOptions::boot_image`, goes: main has neither. - CLAUDE.md and .claude/agents/reviewer.md are main's: #673 landed this branch's half of both. - issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md is main's: this branch's hunk edited stage 0, which #642 landed and deleted. - issues/kernel/the-capability-end-state-is-twelve-answers.md: main deleted the sentence this branch's first hunk edited; the second hunk, which drops "85 `SYS_LISTEN` and 87 `SYS_CONNECT` are retired numbers", is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
The branch was at 649ea51 (#641). Three landings since sit under it: #642 (dc8212c), #659 (c1c5048) and #655 (5daab30). Two content conflicts, each main deleting what this branch's hunk stood beside: - kernel/src/object/ops.rs, close_ends_polls: #655 deleted the log's and the keyboard's close actuators, whose two arms this branch's `Process(_) => false` sat between. Main's two `false` arms stand and the process's is a third. - tests/toyos-rust-tests/src/bin/process_lifecycle.rs, the imports: #642 deleted `toyos::AsHandle` with the pid arm, its one user; this branch's `toyos::poller` import stands alone. Everything else merged by itself: #642's deletions in kernel/src/object/process.rs beside this branch's `Arc<Watch>`, #659's init changes beside the one doc sentence this branch deletes, and the `rust` gitlink at main's 95960d6c214. This commit is the resolution and nothing else. What #655's contract changes in this branch's own lines is the next commit's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Stage 0 of
issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md: theProcesshandle is the only control, so the one call that turned a pid into a handle is deleted with every name only it reached. Deleting thereopenablecolumn that call needed exposed a second thing that needed it, #659's spawn, so the order a spawn mints its handles in changes here too.What changed, per decision
SYS_PROCESS_OPENis deleted outright, and 110 is free.sys_process_open, its dispatch arm,toyos_abi::syscall::{SYS_PROCESS_OPEN, process_open}andSysCap::open_processgo. 110 is not added toretired_syscalls!and the ABI carries no comment for it: rootCLAUDE.mdsays a removed number is free, andissues/design-debt/the-abi-still-keeps-retired-syscall-numbers.mdowns the table.Rights::MANAGEleaves init'sSysCap, where opening by pid was its only use; it stays the right a kill needs.kobject!sealed/reopenablecolumn is deleted, with its control:process::process_object,object::process::reopen_selftest,sched::kthread::open_selftest, theprocess-reopen-selftestactuator, its metal row, judge,SELFTESTSarm andFLASHABLEname.ProcessEntry::objectgoes too:process_objectwas its last caller onmain.maina spawn puts the child'sselfin the child's table, schedules the child, and only then installs the spawner's handle (sys_spawn): a child whose table closes in between takes its object's count to zero and back, which only areopenablerow survives.loader::spawnnow mints the child'sselfas it makes the object (start::own_handle) and hands thatHandleEntryto the commit.PendingHandles::commitholds no object but the one inside that entry: it mints the caller's handle from it under the caller's lock, installs it, and puts the entry in the child's table.sys_spawnreturns the handle the commit answered and installs nothing.ProcessObject::new, then at oncestart::own_handle: one handle, a value in the spawning thread's kernel frame. No table holds it and no thread can name it.commit, the caller's lock held. A refusal drops that handle and the count reaches zero for good: the only other reference to the object isspawn'sArc, which goes asspawnreturns the refusal.commitinstalls the caller's handle: two handles. Once the lock is released every thread of the caller reaches this one, and can close it,dup2over it, duplicate it or transfer it. Each leaves the count at one or more, because the child's own is not in a table any thread reaches.commit, then ofspawn, then inside theProcessDataspawnbuilds. Still reached by nobody.PROCESS_TABLE: the entry is inserted and the thread enqueued. From here the child's threads and the child's teardown reach the child's own handle. A count that reaches zero now is final:KObjectRef::Process(is constructed once, inown_handle, every otherHandleEntry::newin the kernel names an object of another kind made at its own site, and a duplicate needs a live entry.spawnanswers the caller's handle as a number. Nothing is minted.has_room(1)), checked before any endowment moves and with no arithmetic over what the endowments would free.mainserved a caller exactly at its cap whose spawn named an endowment: the move freed the slot the answer then took. This branch refuses that spawnResourceExhausted, by name, with the table unchanged and no child built.abuse_handle_table's arm at the cap pins the refusal as the rule: it spawns from a full table naming one of its handles as an endowment, and onmain's kernel readsOk(RawHandle(8191))(the last control row below).toyos_abi::syscall::spawn's doc states the rule.sys_spawn's kill of a landed child it could not name goes. This is the exit ofissues/kernel/a-spawn-refused-for-its-callers-full-table-has-already-moved-its-endowments.md, which is closed here.loader::spawntakes the commit as a closure, generic over what the commit answers beside the pid:()for the boot's init, which no table but its own holds, and aRawHandlefor the syscall.PendingHandlesloses itsReadyvariant and is a caller's request alone. The alternative, one enum answeringOption<RawHandle>, would havesys_spawnunwrap aNonethe types could refuse. The kernel ships two copies ofspawnfor it; the size is under "The kernel's size". The other cut,spawnsplit at its point of no return into a build and a landing, was not taken:spawnreads 34 of its locals after the commit (counted offkernel/src/loader/mod.rs), an upper bound on what a split carries: at least a dozen of them in a struct declared, built and taken apart once each, against the closure's one parameter and two call sites. That is a count, not a built diff.toyos-proclifemodels the spawn's two lock sections and a close by the spawner's other thread. The model's spawn is cut where the kernel gives up its caller's lock: the commit mints the child's own handle and the caller's, and the landing is its own section.Op::Closeissys_closeby another thread of the spawner on the handle the spawn will answer. L13 refuses a handle minted on an object whose last had gone.mutate-spawner-handle-before-the-childs-ownrestores 35e5338's order andmutate-spawner-handle-after-the-landingmain's;src/ci.rs'sCONTROLSruns both.debug_action::HOLD_SPAWN_UNTIL_CHILD_ENDS(23), on the test kernel only: the caller's next spawn whose child lands parks, after the landing, until the child's exit is published. No caller can order a child's end inside the spawn that starts it.spawn_child_ends_first(new, the sharedSYS_DEBUGboot): under the hold, a spawn answers a child that has already ended and its handle reads the code; and a spawn from a full table starts no child, read off a pipe that child would have spoken into.abuse_handle_tablegains the arm at its cap.process_lifecycleloses its pid arm, whose callee is gone. The model gainsa_sibling_closing_a_spawns_handle_before_the_spawn_returns.the-capability-end-state-is-twelve-answers.mdquestion 3 says no arm takes a pid, question 10 dropsSYS_PROCESS_OPEN, and the paragraphs that argued from 110 go;the-kernel-keeps-nothing-it-enumerates.mdloses its clause aboutMANAGE. Filed:issues/kernel/a-kill-on-a-spawns-handle-before-its-child-lands-claims-nothing.md. The commit still installs the caller's handle before the landing, so that window is not removed.git grepat this head forsys_process_open,process_open,open_process,SYS_PROCESS_OPEN,reopenable,process_object,reopen_selftest,open_selftest,process-reopen,process_reopenandprocess-open-kthreadfinds onlytoyos-symbols/tests/fixtures/input-test.bin, whose symbols are frozen test data.Net against
origin/main: +473 −400 in 28 files. Production (kernel,toyos-abi,toyos,src) +149 −264; the model and tests +286 −70; issues +38 −66.Gates at c837976
cargo run -- --ci host(66 steps, both spawn controls among them)cargo run -- --build-onlycargo test --test toyos-build(the QEMU suite, 21 guests)High-risk checks (ABI, process lifecycle, a refcount under concurrency)
Every run, its log lines and every mutation patch are in #642 (comment); the probe is the one of #642 (comment), a patch that adds one machine test booting
tests/testcasesunder QEMU and running the named shared-boot binaries. None of it lands.The model,
cargo test -p toyos-proclife:a_sibling_closing_a_spawns_handle_before_the_spawn_returns: "pid 2: a handle to it was minted after its last one had gone", schedulespawn_under#0 -> spawn_under#0 -> close#1 -> spawn_under#0mutate-spawner-handle-before-the-childs-ownmutate-spawner-handle-after-the-landinga_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it,a_spawn_racing_the_kill_of_its_own_spawnerand the new testThe probe at c837976:
SYS_DEBUGmembersmain's order: the spawner's handle installed onceloader::spawnreturnsmut-old-order.patchPANIC: … src/object/handle.rs:108:9: a handle to a retired Process, fromops::installinsys_spawnmut-slot-not-taken.patchspawn_child_ends_first: "the refused spawn's child started and spoke"mut-slot-not-taken.patchabuse_handle_table: "a spawn from a full table was not refused"mut-head-order.patchmut-head-order.patch, 110 probe armkernel/as onorigin/mainnc-whole.patch, 110 probe armprocess_lifecycleends 139 at the probe's call of 110,abuse_handle_tableon "a spawn from a full table was not refused"commit's parameter type.mut-head-order.patch, the kernel half of c837976 reverted, stays green in every booted member: no guest arm runs two threads of one spawner inside the commit.mut-old-order.patchends in the same assert at this head;mutate-spawner-handle-after-the-landingreds the model.nc-whole.patchapplied,git diff origin/main -- kernelnames one line, the constant the reverted dispatch needs. The 110 probe arm calls 110 througharch::bare_syscalland expectsInvalidArgument, the answer for a number nothing serves: green at this head, and onmain's kernelsys_process_openserves it and ends the caller on the handle it reads. The arm is in the control and not in the tree: no committed test pins a free number.rgfor the deleted names finds no caller: nothing in~/Dev/jan/forks(16 clones) orrust/library; in~/.cargo/git/checkoutsa cached copy of this repository at 6115718 that no manifest names; in~/.cargo/registry/srcthe publishedtoyosandtoyos-abi0.1.0 and 0.2.0, which are the definitions, and uucore's unrelated Windowsprocess_open.Tiers
The order a spawn mints its handles in is held on the host: by
commit's parameter, a handle and not an object, and by the model's L13 with its two controls.spawn_child_ends_firstis a member of the sharedSYS_DEBUGboot, which runs on the T14; it adds no QEMU guest test. What the host cannot reach is that a child ending inside its own spawn leaves the spawner a handle that reads its code: that takes a booted kernel and the hold.The kernel's size
The shipping x86-64 kernel
cargo run -- --build-onlystages, read with the toolchain'sllvm-size -A;origin/mainis this worktree withgit diff --binary c8379761c origin/mainapplied, built, and reversed..textorigin/main(a31eec5)loader::spawnis one symbol of 15396 bytes onorigin/mainand two at this head, 11263 for the syscall's and 11398 for init's, which runs once a boot. The aarch64 kernel was not measured.T14
At 35e5338, run by the orchestrator (#642 (comment)), LENOVO 20W0003AMZ, BIOS N34ET71W (1.71): six boots, each
toyos-metalexit 0 with verdictpassed, and 9 members ended, 9 with exit 0.6d55f4de59ee23a6f04f9b0248204196d97a3f7cbd2a15f933d8bcc6deb1a84babuse_spawn_argv0,spawn_image_object0e950408bd9bd29eff453d46237a893325e77e4813187ffef6223d73270b43d3fspawn_child_ends_first0,spawn_lands_claimed070672353b04ff7fa5e45229cba66ac2581e0f698308887f15a3bdd42896335a4abuse_handle_table0a73d68406b3c64180062e976384e2264506ce22c511486935b588c84b693409bhandle_lifetime0a18c5a34fc6c6130992a2b6b291e20929e886d58d04072711409840957345ed9process_tree0b7d7759a101234dc18b6755e52f541ffdc7ff872f82d32bc3fd9d0f92a350a47process_lifecycle0,std_process0The rows' judges over those readbacks,
cargo test --test toyos-build -- --metal --metal-readback <dir> <filter>at 35e5338:spawnexit 0 (4 passed, 0 failed, 2 boots),handleexit 0 (2 passed, 0 failed, 2 boots),processexit 0 (PASS process_tree; 3 passed, 0 failed, 2 boots). These are that head's green arms; no mutation was booted on the machine.At c837976, run by the orchestrator (#642 (comment)), same machine: seven boots, each image's sha256 checked against the request before it was flashed, each
toyos-metalexit 0 with verdictpassed, and 9 members ended, 9 with exit 0.commitmoved since 35e5338, so these are the six boots again and one row of theselftestsimage, which has one arm fewer.fc71752979a3016e43e9827aa2582606dcce28ea4fdf000594f41a80ca178944abuse_spawn_argv0,spawn_image_object02eb796a5994c42f0cb518dad181f5302a6a72d1547be66c3e3361f1c023f4378spawn_child_ends_first0,spawn_lands_claimed00b5a8c89e3d74d3bcd06932a415ca17f0bb16663be5e9ea3583cbac34faa05cdabuse_handle_table00baa985b23578b82bc8f8bf07dccd17401975f560a895681b40d2630692c66b5handle_lifetime0d7b73c13e6736fda7ea01a1bcb96a443abccb50fc9d6597f7dd68fa2fad1c90cprocess_tree075eca94fff32d454f6c3384708c9490c502d0a6c230c5d2eea991e1add19e965process_lifecycle0,std_process0e6289effac6efd34db4212a7a94108071baccc913387f04876c04fe2ae24036fkernel.log:97virtio: pci cap selftest 15/15The rows' judges over those readbacks,
cargo test --test toyos-build -- --metal --metal-readback <dir> <filter>at c837976, run by the orchestrator from the clean worktree:spawnexit 0 (4 passed, 0 failed, 2 boots),handleexit 0 (2 passed, 0 failed, 2 boots),processexit 0 (PASS process_tree; 3 passed, 0 failed, 2 boots),pci_capability_walkexit 0 (PASS pci_capability_walk; 1 passed, 0 failed, 1 boot). Thespawn,handleandprocessjudgings each offeredboot.shared.*rows fortests/metal/lenovo-20w0003amz.toml; none is committed, sinceshared's whole boot is not this branch's. These are the head's green arms; no mutation was booted on the machine.🤖 Generated with Claude Code
https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm