Skip to content

A child's arguments and environment are memory its parent lays out - #1200

Merged
Japabu merged 11 commits into
mainfrom
startup-block-env
Nov 11, 2026
Merged

Japabu merged 11 commits into
mainfrom
startup-block-env

Conversation

@Japabu

@Japabu Japabu commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The kernel copied every spawn's argv and environment, wrote the argv onto the child's stack, kept the environment for the child's whole life (ProcessData::env) and answered it through SYS_GET_ENV. It read none of it but argv[0], for the process's name. Since #1189 a spawn is the parent handing the kernel ranges to map, so this was the last userland data the spawn path carried (issues/the-kernel-is-small-interrupts-post-and-threads-wait.md, step 9's file half).

Resulting behavior

  • A child's arguments and environment are a startup block (toyos::startup): argc, the argv and envp pointers and their strings, which the parent writes into a memory object and lays out directly below the program. The kernel copies its pages as it copies the program's and reads none of it. PWD, the starting directory, travels in it as before.
  • The block lies at one address in every process, so every copy of the SDK in a process reads it there: std's and libc's entry, std::env, libc's environ, and the working directory's owner. A library's own std sees the arguments too, which the per-copy ARGC/ARGV statics never showed it.
  • A child's stack starts empty.
  • Deleted: SYS_GET_ENV (73) and get_env, SpawnArgs' argv_* and env_*, ProcessData::env, the kernel's stack block and the charge for the copies, and AddressSpace::fill with both wrote_codes, whose one caller was that stack write. Also the MAX_PATH charge per process that Step 9's file half: the kernel keeps no working directory; a spawn states it in the child's PWD #1182 left behind when the working directory went.
  • kernel/src: 214 files and 55,303 lines on main (ad5db5a), 214 and 55,163 on this branch (git ls-files 'kernel/src/*.rs', lines by wc -l).
  • Whole diff against main: 51 files, +831 −643. Kernel +31 −171; SDK, std and libc +375 −148; guest, harness and SDK integration tests +299 −224; build, manual and records +126 −100.

