Skip to content

A child's handles are a list its parent names: labels and namespaces leave the kernel - #1252

Merged
Japabu merged 23 commits into
mainfrom
endow-labels-and-namespace-leave-kernel
Nov 12, 2026
Merged

Japabu merged 23 commits into
mainfrom
endow-labels-and-namespace-leave-kernel

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The kernel kept, for every process, the labels its parent put on each endowment: it compared three of them (job, self, quit), answered them back through SYS_ENDOWMENTS, charged every process for 32 entries and 4 KiB of label text, and held a namespace object mapping service names to connectors behind three more calls (SYS_NAMESPACE_BUILD, SYS_NAMESPACE_OPEN, SYS_NAMESPACE_NAMES). None of that is the kernel's: it is what one program calls a handle it gave another.

Resulting behavior

  • A spawn moves a list of handles and nothing else. The child's table starts with RawHandle::SELF, RawHandle::NOTICE, then the endowments in the order the spawn listed them (RawHandle::endowed(i)). The kernel installs them so and asserts the order.
  • Names are bytes of the child's startup block. The parent writes a table (bytes u32 | count u32 | count x {len u16 | name}) whose entry i names endowed(i). The child's SDK checks it once, whole, before any lookup. A table that is not a list of distinct, non-empty UTF-8 names ends the child with status 125 and a line on the fd:2 it still names. The relocator's pre-relocation fd:2 lookup walks the same table with the stack alone.
  • SpawnArgs::job replaces the job label. It is a borrowed handle carrying WRITE, resolved once like place and pager. So the commit's re-check that a moved handle still named the job goes.
  • SYS_CONNECT (102) connects through a connector carrying WRITE. Connectors are now minted DUP | TRANSFER | WRITE. One without WRITE can only be passed on.
  • Deleted: SYS_ENDOWMENTS (99), SYS_NAMESPACE_BUILD (101) and SYS_NAMESPACE_NAMES (170) are gone, and the freed numbers stay unassigned. ObjectKind::Namespace goes and its word 8 is retired: a kind's word is ABI and never reassigned, as ObjectKind states since A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239. A const check holds word 8 unused, and every other kind keeps its word. Also deleted: the kernel's namespace object, SpawnEndow, EndowEntry, the label blob and its per-process charge, MAX_NAMESPACE_ENTRIES and MAX_SERVICE_NAME. SYS_NAMESPACE_OPEN's name was the last caller of the kernel's 64 KiB user_str, since A spawn's path is one line, and no judge finds a child by it #1238 bounds a spawn's path by MAX_PATH. So user_str and MAX_USER_STR go, and each thread is charged for what a spawn copies in and holds where it may be preempted (its path, name and build-id at their bounds, its ranges, its endowments and the mappings of the device claims it moves) instead of two 64 KiB strings and a page. SYS_SYSINFO's roster, which it holds across a copy-out that can wait, is charged to the caller's job for the call, and the live-thread bound guards the roster alone: a header alone is never refused. MAX_SPAWN_ENDOWMENTS is 96: the 30 one spawn carried plus the 64 a namespace held.
  • The supervisor, whose parent is the kernel, starts holding its system capability, the file server's image, ROOT's bytes and the boot names at the four handles toyos_abi::boot::supervisor states.
  • Userland: a service is a connector named svc:<name> (a row's receives, its view's fs: directories, minted grants) or provide:<name> (one a launching client sent). toyos::endow::service looks one up and connects. A helper is given a duplicate of each service its parent holds, narrowed by CommandExt::services_only. Everything else is taken once by name. A provided connector reaches only a row that receives its name, as the client's handle narrowed to DUP | TRANSFER | WRITE. A handle without those rights refuses the launch, and one the kernel does not report as a connector is refused by name (A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239's typed receive). A connector a peer sends, such as a session's launcher and controls, is typed only where the kernel reported a connector. Swap's connector arrives with WRITE alone. The supervisor adopts its own directory capabilities (std::os::toyos::fs::adopt_services), and no child inherits them. So a storage row, whose view is empty, now starts holding no file grant, where it used to inherit the supervisor's whole tree. That closes issues/storage-rows-start-holding-the-supervisors-whole-tree.md, and the new guest test adopted_services_stay holds it.
  • The shell's -c interrupt race, fixed. A quit of a -c shell reaches its children by the kernel's walk. A child that lands after the quit holds the reason unread, or none, and that quit never asks it. The shell meant to ask such a child itself, but it looked at a flag its listener thread set after reading the notice outside any lock.
    • A Ctrl+C arriving while the shell was still spawning its child could be missed. The child (cat) then ran for ever, and the shell with it. That is the tty_interrupt stall in guest run 43053472977, and main has the same race.
    • Now the notice is read only under the shell's lock, by both threads. The shell looks right after each child's spawn, and a -c shell is marked as one before it listens.
    • The decisions live in system/shell/src/quit.rs, which names nothing of the SDK. The new host crate shell-copies compiles it as it is and runs one interrupt and one child through every order.
    • What is left is the window no userland step can see: a child that listens between its landing and the shell's look is asked twice. It is recorded in issues/a-c-shell-cannot-tell-a-child-its-quit-asked-from-one-that-landed-after.md, with the kernel-side exit.

