Skip to content

tests: two shared-boot members assert what a spawn does on main - #1219

Merged
Japabu merged 1 commit into
mainfrom
spawn-tests-after-startup-block
Nov 11, 2026
Merged

Japabu merged 1 commit into
mainfrom
spawn-tests-after-startup-block

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A whole testcases boot on the T14 at 23f692c (#1204's images, which hold
#1189 and #1200) showed two shared-boot members red
(judge,
their lines):

  • 206_libc_refusals exit 1: sysconf _SC_ARG_MAX: 2093056 produced against
    65536 expected.
  • test_rs_served_file_offset exit 101: assertion failed: matches!(helper.spawn(None), Err(Refused::Syscall(InvalidArgument))).

Neither runs anywhere but that shared boot, and #1200's own T14 boots ran
nine other members (and process_tree).

Resulting behavior

  • 206_libc_refusals.expect says 2093056. Since A child's arguments and environment are memory its parent lays out #1200 libc answers
    _SC_ARG_MAX with toyos::startup::MAX_BLOCK, 2 MiB less a page: the most
    a child's startup block holds of its arguments, its environment and a
    pointer to each, which A child's arguments and environment are memory its parent lays out #1200 chose and its description says _SC_ARG_MAX
    reports. PAGE_SIZE is the ABI's one 4096, so the value is the same on
    both architectures. 65536 was the bound the kernel once put on each of a
    spawn's argv and environment.
  • served_file_offset's refused-spawn arm reaches the kernel again.
    Its claim: a spawn the kernel refuses after the SDK duplicated its
    descriptors leaves the caller its export and the shared offset. Since
    The loader is the program's, and the kernel parses no ELF #1189 the SDK refuses a spawn with no image (Refused::Invalid) before it
    duplicates anything, so spawn(None) never got there. The arm now spawns
    a real image (/system/bin/toybox, read as image_of reads it) placed
    under a pipe's write end. That handle carries WRITE and names no
    process, so SYS_SPAWN lays out the program and then refuses the place
    InvalidArgument (spawn_place: a wrong type of a sent handle is refused,
    not a fault). In this arm the SDK's one source of
    Refused::Syscall(InvalidArgument) is that syscall, made after the
    duplicates exist, so the assertion also shows the arm reached the state
    it tests.

The sweep: same class elsewhere

Looked for tests that encode a spawn refusal, an argument or environment
bound, ARG_MAX, a process name or an .expect that #1189 or #1200 changed.

  • Shared T14 members (every discovered Rust binary, every corpus case
    with an .expect, LAST_MEMBERS, on testcases; ACTUATOR_TESTS on
    shared-debug): checked by run, not inspection. The 23f692c boots
    ran all of them, 236 on testcases and 9 on shared-debug; these two were
    the only members red. Between 23f692c and this base, main changed
    comments only on any spawn, startup or libc path, and added no member.
  • Binaries other rows drive (the 58 RUST_SKIP binaries that do not
    ride testcases, and the harness's judges): by inspection. None asserts a
    value The loader is the program's, and the kernel parses no ELF #1189 or A child's arguments and environment are memory its parent lays out #1200 changed. Those that assert spawn or name behavior
    ran green in A child's arguments and environment are memory its parent lays out #1200's own checks: libc_children (every tests/guest/libc
    program and its five Rust probes), session_lifecycle,
    a_launch_gives_each_unsent_stdio_slot_a_null and process_tree.
    memory_floor expects a spawn refused for memory to be OutOfMemory,
    which std's image read gives (ResourceExhausted) before any startup
    block is made.
  • .expect files exist only in the TinyCC corpus; every one that runs
    ran in that boot.

sweep notes, the 58 binaries with their grep counts

Checks

check revision result
cargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases, macOS arm64 a336ce8 exit 2, the harness's Staged verdict (a staging run judges nothing, so it never exits 0). It built every x86_64 tests/guest/rust-tests binary, served_file_offset with no compiler diagnostic, compiled the C corpus and staged 236 shared members. log, notes
the staged testcases image searched for every sysconf _SC_ARG_MAX: line a336ce8 exit 0: one, 2093056 (notes)
T14, the whole testcases boot (image sha256 0544ef99ce710e44…) a336ce8 toyos-metal verdict passed. ===TEST_END test_rs_served_file_offset exit=0=== after served_file_offset: passed, with no child started for the refused spawn; ===TEST_END 206_libc_refusals exit=0=== after ccheck: 0. All 256 jobs ended; the three nonzero exits are the same three as at 23f692c (test_rs_wake_onto_busy, a measurement; test_rs_i219_msi, recorded in issues/i219-msi-passes-only-when-the-network-sends-it-a-frame.md; test_rs_usbd_spare_vector, an AMD-bench member). back_secs 98, under its ceiling of 100. The two members were judged by name from the kernel log; the harness's boot:testcases judge also needs the testcases-watchdog boot and was not run.

No host check: neither file is built on the host. tests/guest/rust-tests
is outside the host workspace, so host tests and clippy never compile it,
and a corpus .expect is read only when a boot is staged, which the staging
run above did. The staging compile is that test's compile check.

No guest row: both members run only on the T14's shared boots, and the
sweep found no QEMU row whose assertion changed.

Risks

🤖 Generated with Claude Code

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>
@Japabu
Japabu force-pushed the spawn-tests-after-startup-block branch from 5efb81f to a336ce8 Compare November 11, 2026 15:31
@Japabu Japabu changed the title tests: two shared-boot members assert what a spawn does since the startup block tests: two shared-boot members assert what a spawn does on main Nov 11, 2026
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1219 at a336ce8

Architecture

Test-only, +11 −4: one .expect line and one test arm. No production code, manual or ABI change, so no manual update is owed.

206_libc_refusals.expect. 2093056 is the value #1200 meant. #1200's description says "Arguments plus environment plus their pointers are bounded at 2 MiB less a page … _SC_ARG_MAX says so", and libc answers toyos::startup::MAX_BLOCK (sdk/libc/src/misc.rs:262). That constant is RESERVED − PAGE_SIZE = 2097152 − 4096 (sdk/toyos/src/startup.rs:30,36). PAGE_SIZE is the ABI's single 4096 (sdk/abi/src/syscall.rs:152), so the value is the same on both architectures.

POSIX lets the implementation decide whether the pointers and null terminators count toward ARG_MAX, and ToyOS counts them. The block also holds two things the caller did not send: the argc word, and the PWD=<cwd> entry the SDK adds (sdk/toyos/src/files.rs:475-489). So a caller's own argv+envp get 21 + len(cwd) bytes less than _SC_ARG_MAX, which is at most 4117 with a MAX_PATH cwd. That is within the 2048 bytes xargs keeps in reserve unless the cwd is longer than about 2 KB. This comes from #1200, not this PR (see the suggestion below).

served_file_offset. I read the arm's history. 7f52fb7 added it alongside the fileserver change that kept DUP on an exported connection. Its claim: when the kernel refuses a spawn after the SDK has duplicated the caller's borrowed descriptors, the caller keeps its export and the shared offset.

  • Where the refusal happens. The old spawn(None) failed the kernel's first check (image_len == 0), before any endowment was read. The new pipe place is refused at spawn_place (kernel/src/syscall/dispatch.rs:119). That runs after spawn_program and still before build_child_handles, so the arm reaches the same state it used to.

  • The refusal is the kernel's, and the assertion shows the duplicates were made.

    • The SDK passes the place through unchecked (sdk/toyos/src/spawn.rs:293-296).
    • A pipe's write end carries WRITE (kernel/src/object/ops.rs:28), and get checks rights before type. So get_sent hits WrongType, which it answers InvalidArgument (kernel/src/object/handle.rs:274-283).
    • SYS_HANDLE_DUP with unchanged rights cannot return InvalidArgument (kernel/src/syscall/handles.rs:58-73). An InvalidArgument therefore means the duplicates existed when the kernel refused.
    • Every SDK-side Refused::Invalid happens before duplication, so a kernel-only refusal is the right path to exercise.
  • Is the claim held better elsewhere? Partly:

    • The kernel's all-or-nothing move is held by PendingHandles::commit's structure and by abuse_spawn_maps.
    • The place refusal itself is held by process_tree's a_pipe_is_no_place and a_manage_only_handle_is_no_place.
    • What only this arm holds is the SDK side: a refusal must not close or consume a borrowed export. The types do not guarantee that, because the SDK can build OwnedHandle(raw) from a borrowed raw handle without unsafe.

    The arm rides an existing member at no extra boot cost, so repairing it rather than deleting it is right.

  • The two new comments are accurate and say why the test is built this way.

Sweep

Credible and, for this class of test, complete.

  • Shared members, checked by run. I confirmed the 23f692c judge: 236 testcases members and 9 shared-debug members, and the only class reds were these two. git diff 23f692c91 origin/main touches spawn, startup and libc paths only in comments.

  • What a run cannot find. A run finds members that went red, not members that still pass but no longer reach what they check. I grepped every guest test for loose spawn-refusal assertions (is_err, Refused::, posix_spawn … != 0). The only hits:

    Raw SpawnArgs users are compile-checked by the ABI change.

  • Non-members and harness judges. The inspection notes on the non-member binaries and the harness judges match what I read, including memory_floor's OutOfMemory and the name-length argument.

Evidence

  • Staging. At a336ce8, exit 2 Staged. The staged image's only _SC_ARG_MAX line is 2093056.
  • T14 testcases boot. Run by the orchestrator after the PR was written. toyos-metal verdict passed. The booted image's mtime matches the staging log's testcases line (14:47:07Z); its sha256 starts 0544ef99ce710e44.
    • ===TEST_END test_rs_served_file_offset exit=0===, after served_file_offset: passed. The kernel log shows one toybox child, the echo (pid 22544), and no child for the refused spawn.
    • ===TEST_END 206_libc_refusals exit=0===, after ccheck: 0.
    • All 256 jobs ended. Only three exited nonzero, the same three as at 23f692c:
      • test_rs_wake_onto_busy exit 54: a measurement, as its registration says.
      • test_rs_i219_msi exit 1: the recorded issues/i219-msi-passes-only-when-the-network-sends-it-a-frame.md.
      • test_rs_usbd_spare_vector exit 101: an AMD-bench member ("no MSI-X capability").
    • back_secs 98 against a record of 50, under the ceiling of 100. At 23f692c it was 124, which A move names the rights it delivers: a ring and a manifest's SysCap stay where they were given #1204 left unattributed.
  • Not yet done. The harness judge has not been run on this readback.
    • What must be green when it is: every row as at 23f692c.
    • Reds expected on the T14 for machine reasons:
      • idle_entered
      • amdvi_translates_the_boot_controller
      • amdvi_remaps_the_boot_controller
      • amdvi_usbd_drives_its_own_entry
      • console_uart_driven (red on usbd_spare's panic line)
      • i219_messages_on_its_msi_vector
    • How to judge it. request.txt also lists testcases-watchdog. Judging boot:testcases takes a row that needs that boot and restages the images, as A move names the rights it delivers: a ring and a manifest's SysCap stay where they were given #1204 found. Either run both boots, or judge the two members by name.
  • No host check. Correct: tests/guest/rust-tests is excluded from the host workspace (Cargo.toml:196), and the staging run compiled it.

Findings

No blockers.

  • For the orchestrator, not a code finding. The PR description is now out of date:
  • SUGGESTION (A child's arguments and environment are memory its parent lays out #1200's scope, not this PR's): sdk/libc/src/misc.rs:262 reports the whole block, so it overstates a caller's room by 21 + len(cwd) bytes (above). One option is to subtract the worst case, 8 for the argc word + 8 for PWD's pointer + "PWD=".len() + MAX_PATH + 1. The other is to record it. Recording or fixing it is your call; it does not affect landing this PR.
  • SUGGESTION: tests/guest/rust-tests/src/bin/served_file_offset.rs:1-11: the header lists the test's claims but not the refused-spawn arm's, and has not since 7f52fb7. Optional.

LAND

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: verified the T14 readback at a336ce8 myself: ===TEST_END test_rs_served_file_offset exit=0=== (after served_file_offset: passed) and ===TEST_END 206_libc_refusals exit=0===; 256 jobs ended; the only nonzero exits are test_rs_wake_onto_busy (54), test_rs_i219_msi (1) and test_rs_usbd_spare_vector (101), the same three as at 23f692c. Description updated with the result. Review: LAND. Queueing.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 15:43
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 1631fff Nov 11, 2026
10 checks passed
@Japabu
Japabu deleted the spawn-tests-after-startup-block branch November 11, 2026 16:03
Japabu added a commit that referenced this pull request Nov 11, 2026
…y come to

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the argument count and the PWD=<cwd> entry the
spawn states (files::starting_in), so a caller's own argv and envp, counted
as POSIX counts them (each string with its NUL, a pointer to each and the
null ending each list), had 21 + len(cwd) bytes less than reported, up to
4117 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its count word and the longest
stated entry: a pointer, PWD=, MAX_PATH bytes and a NUL, from
startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the limit and
the block cannot drift. libc answers it: 2088939. A host test composes what
Helper::spawn composes, the longest stated directory and then the block,
for a caller's MAX_ARGS bytes, and one byte more is refused.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088939.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the argument count and the PWD=<cwd> entry the
spawn states (files::starting_in), so a caller's own argv and envp, counted
as POSIX counts them (each string with its NUL, a pointer to each and the
null ending each list), had 21 + len(cwd) bytes less than reported, up to
4117 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its count word and the longest
stated entry: a pointer, PWD=, MAX_PATH bytes and a NUL, from
startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the limit and
the block cannot drift. libc answers it: 2088939. A host test composes what
Helper::spawn composes, the longest stated directory and then the block,
for a caller's MAX_ARGS bytes, and one byte more is refused.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088939.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the argument count and the PWD=<cwd> entry the
spawn states (files::starting_in), so a caller's own argv and envp, counted
as POSIX counts them (each string with its NUL, a pointer to each and the
null ending each list), had 21 + len(cwd) bytes less than reported, up to
4117 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its count word and the longest
stated entry: a pointer, PWD=, MAX_PATH bytes and a NUL, from
startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the limit and
the block cannot drift. libc answers it: 2088939. A host test composes what
Helper::spawn composes, the longest stated directory and then the block,
for a caller's MAX_ARGS bytes, and one byte more is refused.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088939.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the argument count and the PWD=<cwd> entry the
spawn states (files::starting_in), so a caller's own argv and envp, counted
as POSIX counts them (each string with its NUL, a pointer to each and the
null ending each list), had 21 + len(cwd) bytes less than reported, up to
4117 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its count word and the longest
stated entry: a pointer, PWD=, MAX_PATH bytes and a NUL, from
startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the limit and
the block cannot drift. libc answers it: 2088939. A host test composes what
Helper::spawn composes, the longest stated directory and then the block,
for a caller's MAX_ARGS bytes, and one byte more is refused.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088939.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the argument count and the PWD=<cwd> entry the
spawn states (files::starting_in), so a caller's own argv and envp, counted
as POSIX counts them (each string with its NUL, a pointer to each and the
null ending each list), had 21 + len(cwd) bytes less than reported, up to
4117 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its count word and the longest
stated entry: a pointer, PWD=, MAX_PATH bytes and a NUL, from
startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the limit and
the block cannot drift. libc answers it: 2088939. A host test composes what
Helper::spawn composes, the longest stated directory and then the block,
for a caller's MAX_ARGS bytes, and one byte more is refused.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088939.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the names' address, the argument count and the
PWD=<cwd> entry the spawn states (files::starting_in), so a caller's own
argv and envp, counted as ToyOS counts them (each string with its NUL, a
pointer to each and the null ending each list), had 29 + len(cwd) bytes
less than reported, up to 4125 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its names' address and count
words and the longest stated entry: a pointer, PWD=, MAX_PATH bytes and a
NUL, from startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the
limit and the block cannot drift. libc answers it. A host test composes the
longest stated directory and then the block, with no names, for a caller's
MAX_ARGS bytes, and one byte more is refused. The names table is counted
in a later commit.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088931.

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

sysconf(_SC_ARG_MAX) answered the whole startup block, MAX_BLOCK. A
helper's block also holds the names' address, the argument count and the
PWD=<cwd> entry the spawn states (files::starting_in), so a caller's own
argv and envp, counted as ToyOS counts them (each string with its NUL, a
pointer to each and the null ending each list), had 29 + len(cwd) bytes
less than reported, up to 4125 with a MAX_PATH directory.

toyos::spawn::MAX_ARGS is the block less its names' address and count
words and the longest stated entry: a pointer, PWD=, MAX_PATH bytes and a
NUL, from startup::MAX_BLOCK, startup::WORD and files::MAX_STATED, so the
limit and the block cannot drift. libc answers it. A host test composes the
longest stated directory and then the block, with no names, for a caller's
MAX_ARGS bytes, and one byte more is refused. The names table is counted
in a later commit.

206_libc_refusals.expect, which #1219 set to MAX_BLOCK's 2093056, says
2088931.

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