Decisions a reviewer should weigh

  1. A fixed address, not a word the kernel hands the entry. The alternative was SpawnArgs carrying one opaque word delivered in the first argument register. A fixed address needs nothing of the kernel and needs no symbol shared between the SDK copies in a process. Its cost: a spawner that lays out no block must still lay out the page, so lay_out lays every program with an empty block (a page of zeros), which is what the supervisor and a raw spawn start with: no argument, an empty environment.
  2. The block and reserved pages fill the 2 MiB below the program, so a program's ranges stay one span and the bound has a definite edge. Review found the filler is not needed to keep a later mapping from being named as the executable in a crash frame: an address below the image's bias is already refused. It stays as harmless. Arguments plus environment plus their pointers are bounded at 2 MiB less a page (the kernel's bound was 64 KiB each); _SC_ARG_MAX says so.
  3. lay_out lost its at parameter. Every caller passed BASE, and the block's place is defined against it.
  4. A process's name is a bounded label its parent chooses (SpawnArgs::name_ptr), by the orchestrator's decision after the T14 showed what naming it for the path cost: every launched applet was toybox in the roster, its exit record and a crash dump. The SDK names a child for its argv[0]'s last component, as the kernel did.
    • Bounded and validated at the boundary: 1 to 27 bytes of UTF-8 holding no control character and no white space (toyos_abi::syscall::process_name). The length is checked before anything is copied; anything else is InvalidArgument. A name is therefore one token of one line wherever a kernel record prints it, which argv[0] never was; this closes issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md. The roster is not such a record: it answers a thread's own name, which SYS_SET_THREAD_NAME takes unvalidated, ahead of the process's.
    • Nothing trusts it. I checked each reader. The kernel prints it (exit record, the fault path's process line) and answers it in the roster and as a claim's holder in the inventory, which lib/inspect prints; no right, claim or lookup reads it, and a thread could already rename itself through SYS_SET_THREAD_NAME. Log attribution is by the ring a record came out of, which the supervisor registers on a connection only it holds, not by the name. The supervisor matches rows, grants rights and restarts by the handles it holds and never reads the roster. The roster's readers are ps, osbench's report and tests.
  5. The kernel still keeps the path (SpawnArgs::path_ptr), up to 64 KiB of userland text per process: the spawn's record and a crash's frames name the file by it, and symbolize opens that file. Removing it needs frames named by build-id and a userland index from build-id to file, which is not this change. Recorded in the track.
  6. The child does not validate the block. Its parent laid out every byte of its memory, the program included.
  7. The block's object is counted to the job the child is endowed, as a read image is, or else to the parent's.

Not changed: an empty argument is still dropped, now by the block's writer where the kernel's split did it (issues/a-spawn-drops-an-empty-argument.md, updated). A non-UTF-8 argument is no longer refused by the kernel.

Checks

All logs: https://github.com/ToyOSOrg/evidence/tree/8567264843bac075add2a7862892c8e306ca9b6e (its README tabulates command, revision, target and exit).

check revision result
guest rows: libc_children, session_lifecycle, a_launch_gives_each_unsent_stdio_slot_a_null, console_image_boots (x86-64 under KVM), virt_user_mode (AArch64 emulated) 8670c0d, the head, on main ad5db5a green, 5 of 5 (run 42979143481). log
cargo run -- --ci host, lint included, macOS arm64 8670c0d exit 1: 109 of 110 steps green; clippy, warnings denied is clean. the build system: 384 passed, 1 failed, llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key with the child it runs, red on every macOS host (issues/the-llvm-environment-test-breaks-git-on-macos.md). The log has 75 control steps, of which 73 print FAILED; no other step holds one. log
T14, three boots, ten members images of 9885370, judged by the harness at 166944c 10 passed, 0 failed. judge, job lines: proctreecase, shared-debug, testcases
earlier guest rows 494063c, 60db1b9, e9bbb29 green but for libc_children once at e9bbb29, by tty_interrupt's stall, whose record main has since closed (ecb718f). logs

What the T14 shows

  • startup_block: the child given a block exited 41 and the child given none 42, as the parent asserts.
  • abuse_spawn_path_and_name: each of the seven refusals said, then the longest name started a child.
  • process_tree: exit 0, with the launched applets recorded as cat, and judged green by the repaired judge.
  • abuse_handle_table, spawn_child_ends_first: exit 0, where cf06504's aborted.
  • abuse_spawn_maps, abuse_page_straddle, spawn_image_object, spawn_lands_claimed, start_refuses_itself: exit 0.

What ran on what

Review

  • Round 1: startup_block's two exit codes; the block recorded in issues/no-user-address-is-drawn-per-spawn.md; all suggestions.
  • After round 1: the name field (decision 4), by the orchestrator's decision.
  • Round 2: issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md closed; the two comments; all suggestions.
  • Round 3: the host's NAME_LEN is the ABI's (my miss: I reported one failing test at 9885370 where there were two); issues/a-spawns-record-quotes-the-path-its-caller-states.md states the forged line and carries the orchestrator's two-part exit, neither part implemented here.
  • Round 4: process_tree.rs's header says B's first spawn is refused in the caller, before the kernel hears of it.
  • process_tree's judge no longer counts a kernel line no spawn prints since The loader is the program's, and the kernel parses no ELF #1189, and issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md is closed. The job asserts that refusal. A spawn refused after admission giving back what admission took is held on the host by kernel/proclife's interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it, which the control mutate-refused-spawn-keeps-the-count reds, and on the T14 by the three of abuse_spawn_maps' sixteen refusals that come after admission.
  • Main's inventory of what the kernel still holds (issues: what the kernel still holds, by responsibility #1208) marks the arguments-and-environment row done, as Where a boot came from is the supervisor's to read, not the kernel's #1210 marked its own.

Two commit messages on the branch say what the tree no longer does, and the squash should take this body: the round 3 commit's last sentence describes a sighting hunk a rebase dropped with its record, and the commit before the last carried conflict markers in the track's record, which the last commit removes.

Unresolved

  • The track's rule that each step measures the longest interrupts-off and preemption-off windows is not met here: no mask_windows run was made.

🤖 Generated with Claude Code

Japabu added a commit that referenced this pull request Nov 10, 2026
kernel/src holds 215 files and 55,245 lines, from 215 and 55,386. The startup
block's reservation is the module's own constant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1200 at cf06504 (merge base f9b5d7f)

Read: both commits, the whole diff, every changed file, the kernel's spawn path and crash-frame naming, the SDK's spawn, std's Command, libc's posix_spawn, the supervisor's launcher, the build's supervisor layout, and the evidence at 8338c0db. Nothing was built or run.

Architecture

The ownership is right. What a parent tells its child is the parent's data and the child's to read, and the kernel loses a copy, a store, a syscall and a write path for it. The convention lives in the SDK (toyos::startup), not in toyos-abi, which is correct: the kernel is no party to it. The kernel's remaining charge arithmetic holds: a spawn now copies a path (at most 64 KiB) and labels (at most 4 KiB), inside what a thread is charged for one call, so dropping the extra charge is sound.

Verified as claimed: kernel/src 215 files and 55,386 lines at the base, 215 and 55,245 at the head. Kernel +18 -159. SDK, std and libc +280 -142, of which startup.rs is 125 lines of production and 44 of unit tests. Guest and SDK integration tests +210 -191. Net production shrinks; the consequential addition is the block's writer and reader.

What was asked

Does the child validate the block? No, by decision 6, and I accept it. A block is malformed only if the spawner bypassed startup::block, and that spawner also chose every byte of the child's program, its entry and every handle it holds, so a check defends nothing the same party cannot do in code. The launcher is the case that looks like a boundary, a caller choosing arguments for a row with more authority, and there the supervisor writes the block itself from the caller's strings, so the structure is the supervisor's. The failure of a malformed block is a fault at the bad pointer in the child's start: loud, not named.

Can the kernel still trust it? No. Nothing in the kernel reads the block or any argument. The only userland text a spawn hands it is the path, bounded and checked as before, and the endowment labels.

Does the fixed address weaken anything? Not today, since the image, the stack and every mapping are already at constant addresses. It does add debt against a recorded defect: see blocker 2.

The cap. Refused before anything is created, by name (Refused::Invalid, E2BIG in libc, InvalidInput in std), and host-tested at the boundary byte.

Every spawner. std: environment captured with env_clear honoured, PWD replaced by starting_in. libc: envp or environ; the exec family stays refused. Launcher: env_clear, the request's environment and directory, the block counted to the job it endows. Kernel's first spawn: the build lays the supervisor out with the same function, so it starts on a page of zeros, no argument and no environment, which every green boot exercised. Raw spawns in tests go through lay_out.

Deletion. Complete. No reference to SYS_GET_ENV, get_env, the four SpawnArgs fields, ProcessData::env, fill, populate_private, wrote_code, argv_block, or std's ARGC and ARGV remains outside the track's record. Number 73 is unused. The Rust fork names none of them.

Decision: the name from the path. Accept. I traced where it differs and the PR's account is right: only a launch that reaches a row through a link. A helper keeps the link's own name, because std passes the path it resolved, so the shell's ls is still ls, and the C corpus's per-case links and metalprobe's still record under the case's name, which the T14's judges read. A launched ls now reads toybox in its exit record, the roster and a crash dump, as its spawn record already did. The cost is that one applet has two names depending on who started it. That is cosmetic and does not justify a name field.

Decision: fixed address versus an opaque word. Accept the address. The word would cost a SpawnArgs field and an entry-register contract on two architectures and would not remove the need for every SDK copy to find the block. Two notes. The stated reason for filling the 2 MiB, that a gap would let a later mapping be named as the executable, does not hold for the position-independent images the SDK lays out: frame_offset already refuses an address below the bias (lib/symbols/src/frame.rs:62). The filler is harmless and gives the cap a definite edge, so I do not ask for its removal. And the manual says "below it", which is the form that survives a drawn base.

The manual. Accurate and names no code. docs/src/architecture/kernel.md:20 is left as one overlong line.

Evidence. Verified: both guest runs are of 494063c and green, 6 of 6 and 4 of 4. The host run's one red step is the recorded macOS test, in a file this branch does not touch; the other FAILED lines in that log are the loom controls reaching their verdicts. cf06504 differs by a record and one pub. The nine T14 members have run nowhere, and landing waits on that boot.

BLOCKER

  1. tests/guest/rust-tests/src/bin/startup_block.rs:72 — the child told to play said chooses its role from the arguments under test. If its block arrives as zeros it has no argument, takes the unsaid branch, finds no variable, and exits 0, and line 83 accepts that. So the test cannot tell "300 KiB arrived whole" from "nothing arrived", and the PR's sentence on what it holds is overstated. This is the answer on the negative control: none needs running if the verdict cannot be reached by the wrong role. Give the two roles distinct exit codes and assert each. The in-flight T14 result stays valid for the other eight members; this member needs one run after the change.

  2. issues/no-user-address-is-drawn-per-spawn.md — not updated. This branch adds a fourth constant address, and the only one whose bytes another party chooses: writable, up to 2 MiB, and in a launched program filled by a caller with less authority than the row. That record's exit counts three bases and would be met with the block still at one address. Record the block there with its exit, for instance that it moves with the image, which every SDK copy can find through the module list. No rerun.

SUGGESTION

  • sdk/toyos/src/startup.rs — block and Block::new are always called as a pair, and Block::new takes any bytes under the comment "which block made"; one over MAX_BLOCK underflows in starting_with. One constructor from arguments and environment removes the precondition.
  • sdk/toyos/src/startup.rs:108 — "every process starts with the block's first page mapped" is true of a program laid out by lay_out, not of every process. Say so.
  • tests/guest/rust-tests/src/bin/abuse_spawn_maps.rs:74 — m[0] is now the one-page block, so this range ends exactly at the user half's top and is refused by the space's own bound, not as "past the user half". Use m[text], as the three cases below it now do.
  • issues/the-kernel-is-small-interrupts-post-and-threads-wait.md:175 — "one userland text" overstates. The kernel also keeps each process's endowment labels for its life and answers them through SYS_ENDOWMENTS, the shape the environment had, and they are the next candidate for the block.
  • sdk/toyos/src/elf/program.rs:62 is an unwrapped line, and sdk/std/sys/pal/mod.rs:9 is a blank line the deleted import left.

For the orchestrator

The PR body's "tests +238 -297" does not add up against the 545 and 513 total; I count +210 -191.

SEND BACK

Japabu added a commit that referenced this pull request Nov 10, 2026
startup_block's children exit with a code of their own once their checks
hold, and the parent asserts each: a child told to read 300 KiB that found
nothing took the other child's role and exited 0, which was accepted.

issues/no-user-address-is-drawn-per-spawn.md records the startup block as a
fourth constant address, the one whose bytes another party chooses, with its
exit: it moves with the image.

toyos::startup has one constructor, from arguments and environment, so no
caller can hand Program::starting_with a block past its reservation. The
track says the kernel keeps two userland texts, the endowment labels being
the other. abuse_spawn_maps moves a program's range past the user half, not
the block's page.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
… run

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
… unchanged

soundserver's bell arrives READ|WAIT, so the wait side really does go
when its client's table does, as the comment beside it claimed.
`RIGHTS_UNCHANGED` is `TransferEntry::UNCHANGED`: `SYS_HANDLE_DUP`'s
word for the source's own set is u32::MAX where it was u64::MAX, and
its two SDK wrappers are the only writers. The manual says a holder
keeps a handle until it gives it up or ends.

issues/a-received-handle-has-no-knowable-type.md records that it is
taken up as (handle, ObjectKind) at receive, the three constraints its
review set, and that event sides widen its reach.

The spawn-side fixes of this round follow in the rebase onto #1200.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1200 at 52ec105 (code at e9bbb29), changes since cf06504

Read: the four commits since round 1, their whole diff, each changed file, the kernel's spawn arm and every reader of a process's name, the SDK's spawn, the supervisor's launcher, the roster's and the inventory's userland readers, tty_interrupt, and the evidence at 96fb0284. Nothing was built or run.

Architecture

The name field is sound and its condition holds: the name is a label and nothing decides by it. Against main it adds no authority, since main's kernel already took the name from the parent's argv[0]; it now takes it stated, bounded and checked, and parses no path for it. Since round 1: kernel +17 -16, SDK +118 -29 (about 35 of the additions are two host tests), guest tests +150 -87. Whole branch as the body says: kernel +29 -169, kernel/src 215 files and 55,246 lines.

Who reads the name. Written once, at ProcessEntry::new. Read at four places, all prints or answers: the exit record (kernel/src/process.rs:721), the fault path's process line (kernel/src/arch/x86_64/idt/exceptions.rs:342), the roster behind a thread's own name (kernel/src/syscall/machine.rs:330), and a claim's holder in the inventory (kernel/src/inventory.rs:36). The kernel compares it nowhere. In userland the roster is read by ps, osbench and tests; the holder name only by lib/inspect, which prints it. The supervisor reads the inventory for functions and partitions, never a holder's name, matches rows by the manifest's name and restarts by the handles it holds. logkeeper reads no roster. Crash frames and symbolize go by the path, as before. A launched program's name is its caller's argv[0], so a caller labels a process that has a row's authority; that is main's behavior and gives the caller nothing.

The boundary. Complete. name_len is refused above 27 before any copy (kernel/src/syscall/dispatch.rs:129); the copy is user_vec, the helper the labels use, which answers BadAddress for a window outside the user half or a fault; process_name refuses empty, non-UTF-8, control and white-space characters. No input reaches a panic: the one expect is on the kernel's own constant. A thread's rename touches the thread's entry only, so a kernel record's name stays what process_name took.

Is the ABI shape minimal. Yes. One pointer and length, one bound, one validator both sides call. An inline 28-byte field would save the second copy and cost a rule for the bytes after the NUL: a wash, and the pair matches the struct's other texts. No field at all means the child naming itself at entry, which leaves a process that dies before its runtime runs without a name in its own exit record. process_name belongs in toyos-abi: it is the kernel's contract and the SDK needs the bound.

Round 1. Both blockers are resolved. A said child that gets no block now exits 42 where the parent asserts 41. The address record carries the block with an exit. The suggestions are taken and Block::new no longer has a precondition.

The two repaired members. Correct, and the account is right: at cf06504 the helper made the block's object inside a spawn from a full table and panicked. Both lay out before filling. The kernel claim each holds is unchanged.

libc_children. I accept independence. The log shows the shell said /> shell, the nested prompt, and nothing after: no echo of cat and no spawn error. So the nested shell was started under its name, wrote its prompt and never took the line, which is the record's signature. The name is consumed in the spawn arm and read at the four places above; none is on a pipe read or its wake, and no spawn was attempted after the stall began. One red in three branch runs is inside main's recorded rate.

BLOCKER

  1. issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md — left as it was, and this change is its subject. It cites make_name (kernel/src/loader/start.rs), which e9bbb29 deletes, and describes a forged name, test_rs_job pid=1 code=0, that the kernel now refuses at spawn. Decision 4 claims exactly that property without naming the record, so the tracker keeps an open defect that no longer exists as written. Its exit offers "refused at spawn"; the test it asks for cannot be written any more, and abuse_spawn_labels' two-word case with the host test holds the same claim cheaper. Close it by the closing procedure, or rewrite it to what the author holds is left. No run.

  2. build/bootlog.rs:81-86 and tests/common/metal.rs:308-310 — false since e9bbb29. The first names make_name, which no longer exists, and both say the kernel fills the name from the path's last component. The name every judge matches is now what toyos::spawn::name writes, and recorded_name must agree with that function: it cuts at a byte where the SDK cuts at a character and writes _, the same for every ASCII job name. A reader sent to a deleted kernel function cannot find the pair that has to stay equal. Say what is true. Compile only.

SUGGESTION

  • tests/guest/rust-tests/src/bin/abuse_spawn_labels.rs — in this ABI "labels" is the endowment blob (labels_ptr, MAX_SPAWN_LABELS_LEN), which this test leaves empty. A name saying path and name would not send a reader looking for the endowment labels' test here. Free now, since no boot has run; it does mean staging again.
  • sdk/toyos/src/spawn.rs:402 — name is pub and called from one place in its own module.
  • kernel/src/process.rs:195 — unwrap_or("?") is unreachable now that every name is process_name's.
  • kernel/src/syscall/proc.rs:108 and kernel/src/loader/mod.rs:132 spell one array type with two constants.
  • The sighting: the run's serial artifact holds the nested shell's exit record. Its syscall counts would say whether it sat in one read, as The loader is the program's, and the kernel parses no ELF #1189's sighting did, and the artifact expires in seven days.

Landing precondition, not reviewed

The three staged boots of e9bbb29 on the T14, judged by the harness over the readbacks at exit 0 with ten passed. A boot's own passed is not enough: at cf06504 all three said it with three members red.

  • startup_block: never run with the 41 and 42 verdicts.
  • abuse_spawn_labels: never run anywhere, and the only run of the kernel's seven refusals.
  • process_tree: the roster's cat, the end-to-end check of the decision's motive.
  • abuse_handle_table, spawn_child_ends_first: the two repairs.
  • abuse_spawn_maps, abuse_page_straddle, spawn_image_object, spawn_lands_claimed, start_refuses_itself: raw spawners under the moved struct.

Fixing the two blockers changes no image, so the staged images stay valid unless the rename is taken.

For the orchestrator

  • The body's list of readers omits the inventory's holder name. It is a print like the others.
  • "One token of one line wherever a record prints it" is true of the kernel's records. The roster answers a thread's own unvalidated name ahead of it, which the body says elsewhere.
  • By reading, not run, and not this change's: spawn: {path} pid= writes the caller's path, any UTF-8 up to the record's length, and I found no escaping in the kernel's message writer. If the renderer adds none, the forgery blocker 1's record describes is still reachable through the path. Worth a record of its own before that one is closed as whole.
  • The track's mask_windows rule is unmet, as the body says. The new arm copies 27 bytes under no lock.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md
is closed. Its forged name, `test_rs_job pid=1 code=0`, is refused at spawn:
a name is 1 to 27 bytes of UTF-8 with no control character and no white
space (toyos_abi::syscall::process_name), so it cannot carry a record's
separators. The claim is held by sdk/abi's
`a_process_name_is_one_bounded_token` on the host and by
abuse_spawn_path_and_name's two-word and two-line names on the T14's boot.
`make_name`, which the record cited, went with the name field.

The same forgery through the path is not closed and has its own record,
issues/a-spawns-record-quotes-the-path-its-caller-states.md: `spawn: {path}
pid=` writes a label its caller states, unchecked and unescaped. Read, not
run.

build/bootlog.rs and tests/common/metal.rs say where a recorded name comes
from now, the parent's SDK, and what `recorded_name` must agree with.

abuse_spawn_labels is abuse_spawn_path_and_name, since "labels" is the
endowment blob in this ABI. The SDK's `name` is private. The kernel spells a
name's array one way, THREAD_NAME_LEN, which is the roster's NAME_BYTES, and
`name_str` no longer answers `?` for a name that cannot be.

The stall's sighting says the run's serial artifact ends at the console's
hand-over and holds no exit record of the nested shell.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3 of #1200 at 9885370, changes since e9bbb29

Read: both commits in the range, their whole diff, every writer and reader of a process's name, the path's way from SYS_SPAWN to /log, the judges the new record names, the issue rules, and host/r3-ci-host.log at evidence 9304ffeb. Nothing was built or run.

The change list

Complete as stated. The range holds the five code items, comments in build/bootlog.rs and tests/common/metal.rs, and three issue records; nothing else. The rename left no stale reference (guest binaries are found by file), and name has no caller outside its module. Item 1 has a consequence the list omits: blocker 1.

name_str's expect

Unreachable; no path constructs a non-UTF-8 process name.

  • ProcessEntry::name is private and written once, in ProcessEntry::new, whose one caller is loader::spawn (kernel/src/loader/mod.rs:213).
  • loader::spawn has two callers. SYS_SPAWN passes what process_name returned (kernel/src/syscall/dispatch.rs:133). The supervisor's spawn passes process_name of the kernel's own constant (kernel/src/loader/mod.rs:335). There is no other kernel-created process.
  • process_name answers at most 27 bytes of checked UTF-8 padded with NULs, which is UTF-8 as a whole.
  • SYS_SET_THREAD_NAME writes ThreadEntry::name only. name_str exists on ProcessEntry alone and its two callers, the exit record and the fault report, both read the process's entry.

Round 2's blockers

  1. The name's record is closed by the procedure: the file is gone, the tree holds no citation of its slug, its rule sits at process_name's doc, and the commit carries the story. Resolved. Its replacement is blocker 2 below.
  2. build/bootlog.rs:81-90 and tests/common/metal.rs:308-313 are true against toyos::spawn::name. Resolved, except the test doc 400 lines down, which is part of blocker 1.

BLOCKER

  1. build/bootlog.rs:510 — the_kernel_writes_the_records_the_host_reads is red at this head on every host. It holds kernel/src/process.rs to the text THREAD_NAME_LEN: usize = 28, and item 1 made that line = toyos_abi::inventory::NAME_BYTES. The branch's own log says so: host/r3-ci-host.log:37 ... FAILED, :583 kernel/src/process.rs does not write "THREAD_NAME_LEN: usize = 28", :614 382 passed; 2 failed. The PR body and the evidence README both say the step is red by the macOS llvm test alone, which is false, and the required host check will be red on Linux too.

    • The build crate already links toyos-abi (Cargo.toml:239), so NAME_LEN can be the ABI's constant, or recorded_name can use MAX_PROCESS_NAME. Then the third needle goes, with the doc at :501-503. That doc is false as it stands: the kernel truncates no name since e9bbb29, and something does link this crate to the length.
    • Evidence owed: the the build system step on the fixed revision. A fix inside build/bootlog.rs changes no kernel or userland byte, so it does not invalidate the staged images.
  2. issues/a-spawns-record-quotes-the-path-its-caller-states.md — accurate in what it says, wrong in what it leaves out and in its exit. By reading, not run.

    • :16-25 omits the line break, which is the worst case. user_str checks UTF-8 only, Message::write_str (kernel/src/log/mod.rs:89) copies the text as it came, LogRecord's Display writes the message raw (sdk/abi/src/log.rs:207), and logkeeper writes that (system/logkeeper/src/main.rs:535). A path of x, a newline and [2026-11-11 10:00:00 1.000 cpu0 kernel] exit: test_rs_job pid=1 code=0 cpu=0ms puts a whole line in /log that toyos_logstream::parse reads as a kernel record. Readback::exit_code takes the first pid= and code= and keeps the lowest pid. That is the closed record's forged verdict, and more than the name could carry, since a head never fitted in 27 bytes. The same line can wear any program's head. The commit says "the same forgery through the path ... has its own record"; the record describes only a spawn: record read as another's. A refusal writes the path too, so the spawn need not succeed.
    • :35-41 the exit's second option does not close the record's first way. A label of /system/bin/logkeeper holds nothing to escape, so the escaped record still reads spawn: /system/bin/logkeeper pid=N. The exit's test, labelled /system/bin/logkeeper pid=1, exercises neither that way nor the line break.
    • Say the line break and what it reaches, and give an exit that closes each way the record lists. No run.

Severity. No flag is right by issues/README.md. security is an isolation boundary or an authority broken by itself, and nothing in system/, apps/, sdk/ or lib/ decides by a kernel record: every reader is a judge in tests/ or build/, or a person. Stating another program's path outright is the weaker half: this PR makes the path its caller's label, so the defect there is the judges trusting it. The line-break form is the one a reader of the record must not miss, because it forges a test's verdict. Whether forged attribution in /log counts as a broken boundary is the owner's reading of that flag, not mine.

For the orchestrator

  • The body's host row and the evidence README's row for host/r3-ci-host.log name one failing test; the log names two.
  • The sighting's "2.4 s into each boot" is 2.4 s, 2.8 s and 4.0 s across the three UART logs of run 42929489113. Its claim holds: none carries the nested shell's exit record.
  • The three T14 boots remain the landing precondition, as in round 2.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…'s record says its worst case

build/bootlog.rs held kernel/src/process.rs to the text `THREAD_NAME_LEN:
usize = 28`, which 9885370 changed, so
`the_kernel_writes_the_records_the_host_reads` was red on every host from
that commit. NAME_LEN is toyos_abi::inventory::NAME_BYTES, the constant the
kernel's is defined as, and the needle goes; the test's doc no longer says
the kernel truncates a name or that nothing links the crates.

issues/a-spawns-record-quotes-the-path-its-caller-states.md states the line
break: a path carrying a newline and a whole kernel record puts a line in
/log that a judge reads as a verdict, from a spawn that need not succeed.
Its exit is the orchestrator's decision in two parts, one for the forged
line and one for the stated path.

The stall's sighting gives the three UART logs' ends as they are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
kernel/src holds 215 files and 55,245 lines, from 215 and 55,386. The startup
block's reservation is the module's own constant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
startup_block's children exit with a code of their own once their checks
hold, and the parent asserts each: a child told to read 300 KiB that found
nothing took the other child's role and exited 0, which was accepted.

issues/no-user-address-is-drawn-per-spawn.md records the startup block as a
fourth constant address, the one whose bytes another party chooses, with its
exit: it moves with the image.

toyos::startup has one constructor, from arguments and environment, so no
caller can hand Program::starting_with a block past its reservation. The
track says the kernel keeps two userland texts, the endowment labels being
the other. abuse_spawn_maps moves a program's range past the user half, not
the block's page.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md
is closed. Its forged name, `test_rs_job pid=1 code=0`, is refused at spawn:
a name is 1 to 27 bytes of UTF-8 with no control character and no white
space (toyos_abi::syscall::process_name), so it cannot carry a record's
separators. The claim is held by sdk/abi's
`a_process_name_is_one_bounded_token` on the host and by
abuse_spawn_path_and_name's two-word and two-line names on the T14's boot.
`make_name`, which the record cited, went with the name field.

The same forgery through the path is not closed and has its own record,
issues/a-spawns-record-quotes-the-path-its-caller-states.md: `spawn: {path}
pid=` writes a label its caller states, unchecked and unescaped. Read, not
run.

build/bootlog.rs and tests/common/metal.rs say where a recorded name comes
from now, the parent's SDK, and what `recorded_name` must agree with.

abuse_spawn_labels is abuse_spawn_path_and_name, since "labels" is the
endowment blob in this ABI. The SDK's `name` is private. The kernel spells a
name's array one way, THREAD_NAME_LEN, which is the roster's NAME_BYTES, and
`name_str` no longer answers `?` for a name that cannot be.

The stall's sighting says the run's serial artifact ends at the console's
hand-over and holds no exit record of the nested shell.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…'s record says its worst case

build/bootlog.rs held kernel/src/process.rs to the text `THREAD_NAME_LEN:
usize = 28`, which 9885370 changed, so
`the_kernel_writes_the_records_the_host_reads` was red on every host from
that commit. NAME_LEN is toyos_abi::inventory::NAME_BYTES, the constant the
kernel's is defined as, and the needle goes; the test's doc no longer says
the kernel truncates a name or that nothing links the crates.

issues/a-spawns-record-quotes-the-path-its-caller-states.md states the line
break: a path carrying a newline and a whole kernel record puts a line in
/log that a judge reads as a verdict, from a spawn that need not succeed.
Its exit is the orchestrator's decision in two parts, one for the forged
line and one for the stated path.

The stall's sighting gives the three UART logs' ends as they are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…hat #1200 took

issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md
is closed by its exit's second way: the judge's count is gone
(`process_tree` in tests/toyos.rs), and the T14's `proctreecase` boot of
9885370 is judged green by it, `process_tree` exiting 0. What the count
meant to hold, a spawn refused after its admission giving back what
admission took, is held on the host by kernel/proclife's tree tests, and on
the T14 by abuse_spawn_maps, whose refusals come after admission and whose
honest spawn runs after every one. The job keeps its own assertion that 64
zero bytes start nothing.

The sighting of tty_interrupt's stall this branch added is dropped: main
closed that record (ecb718f).

The kernel's inventory, which main wrote while this was in review, no
longer lists a child's arguments and environment as still held, and its
ranking and sums lose that row. kernel/src is 215 files and 55,239 lines
on this tree, from main's 55,379.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r4 of #1200 at ebf926940: SEND BACK

Scope: what changed since 98853704a, across the rebase onto 1cab347fd. Read: git range-diff f9b5d7f12..98853704a 1cab347fd..ebf926940, the three new commits in full, both whole-branch patches compared outside issues/, the records, main's f9b5d7f12..1191cef01 against the interfaces this branch changes, and host/r5-ci-host.log and metal/t14-98853704a/* at evidence cb02fee4. Nothing was built or run. The reviewer that died posted nothing on this PR.

Assessment

This round is host judging code, comments and records. No kernel, SDK or guest instruction changed, so nothing architectural is new. Both round-3 blockers are resolved. Two things remain: a module header that states the opposite of what this round's own commit says, in the file whose record this PR closes; and one guest dispatch owed for the rebased tree.

Net this round: production 0; host harness and its docs +6/−16 (NAME_LEN, one needle, the judge's count); one guest comment line; records +58/−74.

The change list

Complete. Commits 1 to 5 are = in the range-diff. Outside issues/, the two whole-branch patches differ in exactly:

  • build/bootlog.rs:85-86 — NAME_LEN is toyos_abi::inventory::NAME_BYTES, with its doc; :505-506 — the test's doc, and the THREAD_NAME_LEN needle removed below it.
  • tests/toyos.rs:6487-6492 — the judge's doc and the six-line count removed.
  • tests/guest/rust-tests/src/bin/process_tree.rs:411 — one comment line for one, so no panic location moves.

Round 3's blockers

  1. Resolved. r5-ci-host.log:51 has bootlog::tests::the_kernel_writes_the_records_the_host_reads ... ok. The the build system step ends 384 passed; 1 failed; 17 ignored, and its failure list is llvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key with the keyed_and_built child it runs, on the xcodebuild -find git failure the record describes. The run ends 1 of 110 step(s) red, that step. Of the log's 75 test result: FAILED lines, 2 are that step's and 73 sit under control steps. The kernel's constant is defined as the same ABI constant (kernel/src/process.rs:111), so the two cannot drift. Both docs read true.
  2. Resolved. issues/a-spawns-record-quotes-the-path-its-caller-states.md:20-29 states the line break, the three places that copy it untouched, and that it is a test's verdict; :13-14 says a refused spawn writes the path too. The exit closes each way: (a) refuses control characters at the boundary, which closes way 1; (b) takes the path away from every judge, which closes ways 2 and 3. I checked the claim the split rests on: Readback::exit_code matches a kernel record's message by starts_with (tests/common/metal.rs:327), so only a whole forged line reaches a verdict, and (a) is what stops it.

BLOCKER

  1. tests/guest/rust-tests/src/bin/process_tree.rs:12-14 — the header says "B's first act is a spawn the loader refuses once it is admitted under B: its hold on B goes with it, or B is never published and the arm's wait never answers." That is false, and this round says so itself.

    • :411 now reads "the spawn lays it out here and is refused before the kernel hears of it", and 8fa82c05f removes the judge's count for that reason. Helper::spawn refuses the 64 zero bytes in lay_out (sdk/toyos/src/spawn.rs:218) before any SpawnArgs exists. There is no admission, so no hold.
    • This PR deletes the only record that said the job no longer reaches that release. After it lands, the header is the tree's one statement on the subject and it claims coverage the job does not give. issues/README.md's close puts what stays true at the site that owns the subject; that site is this header.
    • Fix: rewrite those three lines in three lines, saying what B's first act asserts now. No instruction and no panic location changes, so no boot is owed and nothing else needs re-review.
  2. Evidence: the rebased tree has never booted, and the branch's last x86-64 guest rows are red.

Is the judge record's close acceptable? Yes, with blocker 1.

  • The record's defect was a judge red on every bench. That is gone, and the T14's proctreecase boot is judged PASS process_tree.
  • Its exit's condition, "a cheaper test holds that release", is met. Admission's drop calls tree::refuse_child on every refusal (kernel/src/process.rs:571-574). The host control mutate-refused-spawn-keeps-the-count is red in CI (build/ci.rs:714). On metal, abuse_spawn_maps has three refusals past Admission::ask (kernel/src/loader/mod.rs:140,147,163: an entry in no executable range, two ranges that meet, a range on the clock page); a kept hold would leave the job unpublished and the runner's wait unanswered.
  • Keeping the arm is fine. It asserts something true and costs nothing; the exit's wording was the filing agent's, not an owner decision. The author's reason against deleting it holds. What does not hold is leaving the header describing the arm as it was.
  • No citation of the slug remains at the head (git grep on the tree).

The rebase

  • The dropped sighting: right. Main deleted issues/libc-childrens-tty-interrupt-can-stall-at-the-nested-shells-line.md; re-adding a sighting to a closed record would resurrect it. Commit 6 of the old series is gone and commit 7 lost only its hunk in that file.
  • The inventory: right. The arithmetic holds: 130 + 95 + 40 + 30 = 295 for the four ranked rows, 435 with the OpRegion's 120 and the last words' 20. kernel/src is 215 files and 55,239 lines at the head against 55,379 at 1cab347fd, as I counted them. The KernelStack::heap paragraph matches kernel/src/loader/start.rs:50-53, which still charges two strings.
  • Nothing semantic collides. Main touched no file under the kernel's spawn, memory or process code, sdk/toyos, sdk/libc, sdk/std or the guest image helper. Its kernel edits are the slot parameter and the log cursor. At the head no use of SYS_GET_ENV, get_env or SpawnArgs' argv_*/env_* remains, and every raw SpawnArgs builder is a file this branch already converted. Main's new spawns go through Command, posix_spawn and the launcher. The shared files (build/bootlog.rs, tests/common/metal.rs, tests/toyos.rs) are edited in different places.
  • signal_quit's reloading child waits blocked, and a read across an interrupt moves to libc_read_restarts #1214, which landed after the rebase, shares no file with the branch; its two guest tests take their role from argv[1] through libc and std as before.

Do the T14 boots stand for ebf926940?

  • Yes, for what they tested. The ten members exercise this branch's kernel spawn path and SDK startup code. That source is what booted: the range-diff is =, and the one later change to an image input is the comment at process_tree.rs:411. judge.log ends 10 passed, 0 failed, 3 boot(s), exit 0, and every job line is exit=0. The judge that passed process_tree at 166944c21 is the function at the head.
  • Not for the combined image, which is blocker 2. No T14 boot is owed for it: what metal added was those ten members on real hardware, and main changed none of the code they test. The merge queue's image job builds both architectures' shipped images on the merged tree, so a target-only compile collision cannot land; the dispatch above covers behaviour.

For the orchestrator

  • 779797cf3's message ends "The stall's sighting gives the three UART logs' ends as they are"; the rebase dropped that hunk with the sighting.
  • The body's "one of 73 control steps": the log has 75 control steps, 73 of which print a FAILED result.
  • ebf926940's message credits "kernel/proclife's tree tests"; the test that reds under the mutation is interleave::tests::a_spawn_racing_its_places_kill_leaves_nothing_under_it_and_publishes_it. And "whose refusals come after admission" is three of abuse_spawn_maps' sixteen.
  • The body's Unresolved section already says no mask_windows run was made; that stays the track's.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
kernel/src holds 215 files and 55,245 lines, from 215 and 55,386. The startup
block's reservation is the module's own constant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
startup_block's children exit with a code of their own once their checks
hold, and the parent asserts each: a child told to read 300 KiB that found
nothing took the other child's role and exited 0, which was accepted.

issues/no-user-address-is-drawn-per-spawn.md records the startup block as a
fourth constant address, the one whose bytes another party chooses, with its
exit: it moves with the image.

toyos::startup has one constructor, from arguments and environment, so no
caller can hand Program::starting_with a block past its reservation. The
track says the kernel keeps two userland texts, the endowment labels being
the other. abuse_spawn_maps moves a program's range past the user half, not
the block's page.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md
is closed. Its forged name, `test_rs_job pid=1 code=0`, is refused at spawn:
a name is 1 to 27 bytes of UTF-8 with no control character and no white
space (toyos_abi::syscall::process_name), so it cannot carry a record's
separators. The claim is held by sdk/abi's
`a_process_name_is_one_bounded_token` on the host and by
abuse_spawn_path_and_name's two-word and two-line names on the T14's boot.
`make_name`, which the record cited, went with the name field.

The same forgery through the path is not closed and has its own record,
issues/a-spawns-record-quotes-the-path-its-caller-states.md: `spawn: {path}
pid=` writes a label its caller states, unchecked and unescaped. Read, not
run.

build/bootlog.rs and tests/common/metal.rs say where a recorded name comes
from now, the parent's SDK, and what `recorded_name` must agree with.

abuse_spawn_labels is abuse_spawn_path_and_name, since "labels" is the
endowment blob in this ABI. The SDK's `name` is private. The kernel spells a
name's array one way, THREAD_NAME_LEN, which is the roster's NAME_BYTES, and
`name_str` no longer answers `?` for a name that cannot be.

The stall's sighting says the run's serial artifact ends at the console's
hand-over and holds no exit record of the nested shell.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…'s record says its worst case

build/bootlog.rs held kernel/src/process.rs to the text `THREAD_NAME_LEN:
usize = 28`, which 9885370 changed, so
`the_kernel_writes_the_records_the_host_reads` was red on every host from
that commit. NAME_LEN is toyos_abi::inventory::NAME_BYTES, the constant the
kernel's is defined as, and the needle goes; the test's doc no longer says
the kernel truncates a name or that nothing links the crates.

issues/a-spawns-record-quotes-the-path-its-caller-states.md states the line
break: a path carrying a newline and a whole kernel record puts a line in
/log that a judge reads as a verdict, from a spawn that need not succeed.
Its exit is the orchestrator's decision in two parts, one for the forged
line and one for the stated path.

The stall's sighting gives the three UART logs' ends as they are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…hat #1200 took

issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md
is closed by its exit's second way: the judge's count is gone
(`process_tree` in tests/toyos.rs), and the T14's `proctreecase` boot of
9885370 is judged green by it, `process_tree` exiting 0. What the count
meant to hold, a spawn refused after its admission giving back what
admission took, is held on the host by kernel/proclife's tree tests, and on
the T14 by abuse_spawn_maps, whose refusals come after admission and whose
honest spawn runs after every one. The job keeps its own assertion that 64
zero bytes start nothing.

The sighting of tty_interrupt's stall this branch added is dropped: main
closed that record (ecb718f).

The kernel's inventory, which main wrote while this was in review, no
longer lists a child's arguments and environment as still held, and its
ranking and sums lose that row. kernel/src is 215 files and 55,239 lines
on this tree, from main's 55,379.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…is refused

In the caller, before the kernel hears of it, not by the loader after
admission. Three lines for three, so no panic location moves.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The kernel copied a spawn's argv and environment, wrote the argv onto the
child's stack, kept the environment for the child's life and answered it
through SYS_GET_ENV. None of it was the kernel's to read. A spawn is already
the parent handing the kernel ranges to map, so what a parent tells its child
is now one of them: a startup block (toyos::startup) of argc, the argv and
envp pointers and their strings, which the parent writes into a memory object
and lays out directly below the program. Every copy of the SDK in the child
reads it at that one address, so a library's own std sees the arguments and
the environment too. PWD, the starting directory, travels in it as before.

The kernel loses SpawnArgs' argv and env fields, SYS_GET_ENV,
ProcessData::env, the stack block it wrote and the charge for copying them;
AddressSpace::fill and wrote_code, whose one caller was that stack write; and
the working directory's MAX_PATH charge #1182 left behind. A child's stack
starts empty. A process's name is the last component of its path, which the
spawn's record and a crash's frames already name the file by: the kernel keeps
that one label and no other text of the child.

lay_out lays every program at BASE with an empty block below it, so the
supervisor, which the build lays out and the kernel starts, and a raw spawn
that says nothing start with no argument and no environment.
Program::starting_with names a block in its place. The block's pages and the
reserved ones above them keep a program's ranges one span, which a crash
record's frames are judged against.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu and others added 10 commits November 11, 2026 09:22
kernel/src holds 215 files and 55,245 lines, from 215 and 55,386. The startup
block's reservation is the module's own constant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
startup_block's children exit with a code of their own once their checks
hold, and the parent asserts each: a child told to read 300 KiB that found
nothing took the other child's role and exited 0, which was accepted.

issues/no-user-address-is-drawn-per-spawn.md records the startup block as a
fourth constant address, the one whose bytes another party chooses, with its
exit: it moves with the image.

toyos::startup has one constructor, from arguments and environment, so no
caller can hand Program::starting_with a block past its reservation. The
track says the kernel keeps two userland texts, the endowment labels being
the other. abuse_spawn_maps moves a program's range past the user half, not
the block's page.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The T14's boots at cf06504 passed as boots and three members did not:

- spawn_child_ends_first and abuse_handle_table aborted (exit 134): each
  fills its handle table and then spawns, and the helper made the child's
  startup block inside that spawn, where no slot is left for its object. Each
  now lays its child out, block and all, before the table fills, as it
  already read its image.
- process_tree panicked: it looks in the roster for the `cat` its shell
  detached, and a launched process now goes by its row's file, `toybox`,
  which `cat` is a link to.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Naming a process for the last component of its path made every launched
applet `toybox` in the roster, its exit record and a crash dump, which is a
loss for `ps` and for reading a crash. SpawnArgs carries the name instead:
1 to 27 bytes of UTF-8 holding no control character and no white space
(toyos_abi::syscall::process_name), bounded before it is copied and refused
InvalidArgument otherwise, so a name is one token of one line wherever a
record prints it. The SDK names a child for its argv[0]'s last component, as
the kernel did, cut and cleaned to what the kernel takes.

The name is a label and nothing trusts it. The kernel prints it and answers
it in the roster. Log attribution is by the ring a record came out of,
which the supervisor registers; the supervisor's rows, rights and restarts
go by the handles it holds. A thread could already rename itself.

process_tree looks for `cat` again. abuse_spawn_path is abuse_spawn_labels
and holds the name's bound and shape beside the path's.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md
is closed. Its forged name, `test_rs_job pid=1 code=0`, is refused at spawn:
a name is 1 to 27 bytes of UTF-8 with no control character and no white
space (toyos_abi::syscall::process_name), so it cannot carry a record's
separators. The claim is held by sdk/abi's
`a_process_name_is_one_bounded_token` on the host and by
abuse_spawn_path_and_name's two-word and two-line names on the T14's boot.
`make_name`, which the record cited, went with the name field.

The same forgery through the path is not closed and has its own record,
issues/a-spawns-record-quotes-the-path-its-caller-states.md: `spawn: {path}
pid=` writes a label its caller states, unchecked and unescaped. Read, not
run.

build/bootlog.rs and tests/common/metal.rs say where a recorded name comes
from now, the parent's SDK, and what `recorded_name` must agree with.

abuse_spawn_labels is abuse_spawn_path_and_name, since "labels" is the
endowment blob in this ABI. The SDK's `name` is private. The kernel spells a
name's array one way, THREAD_NAME_LEN, which is the roster's NAME_BYTES, and
`name_str` no longer answers `?` for a name that cannot be.

The stall's sighting says the run's serial artifact ends at the console's
hand-over and holds no exit record of the nested shell.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The T14's judge counted two kernel records, `spawn:
/tmp/process_tree_invalid_elf: …`, one per B. Since #1189 the spawner lays a
program out, so 64 zero bytes are refused in the caller and the kernel hears
of no spawn: nothing prints that line, and the judge was red on every boot
whose job passed. The job itself asserts the refusal in each B, which A's
checks cannot pass without, so the count goes. The depth refusal, which
only the kernel says, stays.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…'s record says its worst case

build/bootlog.rs held kernel/src/process.rs to the text `THREAD_NAME_LEN:
usize = 28`, which 9885370 changed, so
`the_kernel_writes_the_records_the_host_reads` was red on every host from
that commit. NAME_LEN is toyos_abi::inventory::NAME_BYTES, the constant the
kernel's is defined as, and the needle goes; the test's doc no longer says
the kernel truncates a name or that nothing links the crates.

issues/a-spawns-record-quotes-the-path-its-caller-states.md states the line
break: a path carrying a newline and a whole kernel record puts a line in
/log that a judge reads as a verdict, from a spawn that need not succeed.
Its exit is the orchestrator's decision in two parts, one for the forged
line and one for the stated path.

The stall's sighting gives the three UART logs' ends as they are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…hat #1200 took

issues/process-trees-metal-judge-waits-for-a-kernel-line-no-spawn-prints.md
is closed by its exit's second way: the judge's count is gone
(`process_tree` in tests/toyos.rs), and the T14's `proctreecase` boot of
9885370 is judged green by it, `process_tree` exiting 0. What the count
meant to hold, a spawn refused after its admission giving back what
admission took, is held on the host by kernel/proclife's tree tests, and on
the T14 by abuse_spawn_maps, whose refusals come after admission and whose
honest spawn runs after every one. The job keeps its own assertion that 64
zero bytes start nothing.

The sighting of tty_interrupt's stall this branch added is dropped: main
closed that record (ecb718f).

The kernel's inventory, which main wrote while this was in review, no
longer lists a child's arguments and environment as still held, and its
ranking and sums lose that row. kernel/src is 215 files and 55,239 lines
on this tree, from main's 55,379.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…is refused

In the caller, before the kernel hears of it, not by the loader after
admission. Three lines for three, so no panic location moves.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The rebase's resolution of the inventory left conflict markers in the
commit before this one. The section is main's again, with what #1200 did
marked done as #1210 marked its own, and the track's measurement says the
kernel this branch leaves.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification at 8670c0d, after review r4's SEND BACK at ebf9269 (two items, no code).

  • Blocker 1: tests/guest/rust-tests/src/bin/process_tree.rs:12-14 now says B's first spawn is refused in B, before the kernel hears of it. Three comment lines for three.
  • Blocker 2: the branch is rebased onto main ad5db5a (over Where a boot came from is the supervisor's to read, not the kernel's #1210 and signal_quit's reloading child waits blocked, and a read across an interrupt moves to libc_read_restarts #1214). git range-diff against the reviewed series shows every commit equal except hunks of issues/the-kernel-is-small-interrupts-post-and-threads-wait.md; the two new commits are the header comment and that record. The tree at the head holds no conflict marker (an intermediate commit did; the squash drops it). Guest run 42979143481 at this head: libc_children, session_lifecycle, a_launch_gives_each_unsent_stdio_slot_a_null, console_image_boots, virt_user_mode, 5 of 5 green. The implementer's --ci host at this head: one red step, the recorded macOS llvm test, clippy clean.
  • The T14 boots (10 members green, judged at 166944c) stand as r4 said.

The squash takes the PR body. Queueing.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 07:43
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 2ec4ad9 Nov 11, 2026
11 checks passed
@Japabu
Japabu deleted the startup-block-env branch November 11, 2026 08:07
Japabu added a commit that referenced this pull request Nov 11, 2026
… unchanged

soundserver's bell arrives READ|WAIT, so the wait side really does go
when its client's table does, as the comment beside it claimed.
`RIGHTS_UNCHANGED` is `TransferEntry::UNCHANGED`: `SYS_HANDLE_DUP`'s
word for the source's own set is u32::MAX where it was u64::MAX, and
its two SDK wrappers are the only writers. The manual says a holder
keeps a handle until it gives it up or ends.

issues/a-received-handle-has-no-knowable-type.md records that it is
taken up as (handle, ObjectKind) at receive, the three constraints its
review set, and that event sides widen its reach.

The spawn-side fixes of this round follow in the rebase onto #1200.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
Every manifest-granted SysCap arrived carrying TRANSFER, added by
`syscap_rights` because an endowment could not narrow: the supervisor
had to keep the right on the duplicate to move it at all. A grantee
could hand its grant to a process the manifest never named, and for
`rt` the receiver entered the real-time band itself.

The endowment's rights word is what removes it. `syscap_rights` is now
exactly what the capability arrives with, and the supervisor endows a
duplicate narrowed to it. Only `dup` lets a capability leave its
program, so that name is DUP|TRANSFER: handing a cap on to a child
takes duplicating it and moving the duplicate.

The spelling: `toyos::spawn::Spawn::endow` carries `Moved`, and std's
`CommandExt::endow_narrowed` takes the rights' bits, since std's own
`toyos-abi` is not its caller's. A launch's job goes with the rights
it has, and one named narrowed is refused.

The kernel decodes a spawn's request as `SpawnEndow`s once, in
`build_child_handles`, where it read a byte window twice by hand; the
size assert that tied the request to `EndowEntry` goes.

The loader's boot names, which the supervisor only maps, arrive MAP
alone. The two boot images keep what they had: the supervisor hands
them to the file server.

`moved_rights` follows #1200's `SpawnArgs`.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
A whole `testcases` boot on the T14 at 23f692c showed two shared-boot
members red. Neither runs anywhere but that shared boot.

206_libc_refusals expected `sysconf(_SC_ARG_MAX)` 65536, the bound the
kernel once put on each of a spawn's argv and environment. Since #1200
libc answers `toyos::startup::MAX_BLOCK`, 2 MiB less a page, 2093056 on
both architectures: the most a child's startup block holds of its
arguments, its environment and a pointer to each. That is the value libc
means.

served_file_offset asked `Helper::spawn(None)` for the kernel's
`InvalidArgument`. Since #1189 the SDK refuses a spawn with no image,
`Refused::Invalid`, before anything is duplicated, so the arm no longer
reached what it holds: that a spawn the kernel refuses after the SDK
duplicated its descriptors leaves the caller its export and the shared
offset. It now spawns a real image placed under a pipe's write end, which
carries `WRITE` but names no process: `SYS_SPAWN` lays out the program,
then refuses that place `InvalidArgument`, as it refuses any wrong type of
handle a caller names as a place.

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