Kernel: 56,188 lines in 212 files at main d1cefba to 55,842 in 211 (git ls-tree of kernel/src/**.rs). The whole diff against d1cefba is 177 files, +2,470/-2,618.

Decisions (the design the orchestrator confirmed; recorded in the manual's authority page)

  1. Connect needs WRITE on the connector. A provided connector goes only to a row that receives its name. That second half is the orchestrator's ruling after review round 1, and it reverses the earlier transfer-only forwarding: rights only narrow, so a forward without WRITE had no consumer. Since A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239, the supervisor refuses at the launch, by name, a provided handle the kernel does not report as a connector.
  2. svc: and provide: stay two inherited classes, with one carry. The shell forwards provide: connectors without knowing their names.
  3. The supervisor's four boot handles sit at ABI positions. The alternative, a build-written startup block, would need a block section in the kernel image format to carry four names that one reader uses.
  4. Helpers keep whole-set inheritance, decided in one place (toyos::spawn::inherited). services_only narrows it. Stage 2 of the isolation track tightens it later.
  5. MAX_SPAWN_ENDOWMENTS = 96. What bounds the names is stated at the constant: the startup block.
  6. Validation covers the names table only. Arguments and environment keep A child's arguments and environment are memory its parent lays out #1200's stance.
  7. SYS_CONNECT takes SYS_NAMESPACE_OPEN's number, and the freed numbers stay unassigned.
  8. ObjectKind's words are never reassigned: the namespace's 8 stays retired.

Checks and evidence

Evidence branch pr-1252, at 3a701aae4. Each log ends with its exit code; local paths are masked. The head is 19efa90, rebased onto main d1cefba. rebase-d1cefbaf9/ holds the git range-diff of this history against 258a5e9's, the head reviewed LAND; only the first commit differs.

The rebase onto d1cefba brought #1242, under which every server admits its clients through toyos::admission, with #1251, #1257, #1262 and #1273. Three files conflicted, all in the first commit:

  • system/supervisor/src/main.rs: the imports alone. The launcher, grants and admin ports admit through toyos::admission (main), and a launch's batch reaches the same typed extras, job and place, narrowed to PROVIDED and endowed only to receiving rows (this change). The first resolution kept main's Endowments import, which this change no longer uses; the image build refused it, and the first commit drops it.
  • system/usbd/src/main.rs: the imports. usbd's inspect port admits through toyos::admission (main), and DEV_PREFIX comes from toyos::endow (this change).
  • issues/one-program-that-fills-a-servers-table-holds-every-other-client-out.md: main's rewritten account, which cites the supervisor's build_namespace, now cites received, its counterpart here.

No take_handles caller this change has needs more: lib/session's two connectors and the supervisor's launch batch are typed as before, and #1242 adds no namespace use. The first commit's message gives the kernel's size against d1cefba (55,830 lines in 211 files at that commit).

The rebase onto a7ca8ad brought #1261, under which a poll belongs to the handle it was made through. Main moved there while the rebase onto 429f772 was being checked:

The rebase onto 429f772 brought #1239 under this change, with #1267, #1263, #1223 and #1237. Under #1239, a receive reports each handle's kind, ReceivedHandle::typed::<T>() is the one way to type a handle a peer sent, and FromHandle::from_handle is unsafe with the giver's word as its contract. Eleven files conflicted with main, ten of them in the first commit, and each resolution keeps both sides:

  • issues/a-received-handle-has-no-knowable-type.md (deleted on main, edited here): deleted. A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239 closed it, and this change's edit only updated a fact about the namespace calls.
  • sdk/toyos/src/namespace.rs (deleted here, edited on main): deleted with the namespace. Main's edit removed Namespace::received, the unchecked constructor of a namespace a peer sent. The behaviour it carried is kept for the connectors that replace namespaces. A peer-sent handle is typed only where the kernel reported its kind, so this change's own Connector::received is deleted too.
  • lib/session/src/lib.rs: a session's launcher and controls, which the supervisor sends, are typed through typed::<Connector>() where main typed two namespaces. They are refused by name when they are not connectors.
  • sdk/toyos/src/endow.rs: main's FromHandle, OfKind, ClaimType and the from_handle! list with kinds, without Namespace. This change's name table takes an endowment through the unsafe from_handle on its parent's word, with main's justification.
  • sdk/toyos/src/quit.rs: a process starts holding its notice at RawHandle::NOTICE (this change), so main's from_handle! for Notice is not kept: nothing takes a notice by type.
  • sdk/std/os/fs.rs: main made adopt_namespace unsafe because the handle's kind is its caller's word. It goes with the namespace, and adopt_services, which replaces it, is unsafe for the same reason.
  • system/supervisor/src/main.rs: a launch's extras, job and place are typed as connectors, a job and a process (main), and refused by name otherwise. The extras are then narrowed to PROVIDED and endowed only to receiving rows (this change). The supervisor's own services go to adopt_services under a SAFETY comment, and so does its system capability at its boot handle.
  • tests/guest/rust-tests/src/bin/{moved_rights,mutual_kill,process_tree}.rs: imports only. Main's ReceivedEntry and typed, and this change's connector API.
  • sdk/abi/src/handle.rs stopped the rebase at the commit that retired word 8. Main's ObjectKind now states the rule itself, so the first commit retires word 8 directly under one rule text, with the const check, from_word's lookup and the census's index. No commit now renumbers kinds that a receive reports, and the separate commit is gone. A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239's test a_received_handle_is_typed_only_as_the_kind_the_kernel_reported took ObjectKind::ALL.len(), 19, as a word no kind has, which is DeviceSet's now that word 8 is retired. The test takes word 8 and the word past the last kind instead, in the first commit. The host checks at 43f760fad found it (r11/, test-sdk), and r11b/ passed with the fix.
  • system/supervisor/src/main.rs stopped it again at the commit that narrows provided connectors: the same typed launch handles, with its rights rule.
  • sdk/std/os/fs.rs and system/diskserver/src/session.rs stopped it at the commit that had moved the last uses of Connector::received to FromHandle. That conflict is with the first commit, which now does the same, so both keep the first commit's form. diskserver::held types a duplicate of its own endowed connector on its parent's word.

Two commit messages are corrected for this: the provided-connector commit no longer says a connector's kind is checked at the program's connect, and the malformed-table commit is retitled now that it no longer touches received. The first commit's message also states word 8's retirement and the typed connectors, and gave the kernel's size against 429f772.

The rebase onto 75bdec3 brought #1259 under this change. With it, a memory object's range is mapped where the caller says, SYS_SHM_MAP becomes SYS_VM_MAP, and a spawn's range may share its object's pages. #1243, #1266 and #1232 came with it. Four files conflicted, all in the first commit, and each resolution keeps both sides:

  • kernel/src/syscall/dispatch.rs: the import list has neither main's deleted sys_shm_map nor this change's deleted namespace calls, and adds sys_connect. Main's SYS_VM_MAP arm merged unchanged.
  • docs/src/using/interface.md: main's account of a child's ranges (a memory object's pages or a private copy of them), followed by this change's names in the startup block.
  • tests/guest/rust-tests/src/image.rs: this change's laid_naming now lays out through main's Image::source().
  • tests/guest/rust-tests/src/bin/spawn_image_object.rs: the block takes this change's empty list of names, and the layout takes main's Source::Copied.

Nothing that merged cleanly needed adapting:

Review round 4 rewrote the history so that each commit checks the x86-64 kernel. Until now, the 9 commits from the first through the shell's issue record left user_str dead, which that target's -Dwarnings refuses.

  • Now user_str, MAX_USER_STR, their import and their mention in kernel/pure/sched/cpu.rs leave in the first commit, together with the per-thread charge that replaced them. The commit that deleted them later is gone. The first commit's message now describes both, and the two adaptations folded into it, and gives the kernel's size at that commit.
  • Two comments are corrected in every commit that has them: Handles::table's doc loses the stdio clause the rebase brought back, and system/diskserver/src/session.rs says a session reaches its service through the same connector, not the same name.
  • cargo check -p kernel --target x86_64-unknown-none passes (exit 0) at each of the 22 commits from 8188095 to 5dd83dc (kcheck-range/, with the rewrite's edits in fold_edits.py). It passed again at each of the 23 commits on 75bdec3 (kcheck-range2/). It passed, together with cargo check -p toyos --all-targets, at each of the 22 commits from a7ca8ad to c37b68a61 (kcheck-range5/). Now both pass at each of the 23 commits from d1cefba to e4c6da589 (kcheck-range6/), which differ from 19efa90's range only in the supervisor's import line, which neither check builds. The control, the old first commit 150fcb8, fails (exit 101) on the dead user_str in every run. Only those checks ran at each commit; the other build shapes ran at the head.
  • Each rewritten commit's tree differs from its original only by those edits.

The earlier rebase, onto 8188095, brought #1246 (a thread carries its process's handle table, with_handles), #1235's block-session silence bound, #1226, #1216, #1260 and others under this change. Five files conflicted, and each resolution keeps both sides:

  • kernel/src/loader/mod.rs: main wraps the child's table in process::Handles::new(handles, pager); this change's commit answers (table, held) with no Endowments.
  • kernel/src/loader/start.rs: the spawn's commit keeps this change's logic (no labels, no job re-check, the kernel's two handles first, handed sized once) under main's lock on the caller's table (handles()), not ProcessData's.
  • kernel/src/process.rs: ProcessData::handles stays main's Arc<Handles>, and this change's Endowments stays deleted.
  • kernel/src/syscall/ipc.rs: main's namespace syscalls, moved onto with_handles, stay deleted; sys_connect looks its connector up through with_handles.
  • system/diskserver/src/session.rs: main's ClientEnds and a fresh region per reconnect, with this change's handshake through a connector.

Two adaptations that did not conflict are folded into the first commit:

History before the rebase, carried over unchanged:

  • 041f1b8: review round 1.
  • 85de72b: round 2's text.
  • ec556f5: round 1's SYS_SYSINFO charge also charged a header-only call, so memory_total() could panic under exhaustion. Now only a roster is charged.
  • d5b1749 and f0ef912: two issue records, records only. They are now d9339fd and d3d4000.
  • 90ef886: round 3. SYS_SYSINFO checks its thread bound after the header-only return, and ps names both causes of a refused roster.

Host checks at e4c6da589, a clean tree (r13/). 19efa90 differs from it only in the supervisor's import line, in every commit. No host suite or clippy shape builds the supervisor, so these hold for 19efa90:

  • the host suites of every touched crate: toyos-abi, toyos, the kernel library with sched-check, toyos-manifest, toyos-session, toyos-swap, shell-copies, toybox-copies, and disks, diskserver, fileserver, netstack, nvme, pkg, session, soundserver and sshserver;
  • every kernel clippy shape of build/clippy.rs: host, host with the kernel's controls, x86_64 default, both actuator sets, the instrument set, the two heap-band sets, aarch64 default, both actuator sets and its instrument set;
  • clippy on toyos (all targets and fd-table), the libraries, shell-copies, toybox-copies and toyos-abi.

All exit 0. The image (--build-only) exited 101 at e4c6da589 on the unused import, and exits 0 at 19efa90 (r13b/).

Guest crates compiled at 19efa90 against its sysroot (r13-guest/): rust-tests, test-runner, osbench, metalprobe, hangupholder, inputsink, pciholder and starveling exit 0.

  • kernelprobe refuses every target but AArch64, by design, and is not compiled here.
  • pagerprobe converts toyos::OwnedHandle into std's without declaring the SDK's std feature, which provides that conversion. This change touches neither, and the harness's build of the testcases image, which carries it, has built it.

Host (all at 041f1b8)

  • cargo run -q -- --build-only exits 0: build/build-only-041f1b81e.log.
  • Host suites of the touched crates exit 0: host-r6/. They are -p toyos-abi, -p toyos, -p kernel --lib --features sched-check, -p toyos-manifest -p toyos-session -p toyos-swap and -p shell-copies, plus disks, diskserver, fileserver, netstack, nvme, pkg, session, soundserver and sshserver by manifest. The full --ci host is not run, and the merge queue runs it.
  • clippy, with the adopted lints and -D warnings, exits 0: clippy-r6/. That covers the kernel's host tests, x86_64 with and without actuators, aarch64, toyos with all targets and with fd-table, the three libraries, toyos-abi, and shell-copies with all targets. The host-buildable programs listed above were tested, not linted, locally.

The shell's race (unchanged since round 0)

  • cargo test -p shell-copies: shell-race/.
    • With a never-listening child, every one of 12 orders ends the child.
    • With a listening child, every one of 40 orders reads the interrupt once, or twice only in the recorded window.
    • The negative control mut-landed.patch reds [Quit, Land, Look, Heard] (exit 101, restored), and the baseline exits 0.
  • The first stall was intermittent. Rerun 43060146035, at 1fa5d42 with no fix, was green; the fix comes from reading the code.

Guest rows, now the only integration evidence

  • 43146959560 at 19efa90: all 10 pass. It is the landing run. It covers:
    • x86-64: a_launch_gives_each_unsent_stdio_slot_a_null, session_lifecycle, libc_children, kernel_objects_refused_alone and a_clean_stop_proves_its_image, each one of MACHINE_TESTS;
    • AArch64: virt_user_mode, virt_user_copy_faults, virt_first_entry, virt_jobs_at_el2 and virt_smp, each one of SCREEN_TESTS.
    • It replaces 43135226971, which passed all 10 at e76eba7 on a7ca8ad (verdicts) but whose head could no longer merge. 43134232757, at fc9c1e6 on 429f772, was cancelled while queued, when main moved. The same ten rows passed at 39fa4bb, on 75bdec3, in 43123357772 (verdicts). Before that, I cancelled 43112704482 at 64eb00e after it had started, because a rebase changed kernel dispatch, and 43112666315, which took the stale 90ef886.
  • 43107173240 at 90ef886: kernel_objects_refused_alone, a_clean_stop_proves_its_image and session_lifecycle, all 3 pass (verdicts).
  • 43102568233, the control: the two rows red at 041f1b8, run at main e10b6fd, both pass (verdicts).
  • 43092179151 at 041f1b8: 18 of the 20 rows pass (verdicts and what the reds said). The two reds:
    • kernel_objects_refused_alone: a pipe's first write succeeded while the test held every frame. While it held them, the Log file server (pid 9) ended, freeing its memory. Each restart then panicked creating its pager (ResourceExhausted, fileserver/src/main.rs:290).
    • a_clean_stop_proves_its_image: the 16550 file that the harness read as the first loader pass held no loader line.
  • The 20-row set:
    • x86-64: session_lifecycle, a_launch_gives_each_unsent_stdio_slot_a_null, a_row_reaches_its_own_state_and_no_other, osbench_capability_groups_exit_0, a_spawn_from_an_image_frees_it_when_the_child_ends, a_user_copy_faults_in_mid_copy_or_ends_as_bad_address, libc_children, netstack_holders_exported_to_children, usbd_drives_the_spare, kernel_objects_refused_alone, memory_floor_holds_the_controls, a_window_is_drawn_at_the_scale_and_relaid_at_the_next, the_data_partition_beside_the_stick_is_designated, a_clean_stop_proves_its_image and nvme_disk_boots;
    • AArch64: virt_user_mode, virt_user_copy_faults, virt_first_entry, virt_jobs_at_el2 and virt_smp.
  • Earlier heads:

Owed on metal: the benches are down, and metal is no longer a landing condition. The record is issues/the-kernel-is-small-interrupts-post-and-threads-wait.md; the orchestrator runs these on main when a bench returns.

  • The endow-a boots (testcases, shared-debug and proctreecase) on the T14 and the AMD bench. Main must show:
  • The spawn A/B: main before this PR against main after, in one session on both benches. The rows are spawn_cost and osbench's spawn rows (system's process spawn, the procs group). This change's effect on spawn cost is not yet measured on metal.
  • Not owed, and not run: the sys_connect negative control (a Rights::NONE mutant image, mut-connect.patch). It is a control image rather than a row of main. The other kernel control the design named, endowments installed before the kernel's own two, is an assert_eq! on every install, so a boot fails on it at once.

Risks and open items

  • Spawn cost, unmeasured. A helper spawn now makes one duplicate per inherited connector where it made one for the namespace, and the commit moves that many more handles under the caller's lock. The A/B is owed on metal (above). If it shows a slower spawn, the remedy to measure is a spawn entry that copies rather than moves.
  • The shell's residual window is recorded with the exit that would close it, a kernel change to the quit's rule.
  • Found on main, not caused here, recorded in this PR (records-only commits d9339fd, d3d4000 and 135fcb4; 19efa90 adds to the owed-on-metal record). The round-3 review found no path from this PR to either red:
    • issues/a-file-server-whose-pager-is-refused-for-memory-panics-blaming-its-row.md: the Log server's restarts under exhaustion panic at fileserver/src/main.rs:290 with a false message, until the supervisor closes its ports for the boot.
    • issues/kernel-objects-refused-takes-every-frame-while-its-own-line-may-still-be-written.md: the test's window race. Control 43102568233 at main passed both rows. One green run cannot tell an intermittent race on main from a defect of this PR, so the record's case still rests on reading the test, and 135fcb4 says so in the record.
  • osbench's IPC rendezvous used to open through a one-entry namespace and now connects directly, so that row is not comparable across this change.

🤖 Generated with Claude Code

Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from 1fa5d42 to 2bbd170 Compare November 11, 2026 18:14
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1252 at 6aff197 (base main 6d0cb3a), round 1

Architectural assessment

The core change is right and well executed. Naming handles is userland's job, and the kernel now does only what userland cannot: it fixes slot order (SELF, NOTICE, then endowed(i)), moves handles, and checks WRITE on connect. The names table belongs to the parent/child startup protocol. It lives in toyos::startup rather than toyos-abi, and the child checks it once as a pure function. SpawnArgs::job as a borrowed handle removes the move-then-recheck dance. The supervisor's four handles sit at stated ABI positions. ObjectKind word 8 is retired and held by a compile-time check. The shell race fix puts every notice read under one lock, and a model that runs the real quit.rs backs it up. The model's kernel side matches toyos_proclife::quit (Notice::under, decide).

I checked that the kernel no longer names namespaces, endowment labels, user_str or SYS_ENDOWMENTS (git grep of kernel/). The only hits are ACPI and VT-d uses of the word. Every former SDK, system and test user is converted. Kernel lines: 55,098/211 → 54,732/210, verified by git ls-tree. Net change: production (kernel, sdk, system, lib) +1,537/−1,940; guest tests +437/−506; shell-copies +203; docs and issues +110/−138.

Three things need fixing before landing: the provided-connector path in the supervisor, the per-thread charge's coverage claim, and a handful of leftover "namespace" texts.

Required (BLOCKER)

1. A provided connector without WRITE can never be used by anyone, yet the manual says it exists to be passed on.

  • Where: system/supervisor/src/main.rs:3109-3121 and :3347 (FORWARDED), docs/src/architecture/authority.md:104-107, sdk/toyos/src/endow.rs:43.
  • Why the claim is false:
    • Rights only narrow (HandleTable::rights_to_move, kernel/src/object/handle.rs:310-319).
    • The receiving branch moves the client's handle exactly as sent (:3117).
    • Take a row that does not receive X. It holds provide:X as DUP|TRANSFER. Its helpers inherit that copy (still no WRITE), and its launches forward it (still no WRITE). So does every program below it, including one whose row does receive X.
    • service() then turns the PermissionDenied into NotEndowed.
    • So the class has no consumer. All it does is put a dead handle in every non-receiving launch (e.g. launch terminal from a shell), plus one more dup per helper spawn. The manual's "for it to pass on to what it launches in turn" describes a chain that cannot work.
  • Why main didn't have this problem: there, every provided extra went into the namespace, whose open needed no right on the connector.
  • No current manifest depends on the chain: shell and toybox both receive surface, and locale is toybox.
  • Fix: the simplest is to give a provided connector only to a row that receives its name. That deletes FORWARDED, the branch, the PROVIDE_PREFIX doc clause and the manual sentence.
  • Who decides: this reverses half of orchestrator decision 1 ("endow forwarding rows without WRITE"), so it goes back to the orchestrator.

2. The receiving branch no longer checks the client's provided handle at the launch.

  • Where: system/supervisor/src/main.rs:3117.
  • What main did: it duplicated the connector, which needed DUP, and SYS_NAMESPACE_BUILD checked the type. Either failure refused the launch ("a provided connector cannot be duplicated").
  • What happens now: the handle is moved with whatever rights the client left on it.
    • Without DUP, every helper spawn of the launched program fails: Helper::spawn ?s on try_clone_to_owned of each inherited service (sdk/toyos/src/spawn.rs:275).
    • Without WRITE, the program silently gets NotEndowed.
  • Fix: endow_narrowed(&label, connector.into(), (DUP|TRANSFER|WRITE).bits()), so the kernel refuses the launch at the supervisor's boundary as it did before.
  • After A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239 lands, Connector::from_handle(handle) at :2398 can become a kind-checked typed and also restore the type refusal. The comment at :3109-3113 needs updating either way.

3. The new per-thread charge does not cover what its comment and commit message say it covers.

  • Where: kernel/src/loader/start.rs:40-55, commit message of 2bbd170 ("what its largest call copies in").
  • The comment's contract is "what one of its calls copies in and may hold while it is preempted".
  • SYS_SYSINFO holds more than a spawn, and holds it while preemptible:
    • Its roster buffer is 48 B plus 64 B per live thread, up to 65,536 threads (kernel/src/syscall/machine.rs:273).
    • sysinfo_laid builds it under the table lock, but out.write_at(0, &laid) (:266) copies it out after the lock drops. That copy can fault and wait on a pager.
  • The new call term comes to about 8.5–9 KiB. That is my estimate from the types (Map ≈ 56 B × 64, path 4096, endowments 768); I did not measure it. The roster exceeds it past roughly 130–140 live threads. The old 132 KiB covered rosters up to about 2,000 threads.
  • So ordinary ps on a populated machine went from charged to drawing on the kernel's kept superframes while it waits.
  • Smaller gap: the spawn's own handed vector is held across revoke() after the lock drops (:160-176): 24 B per moved device claim, up to 96 claims. It is not in the term either.
  • What I checked: moving lives entirely under the process Lock, which disables preemption, so leaving it out is correct.
  • Fix, either:
    • charge the roster on the caller's job for the call (exact, and refused ResourceExhausted at its boundary), or
    • word the comment and commit claim truthfully and record the roster gap in issues/.
      Add the handed term, or say why it is left out.

4. Some "namespace" texts survived the PR's own sweep (f01894f). Each now describes something that no longer exists.

  • sdk/toyos/Cargo.toml:6: the package description still lists "namespaces" among the SDK's contents.
  • sdk/window/src/lib.rs:590 and sdk/window/src/watcher.rs:97: "in this program's namespace".
  • issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md:139: "Its namespace is … login row". This PR already edits that file at :173.

Suggestions (optional)

  • Connector::received (sdk/toyos/src/port.rs:101) is used where nothing was received: diskserver::held (system/diskserver/src/session.rs:395) wraps a copy of the process's own endowment, and std adopt_services (sdk/std/os/fs.rs:76) wraps the supervisor's own connectors. Only lib/session/src/lib.rs:293-294 is a real receive. Keeping the name for real receives makes A receive reports what each handle names, and a typed handle is made of a received one only where the kernel said so #1239's swap to typed mechanical.
  • adopted_services_stay tests, as a guest test, an SDK property that holds by construction: inheritable() reads only Endowments::services(), never ADOPTED. The orchestrator asked for a test to close the record, so leave it if wanted. Its header's justification ("what the kernel moved into its table") is weak.
  • The manual says the child exits "with the reason on its standard error". MALFORMED_LINE (sdk/toyos/src/endow.rs:366) only says the table is malformed; one static string per Malformed variant would make the claim true at no cost.
  • PR description: "clippy … exits 0 on every touched shape" overstates. The host-buildable programs changed here (disks, diskserver, fileserver, netstack, nvme, pkg, session, soundserver, sshserver) were tested but not linted. The merge queue lints them; correcting the description is the orchestrator's job.

Settled points the brief asked me to judge

  • Slot and names ABI. The checks sit at the right boundary.
    • endow_count is bounded before it is multiplied; duplicate handles are refused before anything is removed.
    • assert_eq! on every install holds the slot order. A wrong order would be a kernel invariant violation, so failing loudly is correct.
    • The child checks the table whole, once (startup::names). Bounds use checked arithmetic, and the early pre-relocation walk takes at most 96 entries and cannot panic.
    • Every svc: label is inherited by helpers; a narrowed provide: one is affected by finding 1.
  • WRITE on connect. Correct: get_sent::<Connector>(h, WRITE) gives InvalidArgument for the wrong type and PermissionDenied for a missing right. Every minted connector carries WRITE through initial_rights. Swap's WRITE-only endowment works because swap only connects.
  • Shell race. The fix is correct.
    • Both threads take the notice only under ASKED, and landed runs after every spawn site (main.rs:659,756,785).
    • The if guard drops before process_quit.
    • The model's mutation reds [Quit, Land, Look, Heard] (exit 101).
    • The leftover window, where a child is asked twice, is honestly recorded, with the kernel as its exit.
  • Charge. A spawn's own heap is covered except for handed; see finding 3.
  • Manual. It names no code.
  • Closed records. storage-rows-… follows the closing rule: the commit carries the story and nothing outside issues/ cites it. a-namespace-is-… is resolved by this design.

Interfaces other PRs inherit

Evidence

  • Guest run 43075819858 at 2bbd170: completed, 20/20 PASS. That includes libc_children (with tty_interrupt), memory_floor_holds_the_controls, kernel_objects_refused_alone and all 5 aarch64 virt_* rows. The aarch64 rows are needed because the loader is shared code. Head 6aff197 adds only #[derive(Default)], so reusing this run is valid.
  • Host, at 2bbd170: all exit 0.
    • --build-only, the touched host suites and the listed clippy shapes.
    • shell-copies at 6aff197.
    • The SDK mutations (mut-inherit, mut-twice) were run at 8f5cfaf, which is acceptable.
  • No automated test covers provided connectors on either branch. For the fix to findings 1–2, reading the code against moved_rights's narrowing coverage is enough.
  • Metal, once the blockers are fixed. If the fixes touch the supervisor's start() or sysinfo, re-stage A at the new head, or re-run the launch rows (a_launch_gives_each_unsent_stdio_slot_a_null, session_lifecycle).
    • A (testcases, shared-debug, proctreecase, both benches): every job green, in particular:
      • endowment_denied, abuse_handle_table (97 refused, 96 start, digits at their places), abuse_connect_flood (table untouched), abuse_listener_hijack, user_copy_faults, adopted_services_stay, handle_lifetime, process_tree and launch_toctou;
      • no kernel panic from the slot-order asserts;
      • spawn_cost within its guard.
    • B (spawn_cost and the osbench spawn rows, head against base at 1fa5d42/b56693152, same session, both benches): no spawn regression beyond run-to-run spread. A regression stops the PR with copy-instead-of-move measured, per the brief. The osbench IPC rendezvous row is not comparable across this change.
    • C (the sys_connect Rights::NONE mutant, T14): endowment_denied reds at "a connector without WRITE connected" and nowhere else.

SEND BACK: findings 1–4. Finding 1 needs the orchestrator's decision first.

Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from 6aff197 to d3643d8 Compare November 11, 2026 18:59
Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from d3643d8 to 041f1b8 Compare November 11, 2026 19:52
Japabu added a commit that referenced this pull request Nov 11, 2026
The shared boots and the spawn A/B this change was to be judged by, named
with what main must show, for the batched boot of main when a bench returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1252 at b3ac663 (code 041f1b8, base main e10b6fd), round 2

Architectural assessment

Round 1's design assessment stands. The fixes are small and in the right places:

  • The supervisor now gives a provided connector only to a row that receives its name, and narrows it to DUP|TRANSFER|WRITE. The kernel's existing rights_to_move refusal is the only check needed.
  • The roster's charge lives in sys_sysinfo, the call that holds the roster.
  • handed joins the per-thread spawn term, and the comment now claims only what a spawn holds.

The rebase kept #1225's routing exactly. Changes since round 1, excluding main: code +59/−37 (kernel, sdk, system), tests +14/−12, docs and issues +4/−5. I verified the kernel count with git ls-tree: 54,993/211 at e10b6fd → 54,641/210.

Two text-only problems remain. Finding 4 is not fully resolved, and two texts the fix itself wrote are inexact.

Round-1 findings

1. Forward-without-WRITE: resolved.

  • FORWARDED, the else branch, the manual's pass-on sentence and the PROVIDE_PREFIX clause are gone (system/supervisor/src/main.rs:3114-3118, docs/src/architecture/authority.md:101-106).
  • git grep finds no other forwarding text outside issues/.
  • The shell still forwards provided() as duplicates. A receiving row gets a handle carrying DUP, so helpers and onward launches keep working.

2. The client's handle is checked at the launch: resolved.

  • endow_narrowed(.., PROVIDED.bits()) hits HandleTable::rights_to_move (kernel/src/object/handle.rs:310-319). A handle without TRANSFER, or with a want it does not carry, is refused as HandleError::Rights.
  • refuse_as_error maps that to PermissionDenied. That is not a fault, so the supervisor is not ended.
  • The refusal comes before any removal (kernel/src/loader/start.rs:142-150). It reaches the launch's Err(e) arm, which answers MSG_REFUSED (main.rs:2760-2763).
  • The type is still checked only at the program's connect, as decision 1 records.

3. Charge coverage: resolved.

  • sys_sysinfo charges charge::array::<u8>(laid.capacity()) to the caller's job across write_at (kernel/src/syscall/machine.rs:263-267). _held is a named binding, so it lives until the copy-out ends. A refusal is ResourceExhausted, which toyos_abi::syscall::sysinfo answers as 0, as its new doc says.
  • KernelStack::heap adds charge::array::<pcidev::Handed>(MAX_SPAWN_ENDOWMENTS).
  • handed is reserved once at the first claim for count - i. Later reserve_exact calls cannot reallocate, because capacity ≥ len + (count − i) holds for every later i, so it never exceeds MAX_SPAWN_ENDOWMENTS entries.
  • The comment and commit message now claim only a spawn's holdings.

4. Leftover namespace texts: partly resolved. The four texts I named are fixed, along with three image comments and the soundserver policy comment. Others remain; see B1.

The three suggestions were taken:

  • Each Malformed variant has its own line.
  • Connector::received is used only in lib/session.
  • adopted_services_stay gives its reason.

The PR description now says the host-buildable programs were tested, not linted.

Rebase: #1225's routing

Main's #1225 (e10b6fd) touched only files.rs, files/namespace.rs and files/owner.rs.

  • routing.rs against main's namespace.rs:
    • route, Routed, Directories::{holding, node, children} and every test body are identical.
    • The differences:
      • Directories::new is generic over AsRef<str>;
      • two test call sites drop their String::from;
      • one test is renamed.
  • owner.rs against main: it differs only where main read the kernel's namespace.
    • directories() reads endow::services() and so becomes infallible.
    • capability() keeps A path is routed through the view's own names, asking the kernel for none #1225's shape: cache, connect, the refusal is the call's. Dir::named answers NotHeld for a name not held, and reconnect maps PermissionDenied to NotHeld. Main mapped namespace NotFound to NotHeld.
    • The routed directory set has the same members. Main's namespace held both row and provided names, and services() yields both, plus the supervisor's adopted names, which main's adopt_namespace held.
  • Overlap with other main commits since round 1 (process.rs, syscall/machine.rs, pure/sched/cpu.rs, the kernel-is-small record): auto-merged. Only this branch's own edits differ from main there.
  • Readdir: the expect at owner.rs:752-753 still holds by construction. A name's component is shorter than its u16-length-prefixed table entry, and encode_entry's bound is u16.

Required (BLOCKER)

B1. Round-1 finding 4 is not complete. These texts still describe the deleted namespace:

  • tests/guest/osbench/src/ipc.rs:157-159: "ToyOS through a port in a namespace this process built". This is now false: Rendezvous (tests/guest/osbench/src/sys/toyos.rs:238-250) connects directly through its own connector. The PR body itself says this row is no longer comparable across the change, so the comment misdescribes what the benchmark measures.
  • system/console/src/main.rs:156-157: "whose connector goes into the namespace of the shell it spawns". It is now provided as provide:surface.
  • tests/metal/src/image.rs:14-15: "the power connector, which the applet inherits with the runner's namespace". It inherits it as one of the runner's services.
  • sdk/std/sys/fs.rs:1: "namespace ancestors". These are the read-only ancestors of its fs: services. main's module was files::namespace, now routing.

sdk/libc's "filesystem namespace" and "capability namespace" use the word for the path space, so leave them.

B2. Two texts written by the fix for findings 1–2 are inexact:

  • system/supervisor/src/main.rs:3111-3112: "another could not use it, nor could anything it starts". That is the reasoning of the deleted FORWARDED design. In this code, a row that does not receive the name gets nothing because its row decides its services, not because it could not connect. Delete the clause.
  • sdk/std/os/process.rs:65-66 (std's public provide contract): "a connector without DUP, TRANSFER and WRITE refuses the launch" reads as unconditional. The supervisor drops the extra unchecked for a row that does not receive name (main.rs:3115), and that launch succeeds. Suggested wording: "…is not given it; one that is, without DUP, TRANSFER and WRITE, refuses the launch."

Both are comment-only changes. Under the guest-run rule they need no new dispatch, and 43092179151 at 041f1b8 stays the evidence.

Suggestions (optional)

  • sdk/toyos/src/startup.rs:206: "its endowment table's length is not its length" says the same thing twice. The variant's doc says "length word"; the line could too.

Evidence

  • Guest run 43092179151 at 041f1b8: still queued when this review was written, with no verdicts. Under the orchestrator's landing rule, its green rows are still owed.
    • Its 20 rows cover what changed since round 1:
      • the supervisor's launch path: a_launch_gives_each_unsent_stdio_slot_a_null and session_lifecycle;
      • the roster call: session_lifecycle reads it;
      • the rebased routing: a_row_reaches_its_own_state_and_no_other;
      • the loader's handed change, on shared code: the aarch64 virt_* rows.
    • The earlier 20/20 run (43075819858 at 2bbd170) predates the fixes and the rebase, so it does not substitute.
  • My local runs at b3ac663 (code identical to 041f1b8), all exit 0:
    • tests: cargo test -p toyos (331 passed, including all five files::routing tests), -p toyos-abi (54), -p kernel --lib --features sched-check (461), --manifest-path system/diskserver/Cargo.toml (15);
    • clippy with the adopted lints and -D warnings: -p toyos --all-targets, -p kernel for x86_64 with and without boot-actuators,test-actuators, -p kernel for aarch64, and diskserver --all-targets.
  • The author's evidence: host-r6/ and clippy-r6/ are at 041f1b8, and each log ends exit 0. --build-only at 041f1b8 exits 0.
  • No test covers provided connectors at launch. Reading the code against rights_to_move settles fix 2, so no further check is needed.
  • Owed on metal: the metal list in issues/the-kernel-is-small-interrupts-post-and-threads-wait.md:416-431 matches round 1's A and B. It drops C, the sys_connect mutant, as a control image rather than a row of main, and the PR body says so. Under the benches-down ruling, none of it is a landing condition. That includes the closure evidence for storage-rows-…, adopted_services_stay, which runs only on metal. The property itself holds by construction: inheritable() reads only endowed services.

SEND BACK: B1 and B2. Both are text-only, need no new guest run, and the PR then waits only on 43092179151.

Japabu added a commit that referenced this pull request Nov 11, 2026
- osbench's IPC rendezvous, the console's surface port, the metal image's
  `power` connector and std's files module now say what each is: a port's
  own connector, one provided to the shell, one of the runner's services,
  and the read-only ancestors of the directory capabilities.
- The supervisor's provided-connector comment drops the deleted design's
  reasoning: a row that does not receive the name gets nothing because its
  row decides its services.
- std's `provide` says the refusal applies only to a child whose row
  receives the name.
- A malformed table's length line names the length word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the round-2 text fixes (B1, B2, the startup.rs suggestion) at 85de72b: against code head 041f1b8 the only non-comment change is the Length refusal's message text ("length word"); the rest is comments and docs in 8 files (read with git diff). No rerun owed: guest run 43092179151 at 041f1b8 remains the landing evidence; #1252 queues when it is green.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1252 at f0ef912 (code ec556f5), round 3

Scope: the commits since 85de72b. That is ec556f5 (kernel +7/−2, ABI doc +3/−2) and two issue-only commits, d5b1749 and f0ef912 (+70 in two new files).

Architectural assessment

The fix is in the right place and is as small as it can be. The charge stays in sys_sysinfo, the call that holds the roster across a copy-out that can wait, and now applies only when a roster is asked for. No new mechanism.

The fix: correct

  • Header-only calls are not charged. A header-only call is any buf shorter than SYSINFO_HEADER_SIZE + SYSINFO_ENTRY_SIZE, so max_entries == 0. It takes the 0 => Ok(None) arm and never reaches Charge::current (kernel/src/syscall/machine.rs:265-271).
  • The roster charge still bounds what was asked. For max_entries > 0:
    • Charge::current charges the caller's job (charge::current()).
    • The size is charge::array::<u8>(laid.capacity()). After reserve_exact that capacity is exactly 48 + min(max_entries, live) × 64.
    • _held is a named binding, so the charge lives across out.write_at and drops at return.
    • A refusal is ResourceExhausted, which toyos_abi::syscall::sysinfo answers as 0.
  • entries is still allocated and freed under the table lock, so the module's rule for transient allocations covers it, as in round 2.

Callers under memory exhaustion

  • Header-only callers are now safe. Every one goes through toyos::system::sysinfo(&mut [u8; 48]), which has no room for an entry:
    • memory_total and its asserting users: compositor session.rs:360, netstack main.rs:500, and fileserver main.rs:280 (Data and Memory only).
    • The compositor taskbar (>= 48), inspect (Err), and toybox free (prints a message).
    • The sysinfo fork at bcb2381 (.then, so None).
  • Roster callers reach the new refusal. These are SysCap::roster users:
    • osbench: an empty map.
    • The guest tests that assert: none of them runs with frames exhausted.
    • ps: it does not panic, but it is now told something false. See B1.

Required (BLOCKER)

B1. system/toybox/src/roster.rs:19-20 and system/toybox/src/ps.rs:72,77: ps blames a missing right for an exhaustion.

  • The doc says "None is a capability carrying no ROSTER, which the kernel answers with nothing", and ps exits with "refused — this capability carries no ROSTER".
  • Since round 1's charge, the kernel also answers 0 when the caller's job has no room for the roster. toyos_abi::syscall::sysinfo folds both words into 0, so ps cannot tell them apart.
  • Consequence: under memory pressure, ps tells its user to fix a capability that is fine.
  • Neither file is in the branch. The PR's kernel change made them false.
  • Fix: name both causes in the doc and the message. The alternative, giving SysCap::roster the word (Result), is a larger change and is optional.

Suggestions (optional)

S1. machine.rs:289-292: the thread bound still refuses a header-only call.

  • This is on main too. sysinfo_laid refuses past sysinfo_thread_bound() (65,536 live threads) before the header-only return at :306.
  • The bound is per machine, while MAX_THREADS (4096) limits each process. On a large machine, 17 full processes pass it.
  • Past it, memory_total panics in the compositor, the netstack and the file server, although the bound only protects the roster's allocations.
  • Checking the bound after the max_entries == 0 return makes the new ABI sentence ("A header alone is never refused for memory") and the comment's "never refused" exact.
  • If you decline, record it in issues/.

Test for the header-only path

No test runs a header-only call under exhaustion, and none is needed:

  • Inspection settles it. The only charge in sys_sysinfo is in the _ arm of a match on max_entries.
  • A guest test could not reliably expose round 1's defect.
    • Charges are admitted in whole superframes (Frames::charge, kernel/pure/frames.rs:1009-1042), and the shared pool keeps one superframe of slack after an uncharge.
    • So a 48-byte charge is refused only when the caller's pool is within that many bytes of its entitlement.
    • kernel_objects_refused stops at a refused null object. It does not control that remainder, so such a test would be a nondeterministic control.
  • Existing coverage. Header-only behaviour without exhaustion is held by endowment_denied's the_roster_is_a_right_and_the_header_is_not: a header from a buffer one byte short of an entry, and from HANDLE_INVALID.

The reds of 43092179151: no path from #1252 found

a_clean_stop_proves_its_image

  • The 16550 file was empty when it was read. The dump opens with the blank line that loader_log puts between that file and the console (tests/common/qemu.rs:1788-1791), and every line after it is the console's.
  • The branch does not touch loader/, tests/common/ or the row.
  • The bootloader's only changed dependency is toyos-abi. There the branch adds boot::supervisor, which the bootloader does not use: it uses KernelArgs, LAYOUT, SEED_LEN, SLOT_PARAM, tokens and log::Head.

kernel_objects_refused_alone

  • The test is byte-identical to main's.
  • The restarts' panic is main's code (main.rs:297 at e10b6fd).
  • On a running Log server's per-request path, the branch changes only two things, and each still creates one channel as main did:
    • Exports::connect: a direct connect, where main opened through its namespace.
    • The diskserver Session handshake after a block-service restart.
  • The branch lowers each thread's kernel charge (from about 132 KiB to about 9 KiB) and drops the per-process label charge. I see no way that smaller charges make another job's allocation fail where main's succeeded. This is an inference from reading.
  • Round 1's header charge cannot be the cause. Nothing in that boot reads sysinfo in the window: the Log role's pager does not call memory_total, and the restarts died at Pager::create.
  • Why pid 9 ended is unknown, because the serial artifact was lost. Both new issue records say so and label the mechanism as inferred. They match the log.
  • The main control is what can establish independence.

Evidence

  • Mine at ec556f5 (detached worktree), all exit 0:
    • cargo test -p toyos-abi (50 + 4 passed);
    • -p kernel --lib --features sched-check (461 passed);
    • clippy with the adopted lints and -D warnings for the kernel's host lib and tests (sched-check), x86_64, and x86_64 with boot-actuators,test-actuators, and toyos-abi --all-targets.
  • The author's r7/ at 151050220 matches: 461 passed, exit 0.
  • f0ef912 adds only issue files, so these results hold for the head.
  • Guest runs: control 43102568233 (main e10b6fd) and head 43102989837 (ec556f5) were both still queued at writing, with no verdicts.
  • PR body: it still names ec556f5 as the head and does not mention the two issue commits. That is the orchestrator's to correct.

SEND BACK: B1 is text-only. It needs no guest rerun, since it changes only a message and a doc comment in toybox. Landing still waits on 43102989837 and on the control to tell the two reds apart.

Japabu added a commit that referenced this pull request Nov 11, 2026
…h refusals

- `SYS_SYSINFO` checks its live-thread bound after the header-only return.
  The bound guards the roster's allocations alone. Past it, a header-only
  call was refused and `memory_total()` would panic in the compositor, the
  netstack and the file server. This was on main too. So "a header alone is
  never refused for memory" is now exact.
- Since round 1's charge, the kernel answers a roster with nothing for two
  reasons: a capability without `ROSTER`, or a job without the room to hold
  the roster. `toyos_abi::syscall::sysinfo` folds both into 0. So
  `SysCap::roster`'s doc, toybox's roster helper and `ps`'s message name
  both causes, rather than blaming the capability for an exhaustion. Giving
  `roster` the refusal's kind would reach its nine callers, most of them
  guest tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of round 3's fix at 90ef886: B1 resolved in text (SysCap::roster's doc, toybox's roster helper and ps's messages name both refusal causes); S1 taken: sysinfo_laid checks the live-thread bound after the header-only return (read with git diff; the reviewer's suggestion as written). Lands on 43107173240 green, with the main control 43102568233 read for the two earlier reds.

Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from 90ef886 to 64eb00e Compare November 11, 2026 22:32
Japabu added a commit that referenced this pull request Nov 11, 2026
The shared boots and the spawn A/B this change was to be judged by, named
with what main must show, for the batched boot of main when a bench returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
- osbench's IPC rendezvous, the console's surface port, the metal image's
  `power` connector and std's files module now say what each is: a port's
  own connector, one provided to the shell, one of the runner's services,
  and the read-only ancestors of the directory capabilities.
- The supervisor's provided-connector comment drops the deleted design's
  reasoning: a row that does not receive the name gets nothing because its
  row decides its services.
- std's `provide` says the refusal applies only to a child whose row
  receives the name.
- A malformed table's length line names the length word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…h refusals

- `SYS_SYSINFO` checks its live-thread bound after the header-only return.
  The bound guards the roster's allocations alone. Past it, a header-only
  call was refused and `memory_total()` would panic in the compositor, the
  netstack and the file server. This was on main too. So "a header alone is
  never refused for memory" is now exact.
- Since round 1's charge, the kernel answers a roster with nothing for two
  reasons: a capability without `ROSTER`, or a job without the room to hold
  the roster. `toyos_abi::syscall::sysinfo` folds both into 0. So
  `SysCap::roster`'s doc, toybox's roster helper and `ps`'s message name
  both causes, rather than blaming the capability for an exhaustion. Giving
  `roster` the refusal's kind would reach its nine callers, most of them
  guest tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1252 at 64eb00e (rebase onto main 8188095), round 4

Scope: the rebase only. I ran git range-diff e10b6fd49..90ef8869e origin/main..64eb00ee6 and read in full the five resolutions, the two folds, #1246's Handles/with_handles, and every lock the spawn's commit takes.

Architectural assessment

The rebase keeps both designs whole.

What I checked

  • Range-diff.
    • Commit 1's patch differs only in the five resolved files, the two folds and user_copy_faults.rs's context. One hunk was lost (B1).
    • Commit 10 differs in context only (data.handles to handles).
    • The other 21 patches are identical.
  • loader/mod.rs: Handles::new(handles, pager) wraps the branch's (table, held), with no Endowments. SupervisorTable is as the branch had it. Correct.
  • loader/start.rs and lock order. commit takes the caller's Handles lock (handles(), the running thread's own Arc) and no other process-level lock.
    • Under that lock run: the rights checks, the child table's make_room (charged to the job), Own::install, the removes, take_mappings, the installs and one Vec reserve. Main ran the same set under the same lock.
    • The only addition is Own::install. It writes into the child's table, which no thread reaches yet, and takes no lock. revoke still runs after the drop.
    • spawn_job takes with_handles (or with_process_data for the default job) alone, before commit.
    • spawn takes PROCESS_TABLE only after commit returns.
    • So the rebase adds no lock edge, and this path can deadlock against A thread carries its process's handles, and a lookup takes no lock but the table's #1246's other users only where main already could.
    • Charging is kept: KernelStack::heap covers 96 TransferEntry and 96 Handed, and ProcessData::charge keeps main's charge::arc::<Handles>().
  • process.rs: Endowments, ENDOW_ENTRY_LEN and the endowment charge are gone, and ProcessData::handles is main's Arc<Handles>. The one exception is B1.
  • syscall/ipc.rs: the namespace calls stay deleted. sys_connect resolves its connector with get_sent::<Connector>(h, WRITE) under with_handles. It then makes main's room, push and install under one hold, unchanged.
  • diskserver/session.rs: main's reconnect is intact: a fresh region, ClientEnds::new, the opened equality check and the Silence/Violation endings. It now calls handshake(self.service.as_handle(), …). The diskserver suites pass, including the reconnect cases in disk::tests.
  • Folds.
    • spawn_job is correct.
    • The two messages print Guid(self.guid), the same rendering disks uses for Listed::unique.
    • No test or harness matches the old text. The nvme record's wording ("no session's server that broke the protocol, answered nothing in the bound") still describes the new lines.
  • Size. Kernel 55,155 lines in 211 files becomes 54,809 in 210 (git ls-tree at both). The diff is +2,459/-2,620 over 176 files. Both match the description.

Findings

B1 BLOCKER: kernel/src/process.rs:66. The resolution brought back a false clause.

  • Handles::table reads "Every kernel object it can name; stdio is handles 0, 1, 2."
  • On this branch, handle 0 is RawHandle::SELF, 1 is RawHandle::NOTICE, and endowments start at 2.
  • Before the rebase, the branch had removed exactly this clause from ProcessData::handles (c92bc28: "Every kernel object this process can name."). A thread carries its process's handles, and a lookup takes no lock but the table's #1246 moved that field doc onto Handles, and the resolution kept main's text. The range-diff shows the hunk is missing from 150fcb8.
  • Fix: delete the clause.

B2 BLOCKER: the description's "so every commit builds" is false.

  • cargo check -p kernel --target x86_64-unknown-none fails at every commit before d725051, from 150fcb8 to d842457. I ran it at each of the seven code commits in that range. The two record-only commits have the same code as the commits before them.
  • The error is method user_str is never used (kernel/src/user_ptr.rs:110), under that target's -Dwarnings. 150fcb8 removes user_str's last caller (SYS_NAMESPACE_OPEN), and d725051 removes the method.
  • The pre-rebase c92bc28 had the same state, so the rebase did not cause this. But the description now claims the opposite.
  • Main merges keep a PR's commits, so nine commits whose x86-64 kernel does not compile would enter main's history.
  • Fix: move the deletion of user_str, MAX_USER_STR, its String import and the cpu.rs comment's mention into 150fcb8, where the last caller goes. The alternative is that the orchestrator withdraws the claim.
  • Every other check below passes at every code commit: aarch64 (-Adead_code), the host kernel with tests, toyos and toyos-abi for all targets, fd-table, the libs, shell-copies and the nine host-built system crates.

S1 SUGGESTION: system/diskserver/src/session.rs:15-16, 333, 339-340.

  • These lines say a reconnect reaches the service "through the same name", and that "a name whose server is being restarted queues the connection".
  • The fold's own reason for the GUID messages is that a session holds a connector and no name. "Connector" is the accurate word.
  • The text predates the rebase, but the resolution adopted main's rewritten reconnect doc.

S2 SUGGESTION: 150fcb8's message.

  • "Kernel: 54,744 lines in 211 files to 54,388 in 210" counts against the old base.
  • The message also does not mention the session-message change folded into this commit.
  • If B2's history edit reopens the commit anyway, update both.

Evidence

At 64eb00e, in a detached worktree with its own target directory. All commands exited 0:

  • cargo test -p kernel --lib --features sched-check: 463 passed.
  • cargo test --manifest-path system/diskserver/Cargo.toml: 7 and 10 passed.
  • The 12 kernel shapes of build/clippy.rs, with the adopted lints and -D warnings:
    • host, and host with protocol-port and the kernel's controls;
    • x86_64 default, boot-actuators,test-actuators, boot-actuators, the instruments, heap-band-notail,heap-lockspin and heap-band-nohead;
    • the four aarch64 shapes.
  • clippy on diskserver with all targets.
  • The tree was clean afterwards.

Per commit, in a second worktree, for every commit that changes a .rs file or a Cargo.toml: results as in B2.

Not built per commit: the ToyOS-only crates (supervisor, shell, guest tests and others), which need the guest sysroot. The cheapest check is --build-only at 150fcb8. That is the one userland patch the rebase changed, and beyond the kernel its changes are diskserver (host-built above) and user_copy_faults.rs context.

Guest rows: 43112704482 (at 64eb00e) and the control 43102568233 were both still queued when I wrote this. There is nothing to read yet.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from 64eb00e to 5dd83dc Compare November 11, 2026 22:56
Japabu added a commit that referenced this pull request Nov 11, 2026
The shared boots and the spawn A/B this change was to be judged by, named
with what main must show, for the batched boot of main when a bench returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
- osbench's IPC rendezvous, the console's surface port, the metal image's
  `power` connector and std's files module now say what each is: a port's
  own connector, one provided to the shell, one of the runner's services,
  and the read-only ancestors of the directory capabilities.
- The supervisor's provided-connector comment drops the deleted design's
  reasoning: a row that does not receive the name gets nothing because its
  row decides its services.
- std's `provide` says the refusal applies only to a child whose row
  receives the name.
- A malformed table's length line names the length word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…h refusals

- `SYS_SYSINFO` checks its live-thread bound after the header-only return.
  The bound guards the roster's allocations alone. Past it, a header-only
  call was refused and `memory_total()` would panic in the compositor, the
  netstack and the file server. This was on main too. So "a header alone is
  never refused for memory" is now exact.
- Since round 1's charge, the kernel answers a roster with nothing for two
  reasons: a capability without `ROSTER`, or a job without the room to hold
  the roster. `toyos_abi::syscall::sysinfo` folds both into 0. So
  `SysCap::roster`'s doc, toybox's roster helper and `ps`'s message name
  both causes, rather than blaming the capability for an exhaustion. Giving
  `roster` the refusal's kind would reach its nine callers, most of them
  guest tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the rebase round-4 fixes at 5dd83dc: the tree differs from 64eb00e only in comments (process.rs B1; session.rs S1; read with git diff). B2: the per-commit x86-64 kernel check passes at all 22 commits (evidence kcheck-range; control 150fcb8 red). Guest run 43112704482 at 64eb00e therefore covers this head; #1252 queues when it is green.

Japabu added a commit that referenced this pull request Nov 11, 2026
Run 43102568233 ran kernel_objects_refused_alone and
a_clean_stop_proves_its_image at main e10b6fd: both passed. The record
names that result and what it leaves open: a single green run on main
cannot tell an intermittent race there from a defect of #1252, so the
record's case still rests on reading the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu and others added 9 commits November 12, 2026 05:10
`MAX_USER_STR` is gone; `MAX_HANDLES` stands where it stood.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared boots and the spawn A/B this change was to be judged by, named
with what main must show, for the batched boot of main when a bench returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- osbench's IPC rendezvous, the console's surface port, the metal image's
  `power` connector and std's files module now say what each is: a port's
  own connector, one provided to the shell, one of the runner's services,
  and the read-only ancestors of the directory capabilities.
- The supervisor's provided-connector comment drops the deleted design's
  reasoning: a row that does not receive the name gets nothing because its
  row decides its services.
- std's `provide` says the refusal applies only to a child whose row
  receives the name.
- A malformed table's length line names the length word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 1 charged every answer to the caller's job for its copy-out, the
48-byte header alone included. Under memory exhaustion a header-only call,
which nothing else refuses, was then answered `ResourceExhausted`, and
`toyos::system::memory_total()`, which asserts a whole header, would panic
in the compositor, netstack or a file server. Now only a roster is charged
for the call; a header alone is a fixed few bytes and never refused for
memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng its row

Seen in guest run 43092179151: the Log server's restarts under memory
exhaustion each panicked at fileserver/src/main.rs:290 with "its row is no
pager row" for a ResourceExhausted, until the supervisor closed its ports
for the boot. Main has the same code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ay still be written

The race behind run 43092179151's red: a process that ends inside the
test's window frees memory, and the test's refusals are judged against
memory it did not take. The mechanism is inferred; the control on main is
run 43102568233.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h refusals

- `SYS_SYSINFO` checks its live-thread bound after the header-only return.
  The bound guards the roster's allocations alone. Past it, a header-only
  call was refused and `memory_total()` would panic in the compositor, the
  netstack and the file server. This was on main too. So "a header alone is
  never refused for memory" is now exact.
- Since round 1's charge, the kernel answers a roster with nothing for two
  reasons: a capability without `ROSTER`, or a job without the room to hold
  the roster. `toyos_abi::syscall::sysinfo` folds both into 0. So
  `SysCap::roster`'s doc, toybox's roster helper and `ps`'s message name
  both causes, rather than blaming the capability for an exhaustion. Giving
  `roster` the refusal's kind would reach its nine callers, most of them
  guest tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 43102568233 ran kernel_objects_refused_alone and
a_clean_stop_proves_its_image at main e10b6fd: both passed. The record
names that result and what it leaves open: a single green run on main
cannot tell an intermittent race there from a defect of #1252, so the
record's case still rests on reading the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `testcases` boot owed on metal for #1252 now names the four shared-boot
members whose rebased forms have only been compiled: #1261's
`poll_belongs_to_its_handle`, which connects through its connector where it
opened a one-entry namespace, and `messages_are_whole`, `moved_rights` and
`mutual_kill`. No guest row runs them until #1271's shared-boot row lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endow-labels-and-namespace-leave-kernel branch from 258a5e9 to 19efa90 Compare November 12, 2026 03:15
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the rebase onto d1cefba (#1242) at 19efa90. I checked the range-diff against 258a5e9, the head review r5 marked LAND:

  • Commits 2–23 are =.
  • Commit 1 differs only in context lines at the system/fileserver, system/supervisor and system/usbd hunks, where main's toyos::admission imports now sit beside this PR's toyos::endow, and in the record one-manifest-row-grants-every-applet-and-one-is-compared.md, which now cites received where it cited build_namespace.
  • No logic changed.

The landing run 43146959560 at 19efa90 passed all 10 rows (test result: ok. 10 passed), and the body now says so. The PR is mergeable against main 3c5c555 (#1271), which conflicts with nothing here. Metal debts are listed under "Owed on metal" per the benches-down rule. Queueing.

@Japabu
Japabu marked this pull request as ready for review November 12, 2026 04:12
@Japabu
Japabu added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 302fb36 Nov 12, 2026
11 checks passed
@Japabu
Japabu deleted the endow-labels-and-namespace-leave-kernel branch November 12, 2026 04:32
Japabu added a commit that referenced this pull request Nov 12, 2026
… it out

#1252 put the child's names for its endowments into the startup block,
after the arguments and environment, in the same 2 MiB less a page. A
caller's room for its own arguments and environment then depended on how
many endowments the spawn carried and how long their names were, which
nothing bounded but the block, so no constant could say what a caller may
pass: MAX_ARGS answered as if no names were there.

A names table is now at most startup::MAX_NAMES bytes, 64 KiB, refused
past it as a spawn too big. That share of the block is many times any
table a spawn in this tree makes: as many names as a spawn endows, of
about 680 bytes each. MAX_ARGS leaves it out, so a caller's MAX_ARGS bytes
fit whatever the spawn endows. _SC_ARG_MAX is 2023395.

The MAX_ARGS test now spawns with one name filling a table's most bytes,
and startup's test holds a table of exactly MAX_NAMES bytes and refuses
one byte more. 206_libc_refusals.expect and the owed-on-metal record say
2023395.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
… rights

`HandleTable::get_sent`, the lookup of a handle a peer sent (`SYS_CONNECT`'s
connector, `SYS_SPAWN`'s place and job), checked the handle's rights before
its kind, so a handle of another kind that also lacked the right answered
`PermissionDenied`. Its own contract, `connect`'s and the spawn fields' ABI
docs all answer such a handle `InvalidArgument`, and a right means something
only on its own kind. `connect` through an acceptor, which carries no
`WRITE`, is the case `abuse_listener_hijack` holds, and it went red in
`testcases_members_exit_0` once #1252 made connecting need `WRITE`.

The kind is now checked first. A place or a job of another kind without
`WRITE` answers `InvalidArgument` too, as their docs already say.
`WrongType`'s doc says `get_sent` never builds it, and `process_tree`'s
`a_pipe_is_no_place` no longer explains its refusal by the old order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
The adopter, which std's `adopt_services` gives `fs:/system`, then checked
that `toyos::endow::services()` listed it. std links its own copy of
`toyos`, and each copy keeps its own adopted services: std's adoption filled
std's copy, and the program's listing read its own, which adopted nothing.
So the check went red in `testcases_members_exit_0` (run 43156966054,
`assert_failed::<Vec<&str>, [&str; 1]>`).

- The adopter still checks, through its own copy, that it was endowed no
  service: that copy never adopts, so the check holds at any point. The
  check after the adoption goes. The helper's spawn proves the adoption,
  since std reads the helper's image through `/system`, which std holds
  only by adopting it.
- std's `adopt_services` says what ends its adoption: std's own resolution
  of a file or lookup of a service.
- `issues/a-programs-own-toyos-never-sees-the-services-std-adopted.md`
  records the two adopted sets, the defect #1226 closed for stdio's state.
- The owed-on-metal record for #1252 says `testcases_members_exit_0` runs
  the members #1252 adapted on QEMU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…d nothing asked costs one branch

- The Trust page names SSBD among what a program that asks gets, and says
  any program may ask for a child it starts (B2).
- in_force's doc says what it holds in force by construction (B3, in the
  commit before this one, rebased).
- The per-CPU flag whether IA32_SPEC_CTRL exists is the test kernel's
  alone, for its debug read (B4).
- A CPU whose decision names nothing on request returns from switch_to
  after one load (S1). install(), called beside report(), gives each CPU its
  switch state, and the base it installs is the IA32_SPEC_CTRL its bring-up
  read back rather than a second decision (S2, S7). LastAsked names a pid
  (S3).
- speculationprobe refuses an error answer (S4). speculation_on_request
  says it holds nothing where CPUID enumerates no IBPB, as TCG's qemu64
  does not, and states its targets at its registration (S5); back on the
  shared CPU after the probe it reads the asked bits clear (S6).

Rebased onto main 302fb36: #1252's spawn path takes the job and no
labels, and the ask rides PendingHandles beside the pager as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
… it out

#1252 put the child's names for its endowments into the startup block,
after the arguments and environment, in the same 2 MiB less a page. A
caller's room for its own arguments and environment then depended on how
many endowments the spawn carried and how long their names were, which
nothing bounded but the block, so no constant could say what a caller may
pass: MAX_ARGS answered as if no names were there.

A names table is now at most startup::MAX_NAMES bytes, 64 KiB, refused
past it as a spawn too big. That share of the block is many times any
table a spawn in this tree makes: as many names as a spawn endows, of
about 680 bytes each. MAX_ARGS leaves it out, so a caller's MAX_ARGS bytes
fit whatever the spawn endows. _SC_ARG_MAX is 2023395.

The MAX_ARGS test now spawns with one name filling a table's most bytes,
and startup's test holds a table of exactly MAX_NAMES bytes and refuses
one byte more. 206_libc_refusals.expect and the owed-on-metal record say
2023395.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…d nothing asked costs one branch

- The Trust page names SSBD among what a program that asks gets, and says
  any program may ask for a child it starts (B2).
- in_force's doc says what it holds in force by construction (B3, in the
  commit before this one, rebased).
- The per-CPU flag whether IA32_SPEC_CTRL exists is the test kernel's
  alone, for its debug read (B4).
- A CPU whose decision names nothing on request returns from switch_to
  after one load (S1). install(), called beside report(), gives each CPU its
  switch state, and the base it installs is the IA32_SPEC_CTRL its bring-up
  read back rather than a second decision (S2, S7). LastAsked names a pid
  (S3).
- speculationprobe refuses an error answer (S4). speculation_on_request
  says it holds nothing where CPUID enumerates no IBPB, as TCG's qemu64
  does not, and states its targets at its registration (S5); back on the
  shared CPU after the probe it reads the asked bits clear (S6).

Rebased onto main 302fb36: #1252's spawn path takes the job and no
labels, and the ask rides PendingHandles beside the pager as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant