Skip to content

One table reads every refusal, each runtime keeps one from it to its word, and _SC_ARG_MAX is what a caller may pass - #1227

Merged
Japabu merged 7 commits into
mainfrom
sdk-one-error-table
Nov 12, 2026
Merged

Japabu merged 7 commits into
mainfrom
sdk-one-error-table

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

libc and std each kept their own table for every source of refusal: kernel words, a file server's, the descriptor table's, a stream's, netstack's and a spawn's. Several fell through:

  • libc answered EINVAL for any kernel word it did not name and EIO for half of netstack's words.
  • std answered Other for a device failure, for netstack's limit and for a lookup that found no address.
  • getaddrinfo answered -1 for everything.
  • fread, fwrite and fseek set no errno.
  • The pipe ABI had no word for an unreachable network or host, or for a lookup to retry.
  • The shell said "not found" for every spawn error.

Resulting behaviour

toyos::Failure (sdk/toyos/src/failure.rs) is the single vocabulary at the SDK boundary. It reads every word of SyscallError, fs::Error, files::Error, fd::Error, NetError, a stream's Refused, spawn::Refused, ImageRefused and dl::Refused by name, with no catch-all. A word added to any of those types does not compile until it has a Failure. libc keeps one exhaustive table from Failure to errno (errcode::of), and std keeps one to io::ErrorKind (sys::failed). Where a call's POSIX page gives a refusal a different word, the call names that refusal itself and sends every other refusal through the table. getaddrinfo answers POSIX's EAI_* words from the same failure.

Changes a caller sees:

  • libc: a bad address is EFAULT and an unknown kernel word is EIO (both were EINVAL). No netstack is ENOTCONN and netstack's limit is EAGAIN (both were EIO). A launch that started nothing is EIO, not EACCES. A spawn of a file that is no program this machine lays out is ENOEXEC, a spawn past what one carries E2BIG, and a spawn's other malformed requests EINVAL (all three were E2BIG). fread, fwrite, fseek and ftell set errno.
  • std: a device failure is Uncategorized, as other platforms report it, not Other. A spawn past what one carries is ArgumentListTooLong, std's kind for E2BIG, not InvalidInput. A stream's own handle moved into a spawn, and a stream this process holds no end of, keep their specific texts (Misuse::Moved, Misuse::NotThisProcess). Netstack's limit is QuotaExceeded. A name with no address is NotFound. kill, wait and try_wait answer the kernel's word.
  • The shell says "not found" only for NotFound and prints any other spawn refusal.
  • The SDK: spawn::Refused::Invalid is split. NotExecutable is an image that does not lay out, TooBig a spawn or launch past what it carries (its arguments and environment, its endowments, its names table past startup::MAX_NAMES, a launch's provided connectors, one launch frame), and Invalid the rest.
  • Pipe ABI, netstack's boundary: three new words, ERR_NETWORK_UNREACHABLE (10), ERR_HOST_UNREACHABLE (11) and ERR_TRY_AGAIN (12).
    • A connect ended by ICMP destination unreachable gets a word by its code: network codes give network unreachable, port gives connection refused, everything else gives host unreachable. Prohibited and time-exceeded give host unreachable.
    • A lookup with no way out gives network unreachable, and one whose lease changed gives try again.
    • osbench's lookup retry, the only caller that waited on the old word, now also waits on the new ones.

No netstack stays ENOTCONN and NotConnected rather than ENETDOWN and NetworkDown, as std answered before. sshserver's quiet exit on a machine with no network, osbench and the netstack_gone_mid_bind member all key on that kind, so changing it would move three consumers for a better name and fix no defect.

std's fd_error stays as a one-line name over the table, because the rust fork's BorrowedFd::try_clone_to_owned calls it. This change does not need a fork commit.

_SC_ARG_MAX (added to this PR by the orchestrator) reported the whole startup block, toyos::startup::MAX_BLOCK. Since #1252 the block also holds the names' address, the argument count, the PWD=<cwd> entry the SDK states and the child's names for its endowments. The names table had no bound but the block, so a caller's room depended on what the spawn endowed. The changes:

  • A names table is now at most startup::MAX_NAMES (64 KiB), refused as a spawn too big past it. That is many times any table a spawn in this tree makes: as many names as a spawn endows, of about 680 bytes each.
  • _SC_ARG_MAX reports toyos::spawn::MAX_ARGS: the block less its two words, the longest stated entry (a pointer, PWD=, fs::MAX_PATH bytes and a NUL) and MAX_NAMES, derived from startup::WORD, startup::MAX_BLOCK, files::MAX_STATED and startup::MAX_NAMES. A caller's MAX_ARGS bytes therefore fit whatever a spawn endows.
  • The count is ToyOS's: each string with its NUL, a pointer to each and the null ending each list. POSIX leaves whether pointers and terminators count to the implementation.
  • Helper::spawn builds its child's block with one function, starting_block, which the limit's test calls too with a names table of MAX_NAMES bytes, so the limit and what a spawn composes cannot drift.
  • startup::bytes answers Said, a block's bytes as it laid them out (at most MAX_BLOCK, its field private), and startup::Block::new takes only a Said, so a Block still holds nothing Program::starting_with cannot reserve below the program.
  • The value is 2023395. 206_libc_refusals.expect, which tests: two shared-boot members assert what a spawn does on main #1219 set to 2093056, says so.

Coordination

#1226 landed first, and this branch is rebased onto it (see Checks).

#1258 landed first with the kernel word SyscallError::TextBusy (12). This branch adds Failure::TextBusy and SyscallError::TextBusy => Self::TextBusy, which libc answers ETXTBSY and std ExecutableFileBusy, the words #1258 gave it. libc's POSIX-name test holds the ETXTBSY.

#1272 (in review) adds the file server's error Unanswered, which libc answers ETIMEDOUT and std TimedOut in the old tables. If #1272 lands first, this branch's next rebase adds Error::Unanswered => Self::TimedOut to From<fs::Error> for Failure, and the existing tables give the same two words. If this branch lands first, #1272 does it.

#1261 landed first with the kernel word SyscallError::Busy. This branch adds SyscallError::Busy => Self::Busy to From<SyscallError> for Failure, so libc answers EBUSY and std ResourceBusy, the words #1261 gave it, through the existing tables. libc's POSIX-name test holds the EBUSY.

Records

  • Closed: libc-answers-einval-for-every-error-it-does-not-name, libc-loses-the-kernels-word-on-two-write-paths, std-maps-a-device-error-to-other-not-uncategorized, std-net-maps-every-other-syscall-error-to-other, std-lookup-host-answers-no-address-as-other, the-pipe-abi-has-no-word-for-an-unreachable-host-or-a-lookup-to-try-again, the-shell-says-not-found-for-every-spawn-error.
  • Opened (review r1's sighting): a-connect-with-no-route-answers-not-connected. netstack answers a connect with no route ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.
  • a-full-kernel-handle-table-answers-enomem-not-emfile stays open. The kernel uses one word for a full table and for no memory, and the owner ruled out a kernel change made only for POSIX.

Tests

  • Exhaustiveness is the compiler's: every From into Failure and both runtimes' tables are matches with no catch-all, so no test restates them.
  • Host: toyos::failure's pipe-word test holds netstack's codes, the three new ones among them, and one it does not send. libc's errno_codes tests hold that every code libc answers with is errno.h's by name and value, the POSIX names of the kernel's, the descriptor table's, a spawn's, a file server's and netstack's refusals, and getaddrinfo's netdb.h words.
  • Host: spawn::tests::max_args_of_a_caller_s_own_fit_beside_the_longest_directory calls starting_block with a fs::MAX_PATH-byte directory for a caller's argv and envp of exactly MAX_ARGS bytes. Those bytes are counted apart from the block's arithmetic, as ToyOS counts them. The test asserts the block fits, and that one byte more is refused TooBig.
  • Guest: the existing shared-boot members that assert errnos and kinds (on T14 testcases), plus the QEMU rows listed in the checks comment.

Checks

Evidence, masked, with a README per round listing each command, exit code and revision: r10, r9, r8, r7, r6, r5, r4, r1, r2, r3.

Head 31855f8 is the branch rebased onto main ee15a2e, which has #1226, #1259, #1261, #1239, #1252 and #1258. On top of the rebase are a commit for the names table's bound and one for the rebase review: records corrected for #1271, std's ArgumentListTooLong, and wording. The resolutions, oldest first:

Lands on: review LAND, the host checks below and guest run 43156966054 green at b2946a6. The review found the resolutions and the bound correct, and sent back text only. Beyond b2946a6, this head changes std's TooBig arm (the review commit) and adds the TextBusy mapping (the #1258 rebase), both compiled by the testcases build, plus doc comments and records. The run holds for the rest.

  • Host, targeted, at 31855f8: cargo test -p toyos (startup's and the MAX_ARGS tests among them), cargo clippy -p toyos --all-targets, cargo clippy -p toyos --lib --features fd-table, cargo test -p toyos-libc-copies, cargo clippy -p toyos-libc-copies --all-targets and cargo test -p netstack, all exit 0. Clippy runs with the adopted lints and -D warnings. The host-built code touched is sdk/toyos, sdk/libc/arch and netstack; libc proper, std, the shell, osbench and the rust-tests members are guest-built.
  • Build at 31855f8 (and at c40194e and b2946a6 before it): cargo run -- --build-only --boot-config tests/images/testcases.toml, exit 0. It compiled the sysroot (std and libc, which no host check compiles) and the testcases image. The rust-tests crate is built by the guest run's testcases_members_exit_0.
  • Negative controls at b2946a6: MAX_ARGS one byte smaller, one byte larger, and without the names table. Each was applied as a patch and built, and each fails max_args_of_a_caller_s_own_fit_beside_the_longest_directory (r7).
  • Negative controls: MAX_ARGS one byte under and one over. Each was applied as a patch and built, and each fails max_args_of_a_caller_s_own_fit_beside_the_longest_directory at one of its two assertions (r2).
  • Guest rows:
    • Run 43036792480 at 55a94c8, both archs: exit 0, 10 of 10 (the table: libc's errnos, netstack's refusals over the pipe ABI, user copies ending as a bad address).
    • Run 43084578853 at 7826f4f, libc_children virt_user_copy_faults: exit 0, 2 of 2. It covers libc's posix_spawn errno path and every child spawned through starting_block, and it built and booted the aarch64 image.
    • Run 43156966054 at b2946a6: libc_children virt_user_copy_faults a_spawn_from_an_image_frees_it_when_the_child_ends testcases_members_exit_0, exit 1 (r9). The three rows passed. testcases_members_exit_0 reports 240 of 242 members exiting 0. The two red members, diagnosed as main's and awaiting main's run 43155677796 of the same row at this branch's base:
      • test_rs_abuse_listener_hijack gets the kernel's raw PermissionDenied where it expects InvalidArgument.
      • test_rs_adopted_services_stay's adopter fails its assertion that toyos::endow::services() lists the service it adopted. Its spawn succeeded.
      • Neither the kernel nor toyos::endow is changed here, and the child's block bytes for these spawns are main's.
      • testcases_members_exit_0 runs the testcases shared boot on QEMU since The shared boots run as guest rows, each member judged by its own exit #1271: every Rust member, the two guest tests in the conflict among them, and the C corpus. 206_libc_refusals (2023395) and 212_full_table (ENOMEM) are among the 240 that exited 0.
      • Runs 43122100459 at 96b79b6 and 43107765943 at f341be8 were cancelled.
  • Owed on metal (the benches are down, so metal is no longer a landing condition; the item is recorded in issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md): the T14's testcases-watchdog boot at main after this PR, which no guest row runs. main must show it green. The testcases members, 212_full_table printing ENOMEM and 206_libc_refusals printing 2023395 among them, are the guest row testcases_members_exit_0's, which run 43156966054 carries.
  • Image build: the metal staging at 7826f4f (cargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases, exit 2) built the x86_64 testcases image: the sysroot with this std and libc, every rust-tests member and the C corpus. The staged images are deleted.
  • Not run: the full --ci host. The merge queue runs it.

🤖 Generated with Claude Code

@Japabu
Japabu force-pushed the sdk-one-error-table branch 2 times, most recently from fc819b2 to 22440c6 Compare November 11, 2026 16:19
@Japabu Japabu changed the title One table reads every refusal, and each runtime keeps one from it to its word One table reads every refusal, each runtime keeps one from it to its word, and _SC_ARG_MAX is what a caller may pass Nov 11, 2026
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1227 at 22440c6 (on 67713cb)

Architecture

toyos::Failure is the SDK's one vocabulary for a refusal, and it is the right owner: the SDK translates at its boundary, toyos-abi is untouched, and the three new pipe words live in netstack's protocol (sdk/toyos/src/net.rs), not the kernel ABI.

The table is complete and has no catch-all.

  • Every From in failure.rs is exhaustive, and so are libc's errcode::of and std's failed. The catch-all other => arm in of_lookup only forwards to of as EAI_SYSTEM, and the PR justifies it.
  • Forward compatibility belongs to the decoders: SyscallError::from_u64 → Unknown, and NetError::from_error_code → Protocol(code). The table therefore needs no _ arm, and A poll belongs to the handle it was made through, and a ring to its one submitter #1261's Busy cannot compile until someone adds a line. All 11 kernel words are listed.
  • No hand-written map of SyscallError, NetError or fs::Error remains in std or libc. The only exception is the mmap family's POSIX override (range_errno).
  • The kernel's vm_* words are InvalidArgument, ResourceExhausted, BadAddress and PermissionDenied. mmap, munmap and mprotect still answer what they answered before.

No fork change is needed. At the pinned rust commit a189b3a6, the fork calls only crate::sys::fd_error (library/std/src/os/fd/owned.rs:164), and that function is kept.

Main's changes since the base break nothing. Main 45935f4 adds no variant to SyscallError, fd::Error or spawn::Refused. #1238 adds only path_label and changes nothing MAX_ARGS derives from. The exhaustive matches therefore compile after the merge.

The _SC_ARG_MAX arithmetic is correct.

  • The block holds 8·(argc + envc + 3) + Σ(len + 1) bytes.
  • The caller's count is 8·(argc + 1) + 8·(envc + 1) + Σ(len + 1).
  • The stated PWD= entry adds 8 + 4 + MAX_PATH + 1 and replaces any PWD the caller passed.
  • So 2093056 − 8 − 4109 = 2088939 for every cwd. The value is conservative and above _POSIX_ARG_MAX.

POSIX semantics. XSH exec says it is "implementation-defined whether null terminators, pointers, and/or any alignment bytes are included" in ARG_MAX. Counting pointers is therefore a valid choice, but it is ToyOS's choice, not POSIX's (see the third blocker).

The host test builds the block the way a spawn does today. Helper::spawn calls files::starting_in(s.env, s.cwd) and then Block::new → startup::bytes(argv, &env) (spawn.rs:221-226), and the test makes the same two calls in the same order. Worked through by hand, the test fits exactly MAX_BLOCK at MAX_ARGS and refuses one byte more. The evidence controls red both mutations. The test copies that composition rather than calling it; see the suggestions.

Findings

BLOCKER: sdk/toyos/src/failure.rs:96-109 and 270-312, and sdk/libc/arch/src/errno_codes.rs:30-42. Failure::ALL is test-only machinery, and four tests check what the compiler already guarantees.

  • Failure::ALL. It is public SDK production code, and its only users are the tests every_failure_is_listed_once and libc's every_failure_s_errno_is_one_errno_h_defines. The repository's rule is that no production machinery ships solely to satisfy a test.
  • every_kernel_word_is_a_failure and every_file_protocol_word_is_a_failure. Both loop over a total, exhaustive From, which cannot fail. let _ = Failure::from(word) can expose nothing. words > 30 tests fs::Error::from_byte, not this module. The spot asserts restate single match arms.
  • every_failure_s_errno_is_one_errno_h_defines. It is implied by the existing every_code_libc_answers_with_is_errno_h_s, because of returns only errcode.rs's named constants.
  • Required. Delete ALL and these four tests. Keep every_pipe_word_is_a_failure, which checks net.rs's decoding of the three new codes, plus the libc POSIX-name tests and the netdb.h test. The PR body's claim that the tests show "every kernel code … reads as a failure" then goes; exhaustiveness is the compiler's job.

BLOCKER: sdk/toyos/src/failure.rs:92-93 and 253. Refused::Invalid(_) => TooBig gives most of TooBig's sources a false contract.

  • The problem. TooBig is documented as "A spawn carrying more than one carries", but Refused::Invalid also carries other refusals:
    • an ELF that does not lay out (spawn.rs:229-230);
    • no image (218);
    • a bad working directory (222);
    • a spawn kind's shape (191);
    • the launch-shape refusals (338-362).
  • Consequence. The new SDK word's contract is false for most of its sources. libc's posix_spawn of a file that is no loadable program answers E2BIG, where POSIX's word is ENOEXEC. libc did that before, but this PR now names it a size refusal.
  • Required. Make the contract true. The preferred fix is small: split spawn::Refused::Invalid so the layout refusal reads NotExecutable, the size refusals (226, 236, 254, 393) read TooBig, and the rest read InvalidArgument. The minimum fix is a truthful name and doc for the variant, plus a record of the ENOEXEC gap.

BLOCKER: sdk/toyos/src/spawn.rs:37 and 517. The comments "counted as POSIX's ARG_MAX counts them" and "POSIX's count" are false. POSIX leaves the counting of pointers and terminators to the implementation (quoted above). State it as ToyOS's count.

Suggestions

SUGGESTION: spawn.rs:509-530. The test duplicates Helper::spawn's two-call composition instead of using it. If a spawn later states another entry, MAX_ARGS becomes wrong and this test stays green. One function that both Helper::spawn and the test call, from (argv, env, cwd) to block bytes, would make "cannot drift" true.

SUGGESTION: sdk/std/sys/pal/mod.rs:93-104. Refused::Stream(handle) used to say Misuse::Moved(handle) and now says "the descriptor names no object of that kind", which is wrong for a stream's own handle moved into a spawn. A stream's NotThisProcess(handle) similarly lost its handle. Keep the specific text in refused(), as Permission and Invalid keep theirs.

SUGGESTION: NetstackNotFound → ENOTCONN / NotConnected. POSIX's connect page has no ENOTCONN; ENETDOWN and Rust's ErrorKind::NetworkDown name "no network". This matches std's earlier choice, so it is optional.

Sighting, not this PR: netstack's connect with no route (system/netstack/src/serve.rs:263) still answers ERR_NOT_CONNECTED, now that ERR_NETWORK_UNREACHABLE exists.

Coordination note: this branch and #1226 overlap at sdk/libc/src/posix_io.rs dup2, where #1226 keeps fd_errno, which this branch deletes. Whichever lands second writes crate::errcode::of(e). #1261's Busy is a compile error until mapped, as the PR states.

Evidence

Guest run reuse is valid. Run 43036792480 at 55a94c8 succeeded, 10 of 10 rows. git diff 55a94c866 668f1277b changes nothing under sdk/, system/ or osbench, so the run still applies to the table. The rows cover libc's errnos, netstack's refusals over the pipe ABI and EFAULT from user copies. No row reaches the new ICMP and lookup words, and inspection of unreachable_word and answer_resolved is enough for those.

Host evidence. cargo test -p toyos, cargo test -p toyos-libc-copies and cargo test -p netstack, with clippy where a shape exists, all exit 0, and the MAX_ARGS controls are in r1/controls.

LAND-pending condition: metal. Run the T14 testcases boot at the final head. Every corpus case and Rust member must be green, including 206_libc_refusals printing sysconf _SC_ARG_MAX: 2088939 and 212_full_table still printing ENOMEM. testcases-watchdog must be green.

  • If the Refused::Invalid split lands, rerun cargo test -p toyos and cargo test -p toyos-libc-copies.
  • The test deletions need only compile.

Line counts. Production grows about +170 lines (+667/−498), from one table and two runtime tables replacing ten mappers. Tests grow about +200 lines. Records go −193/+10.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the sdk-one-error-table branch from ea69cd0 to 2795520 Compare November 11, 2026 18:51
Japabu added a commit that referenced this pull request Nov 11, 2026
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Response to review r1 (head 2795520, rebased onto e558cc1)

  • BLOCKER Failure::ALL and four tests: fixed. ALL is deleted, along with every_failure_is_listed_once, every_kernel_word_is_a_failure, every_file_protocol_word_is_a_failure and libc's every_failure_s_errno_is_one_errno_h_defines. The pipe-word test, libc's POSIX-name tests and the netdb.h test stay. The body's claim that these tests show exhaustiveness is gone.
  • BLOCKER Refused::Invalid => TooBig: fixed by the preferred split.
    • NotExecutable(&str): the layout refusal (ENOEXEC).
    • TooBig(&str): arguments and environment, endowments, labels and the launch frame. It also takes the launch's "more connectors provided than a launch carries", which is a count bound like the endowments' one (E2BIG).
    • Invalid(&str): everything else, now InvalidArgument (EINVAL).
    • libc's POSIX-name test holds all three.
  • BLOCKER "POSIX's count": fixed. Both comments say the count is ToyOS's, and that POSIX leaves whether pointers and terminators count to the implementation.
  • SUGGESTION shared composition: taken. starting_block(argv, env, cwd) is what Helper::spawn calls, and the test calls it too. To do that, startup::Block::new now takes the block's bytes, startup::bytes is public and startup::Refused is gone. The two guest tests that build a block call startup::bytes first.
  • SUGGESTION std texts: taken. refused() says Misuse::Moved(handle) for a stream's own handle. A new stream_failed says Misuse::NotThisProcess(handle), and TcpStream's stream calls use it. A file call on a stream descriptor still answers the table's generic text.
  • SUGGESTION ENETDOWN: declined. sshserver's quiet exit, osbench and the netstack_gone_mid_bind member all key on NotConnected. Moving them is a naming change, not a defect fix.
  • Sighting: recorded as issues/a-connect-with-no-route-answers-not-connected.md.
  • Coordination: the body notes A process holds its stdio stream state once, with its descriptor table #1226's dup2 and A poll belongs to the handle it was made through, and a ring to its one submitter #1261's Busy.

Checks: the targeted host tests and clippy exit 0 at 2795520, and the MAX_ARGS controls were re-run against the reworked test. The T14 testcases boot is staged at 2795520. Evidence: https://github.com/ToyOSOrg/evidence/tree/dfbfd7f3071b7bf42c597757bad7edd9a6fc83eb/r2

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1227 at 2795520 (on main e558cc1)

Scope: r1's findings and the r1 commit's code. git range-diff shows the first two commits are patch-identical to the reviewed 668f127 and 22440c6.

Architecture

Unchanged: the SDK keeps one vocabulary of failures, and each runtime keeps one table from it. The split costs three variants and makes each spawn::Refused say what the refusal is.

r1 findings

BLOCKER, Failure::ALL and four tests: resolved. ALL and the four tests are gone. Nothing in the tree or at the rust fork's pin (a189b3a6) names them. The pipe-word test, libc's POSIX-name tests and the netdb.h test remain.

BLOCKER, Refused::Invalid => TooBig: resolved. Each refusal now maps to a true word:

  • NotExecutable → ENOEXEC, std InvalidData. Only lay_out's refusals take it (sdk/toyos/src/spawn.rs:233): a parse failure, no PT_LOAD over the headers, shared pages, an image below its base, or W+X. Correct.
  • TooBig → E2BIG. It covers the block (459), the endowment count (239), the labels (257), the provided connectors (357) and one frame (396).
  • The connectors as a size refusal: accepted.
    • MAX_LAUNCH_EXTRAS is the capacity of one crossing, the transfer-handle limit (launch.rs:67-70). That is the same kind of bound as MAX_SPAWN_ENDOWMENTS, which r1 put under TooBig.
    • Its neighbour fds.len() > MAX_LAUNCH_FDS correctly stays Invalid. By then kind() has refused any descriptor above 2, so a fourth descriptor repeats a number, which makes the request malformed rather than too large.
    • libc never reaches the launch path with connectors, so no POSIX caller gets E2BIG from it.
  • Invalid → EINVAL. It covers the remaining refusals: a shape the kind does not take, no image, a bad cwd, a launch's shared file connection, a repeated stdio number, and a job given with rights. Correct.
  • Tests. git grep finds no guest test or corpus case that asserts E2BIG or ENOEXEC from a spawn, so the move changes no expected output. errno_codes::a_refusal_above_the_kernel_is_its_own_errno holds the three words.

BLOCKER, "POSIX's count": resolved. spawn.rs:36-42 and the test's comment now say the count is ToyOS's and that POSIX leaves it to the implementation, as XSH exec says.

Suggestions.

  • starting_block: taken. See the new finding below.
  • std texts: taken. refused() says Misuse::Moved(handle) again. stream_failed restores NotThisProcess(handle) for every stream::* call TcpStream makes on its handle. The failed calls left in connection.rs take NetError or fd::Error.
  • ENETDOWN: declined with a reason, which settles it.
  • The new record a-connect-with-no-route-answers-not-connected matches serve.rs:263 and osbench's retry (internet.rs:112).

New finding

BLOCKER: sdk/toyos/src/startup.rs:84 with sdk/toyos/src/elf/program.rs:48-51. Block::new now takes any &[u8], so a Block no longer guarantees what Program::starting_with relies on.

  • Before. Block::new(args, env, job) made its bytes with bytes(), so every Block held a laid-out block of at most MAX_BLOCK bytes. That made reserved(startup::AT + pages, BASE) (program.rs:51, which computes hi - lo) safe by construction.
  • Now. Block::new and starting_with are both public. Given more than RESERVED (2 MiB) of bytes, AT + pages exceeds BASE, and the toyos profile's overflow-checks turn the spawn into a subtraction-overflow panic inside the SDK. Given bytes that are not bytes()'s output, the child gets pointers it cannot follow.
  • Today. Every caller passes bytes()'s output, so nothing in the tree breaks. But the invariant has moved from the type into a doc comment, "A block holding bytes ([bytes])".
  • Why it matters. Root CLAUDE.md asks to make bugs unrepresentable first. The split was needed only so that a host test could compose a block without a memory object, and it did not need to give up the type.
  • Fix, about five lines. bytes() returns a newtype whose field is private and that only bytes() constructs, and Block::new takes it. starting_block returns it, the MAX_ARGS test stays as it is, and the two guest tests read the same.
  • What is justified. Making bytes public is fine: those guest tests and starting_block call it.

Evidence

  • Host at 2795520 (r2/host/279552072): the toyos tests and both clippy shapes, the toyos-libc-copies tests and clippy, and the netstack tests all exit 0.
  • Controls. The MAX_ARGS controls ran at ea69cd0, before the rebase. The rebase only added path_label beside the test, so they still hold.
  • aarch64. The r2 staging log also built the sysroot for aarch64-unknown-toyos, including std, toyos and toyos-libc (log lines 112-215). The SDK, std and libc changes therefore compile for aarch64 at this head. The rust-tests crate (image.rs, spawn_image_object) and the system programs were not built for aarch64.
  • Staging warnings. The std warnings in the staging log (os.rs:113 and 117, fs.rs:309) are at the same lines on main, so they are not this PR's.

A guest dispatch is owed before the T14 boot.

  • Why. Since run 43036792480 at 55a94c8, Helper::spawn builds every child's block through starting_block and Block::new. std's Command reaches that path, and the supervisor starts every service through Command, so a defect there stops every boot after the supervisor. No guest run has booted 2795520 or a code-identical head, so the rule that a guest boot precede a bench flash is not met.
  • What. Dispatch once, at the final head after the fix (the fix touches the same path): libc_children virt_user_copy_faults.
    • libc_children (QEMU x86-64) covers libc's posix_spawn errno path through errcode::of(&Refused), and every C child is spawned through the new composition.
    • virt_user_copy_faults (QEMU virt, AArch64) builds the aarch64 image and system programs at this head. Its boot spawns every service and the test job through the startup block, which is loader code shared with AArch64.

LAND-pending after the fix:

  • Host: cargo test -p toyos and its two clippy shapes at the final head.
  • Guest: the dispatch above, green.
  • Then metal: T14 testcases at the final head.
    • Every corpus case and Rust member is green.
    • 206_libc_refusals prints sysconf _SC_ARG_MAX: 2088939.
    • 212_full_table still prints ENOMEM.
    • testcases-watchdog is green.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
Block::new took any byte slice once it stopped composing the block itself,
so a Block no longer promised a laid-out block of at most MAX_BLOCK bytes,
which Program::starting_with relies on when it reserves from the block's
end up to the program. A public caller could make the SDK's spawn overflow
that subtraction, or hand a child pointers it cannot follow.

startup::bytes now answers Said, whose field is private and which only it
makes, and Block::new takes a Said. starting_block answers one, and the
callers read as before.

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

Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Response to review r2 (head 7826f4f)

  • BLOCKER Block::new takes any &[u8]: fixed. startup::bytes answers Said, whose field is private and which only bytes constructs. Block::new takes a &Said, and starting_block answers one. The callers read as before, including the two guest tests.
  • Checks: cargo test -p toyos and both toyos clippy shapes exit 0 at 7826f4f. Guest run 43084578853 (libc_children virt_user_copy_faults) is dispatched. The T14 testcases boot is staged at 7826f4f. Evidence: https://github.com/ToyOSOrg/evidence/tree/7749d6336f32e4b31e924507a54ac3f1a6f75fd1/r3

Japabu added a commit that referenced this pull request Nov 11, 2026
The corpus runs only on the T14's testcases boot, and no bench can boot.
The full-table record, whose corpus case is 212_full_table, carries what
main must show there when a bench returns: every member green,
212_full_table's ENOMEM, 206_libc_refusals' _SC_ARG_MAX of 2088939 and the
watchdog boot green.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review r3 of #1227 at 7826f4f (on main e558cc1)

Scope: r2's blocker, the one commit since 2795520, and the PR body. git diff 279552072 7826f4f39 touches only sdk/toyos/src/startup.rs and sdk/toyos/src/spawn.rs (+17/−10).

Architecture

The fix puts the guarantee back in the type, in the module that owns the layout. startup is the only module that can make a Said, a Block can only be made from a Said, and elf::program still relies on Block alone. The production code grows by the newtype and its doc comment. The tests change only by destructuring.

r2 blocker: resolved

Block::new took any &[u8].

  • Said can come only from bytes. Said(Vec<u8>) (startup.rs:49) has a private field and no Default, Clone, From, Deref or accessor. Its only construction is Some(Said(block)) in bytes (startup.rs:75), so nothing outside startup and its test module can make or change one.
  • Every Block holds bytes' output. Block's fields are private, and Block::new(&Said, …) (startup.rs:90) is its only constructor. Every Block therefore holds at most MAX_BLOCK = 2 MiB − 4 KiB of bytes' output. In starting_with (program.rs:49-51) that gives pages ≤ RESERVED − PAGE, so reserved(AT + pages, BASE) cannot underflow.
  • Callers.
    • Helper::spawn (spawn.rs:228-231) goes through starting_block (spawn.rs:456).
    • The two guest helpers (tests/guest/rust-tests/src/image.rs:48-49, tests/guest/rust-tests/src/bin/spawn_image_object.rs:36-37) already bound said at 2795520. By inspection they type-check against &Said unchanged.
    • Nothing else names startup::bytes, Block::new or Said: not the rest of the tree, not libc, and not the rust fork at its pin a189b3a6.
  • Comments. The Said and Block::new doc comments state the contract and nothing more.

New findings

BLOCKER: the PR body and the branch's issue records. The body still makes metal a landing condition, and no record says what metal owes.

  • The rule. The orchestrator ruled on 2026-11-11 that the benches are down, after this head was pushed, so this is not the author's omission. Under that ruling metal is no longer a landing condition. Each check that only metal can establish goes as a bullet beginning Owed on metal: in an issue record this PR updates. The bullet names the rows, the bench and what main must show.

  • The body. It still says "Landing needs every corpus case and Rust member green…", and that run 43084578853 "must be green before the T14 boot". At 7826f4f, git grep -i 'owed on metal' -- issues finds nothing.

  • Consequence. Claims that only the T14 testcases boot checks would land with no record that nobody has checked them. The C corpus is registered only in the metal shared boot (shared_metal → c_corpus_metal, tests/toyos.rs:2244). These therefore run on no guest:

    • 206_libc_refusals printing sysconf _SC_ARG_MAX: 2088939, no errno;
    • 212_full_table still printing ENOMEM;
    • the shared-boot Rust members that assert errnos and kinds;
    • testcases-watchdog.

    Landing them without a record is the silent debt root CLAUDE.md rules out.

  • Fix.

    • Add an Owed on metal: bullet to a record this PR already updates. a-full-kernel-handle-table-answers-enomem-not-emfile already concerns 212_full_table, but the author picks the home. The bullet names T14 boot:testcases and testcases-watchdog, and the four outputs above as what main must show.
    • The change is records only, so it needs no new guest dispatch or host rerun.
    • The body's Checks and Tests sections then list this owed item instead of the metal landing condition. Correcting the body is the orchestrator's job.

SUGGESTION: startup.rs:48. PartialEq and Eq on Said are used only by assert_eq!(bytes(&past, b""), None) (startup.rs:178). Writing assert!(bytes(&past, b"").is_none()) would let them go. Optional.

PR body, otherwise

Evidence

  • Host (reviewer), at 7826f4f in a detached worktree:
    • cargo test -p toyos exits 0, and the three startup tests and max_args_of_a_caller_s_own_fit_beside_the_longest_directory pass.
    • cargo clippy -p toyos --all-targets and cargo clippy -p toyos --lib --features fd-table each exit 0. Both ran with build/clippy.rs's six adopted lints and -D warnings.
    • These results match the author's r3 host logs.
  • Guest run 43084578853 (libc_children virt_user_copy_faults, headSha 7826f4f) was still queued at about 20:05Z, so there is no result to read yet. It remains the guest landing condition r2 set: green at 7826f4f or at a head with identical code.
  • Metal is no longer a landing condition under the ruling; see the blocker.

SEND BACK. The code is finished. Still needed: the owed-on-metal record, the matching body correction, then a green 43084578853.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator note on review r3: its one blocker (no owed-on-metal record; body still made metal a landing condition) is already resolved at 648a40c, a records-only commit on 7826f4f (verified: +7 lines in issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md, naming T14 testcases + testcases-watchdog with 212_full_table, 206_libc_refusals and every corpus case/Rust member green as what main must show). The body was corrected at the same time. #1227 lands when guest run 43084578853 (code-identical) is green.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: landing under the benches-down rule. Review r3's only blocker (owed record) was already resolved at 648a40c (verified). Guest run 43084578853 at code-identical 7826f4f: success. Corpus checks (206_libc_refusals, 212_full_table) are owed on metal and will also run on the shared-boot guest row (#1271) on main; a red there is fixed forward.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 21:20
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
@Japabu
Japabu removed this pull request from the merge queue due to a manual request Nov 11, 2026
Japabu added a commit that referenced this pull request Nov 11, 2026
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the sdk-one-error-table branch from 648a40c to f341be8 Compare November 11, 2026 21:58
Japabu added a commit that referenced this pull request Nov 11, 2026
Block::new took any byte slice once it stopped composing the block itself,
so a Block no longer promised a laid-out block of at most MAX_BLOCK bytes,
which Program::starting_with relies on when it reserves from the block's
end up to the program. A public caller could make the SDK's spawn overflow
that subtraction, or hand a child pointers it cannot follow.

startup::bytes now answers Said, whose field is private and which only it
makes, and Block::new takes a Said. starting_block answers one, and the
callers read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
The corpus runs only on the T14's testcases boot, and no bench can boot.
The full-table record, whose corpus case is 212_full_table, carries what
main must show there when a bench returns: every member green,
212_full_table's ENOMEM, 206_libc_refusals' _SC_ARG_MAX of 2088939 and the
watchdog boot green.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the rebase onto #1226 (f341be8): git range-diff shows commits 2–5 unchanged; commit 1 differs by the one conflict, libc's dup2, now #1226's one line with #1227's error table (crate::errcode::of), the same shape as dup above it (read with git diff). Lands when guest run 43107765943 (libc_children + virt_user_copy_faults, the first build of the resolved dup2) is green.

Japabu added a commit that referenced this pull request Nov 11, 2026
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the sdk-one-error-table branch from f341be8 to 96b79b6 Compare November 11, 2026 23:44
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Nov 12, 2026
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the rebase onto main 3c5c555 at a92cc30: I read the range-diff against 96b79b6. Commits 2–5 are =. Commit 1 differs only in:

The resolution is mechanical, so the queued run 43122100459 at 96b79b6 (hold branch hold/sdk-one-error-table-96b79b6c4) stays valid for everything else. The Busy arm is settled by the author's host checks and --build-only. #1227 lands when 43122100459 is green.

Composition note: #1258 adds TextBusy. Whichever of #1227 and #1258 lands second maps it in Failure, giving libc ETXTBSY and std ExecutableFileBusy.

Japabu added a commit that referenced this pull request Nov 12, 2026
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the sdk-one-error-table branch from a92cc30 to b2946a6 Compare November 12, 2026 04:46
Japabu added a commit that referenced this pull request Nov 12, 2026
Block::new took any byte slice once it stopped composing the block itself,
so a Block no longer promised a laid-out block of at most MAX_BLOCK bytes,
which Program::starting_with relies on when it reserves from the block's
end up to the program. A public caller could make the SDK's spawn overflow
that subtraction, or hand a child pointers it cannot follow.

startup::bytes now answers Said, whose field is private and which only it
makes, and Block::new takes a Said. starting_block answers one, and the
callers read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
The corpus runs only on the T14's testcases boot, and no bench can boot.
The full-table record, whose corpus case is 212_full_table, carries what
main must show there when a bench returns: every member green,
212_full_table's ENOMEM, 206_libc_refusals' _SC_ARG_MAX of 2088939 and the
watchdog boot green.

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

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1227's rebase onto #1252 at b2946a6

Scope: the conflict resolutions against #1252, the new commit b2946a6, and the Busy arm. I compared git range-diff 3c5c55584..a92cc309e origin/main..0d7382974 with r7's range-diff-a92cc309e-b2946a672.txt; they agree. Commit 1 is =, and only commits 2–4 changed. I ran no builds or tests.

Architecture

The rebase keeps the settled design. The names cap sits in the owner of the layout: startup::table, the only writer of a names table, refuses anything over MAX_NAMES. spawn::MAX_ARGS is derived from startup's constants, and Helper::spawn composes its block through the one function the limit's test calls. Every block is built by startup::bytes, whose callers are starting_block and the two guest helpers. libc's execv/execve are refusals, and the launcher's children go through Helper::spawn, so no path builds a block without the cap. The reader in the child still checks the table against the reservation, not MAX_NAMES. That is correct: the cap is the SDK writer's policy for ARG_MAX, not a property of the format. The new commit adds about 9 production lines in startup.rs (5 of them comment) and 4 in spawn.rs. The tests change by about +5/−3.

Resolutions

  • startup.rs. Against main, the only changes are:

    • WORD replaces the literal 8s;
    • Said is added, and Block::new(&Said, job) takes it;
    • startup::Refused is deleted;
    • the new cap.

    A child's handles are a list its parent names: labels and namespaces leave the kernel #1252's layout (names' address, count, pointers, strings, table), table, the reader and its tests are untouched. bytes still checks words * WORD + text + table.len() against MAX_BLOCK, so a Said stays ≤ MAX_BLOCK, and the invariant Program::starting_with relies on holds.

  • Helper::spawn. Against main, A child's handles are a list its parent names: labels and namespaces leave the kernel #1252's table is kept unchanged: the caller's endowments, inherited services and descriptors. The only differences are the refusal words, TooBig (count), Invalid (duplicate or empty name, bad cwd), TooBig (block) and NotExecutable (layout), plus starting_block. No check is lost.

    • The cwd check now runs inside starting_block, after the table is built rather than before it. Endowments are still closed on that refusal, because table drops them. The only effects are a few dups on a spawn that will be refused, and TooBig instead of Invalid when a spawn is both oversized and given a bad cwd. That is acceptable, since it keeps a single composition function for the test to call.
  • Guest helpers. spawn_image_object.rs and image.rs call startup::bytes(.., names) → Block::new(&said, None). laid_naming is A child's handles are a list its parent names: labels and namespaces leave the kernel #1252's. Its only caller, spawn_child_ends_first, passes short names, well under the cap.

  • Busy. The SyscallError::Busy => Self::Busy match is exhaustive. Main's old tables answered EBUSY and ResourceBusy, and this branch's tables give the same answers (errcode.rs:86, pal/mod.rs:48). (Busy, "EBUSY") is in libc's name test.

_SC_ARG_MAX = 2023395

  • The value. 2093056 (MAX_BLOCK) − 16 (names' address, argc) − (8 + 4100 + 1) (PWD pointer, PWD= + 4096, NUL) − 65536 = 2023395.
  • The block. It has argc + envc + 4 words. Two of those words are the argv and envp nulls, which the caller's count already includes. The other two are the 2 * WORD that MAX_ARGS subtracts, so no word is counted twice. Strings and table are unpadded.
  • A guarantee, not a hard limit. starting_in drops a caller's own PWD= and strings skips empty strings, so both only reduce the block. Any caller count ≤ MAX_ARGS fits beside the longest directory and a full table, which is what ARG_MAX promises.
  • The test is tight both ways. I worked through the arithmetic: with MAX_ARGS bytes, a 4096-byte cwd and a 65526-byte name, the block is exactly MAX_BLOCK.
  • Bookkeeping. Before this commit .expect said 2088931. The 2088939 in the brief and in the record was the pre-A child's handles are a list its parent names: labels and namespaces leave the kernel #1252 value. Both now say 2023395.

Refusal word for an oversized names table

table → None → bytes → None → starting_block → Refused::TooBig("arguments, an environment and names past what a child starts with") → Failure::TooBig. libc answers E2BIG, the same word as the endowment-count bound, which is right. std answers InvalidInput; see S1. The u16/u32 try_froms in table can no longer fail, because a name is at most MAX_NAMES − 10 < 65535. That is harmless.

Controls

r7's three patches apply to spawn.rs blob 62803b3d2, which is this head's. Each fails where it should, exit 101:

  • under → spawn.rs:621, "one byte past MAX_ARGS";
  • over → :618;
  • no names → :618.

Removing the cap in table would be caught by startup's new boundary assertion. A 65527-byte name fits u16, so table would answer Some, and the assertion would fail. That follows from inspection, so no control is needed.

Findings

B1: issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md:28-29. "No guest row runs the corpus." has been false since #1271, which is in this branch's base.

  • testcases_members_exit_0 (tests/toyos.rs:537) boots shared_metal's testcases list on QEMU. That list includes c_corpus_metal, whose ccheck compares each case with its .expect (tests/toyos.rs:2540-2566).
  • The PR body depends on this: its own guest-run bullet says the row runs the C corpus with 2023395 and ENOMEM expected. Its owed-on-metal bullet then repeats the record's false sentence.
  • b2946a6 edited this same bullet and left the sentence in.
  • Consequence. The record claims metal is the only check of 206_libc_refusals, 212_full_table and the Rust members. Once 43156966054 is green, QEMU will have established those contents at this head, and the owed item will overstate what is unchecked.
  • Fix (records only, no new run).
    • Delete the sentence.
    • Narrow the owed bullet to what the guest row does not establish (testcases-watchdog is metal-only, tests/toyos.rs:1077), or say why the T14 run of those outputs is still owed.
    • The body's owed-on-metal bullet needs the same correction, which is the orchestrator's.

B2: PR body, Checks, "Host, targeted". "cargo test -p netstack exit 0 at 2795520; netstack is unchanged since" is false.

S1: sdk/std/sys/pal/mod.rs:70. std answers Failure::TooBig with InvalidInput, while libc answers E2BIG.

  • std's own word for E2BIG is ErrorKind::ArgumentListTooLong, stable since 1.83. The fork's unix decoder maps E2BIG to it.
  • This PR already prefers "as other platforms report it" for Uncategorized.
  • It is not a regression (main answered InvalidInput too), so it is optional.

S2: sdk/toyos/src/spawn.rs:598-601. The test's doc says "with the most names a block holds", which reads as MAX_SPAWN_ENDOWMENTS names. The test actually uses one name filling MAX_NAMES bytes. Suggested wording: "with a names table of MAX_NAMES bytes".

S3: sdk/toyos/src/startup.rs:56-57. "It is many times any table a spawn in this tree makes" is a claim about today's callers, and it will go stale. The capacity clause (96 names of 680 bytes, which I checked) is enough rationale.

S4: issues/a-spawn-drops-an-empty-argument.md:11. It still names toyos::startup::Block::new as the writer that skips empty strings. Since r2 that is toyos::startup::bytes. This predates this round and was missed in r2, so the one-word fix is optional here.

Evidence

  • Host (r7/host/b2946a672). All five exit 0 at rev b2946a6:
    • cargo test -p toyos (227 passed, including a_table_holds_as_many_names_as_a_spawn_endows, a_block_past_its_reservation_is_refused and the MAX_ARGS test);
    • both toyos clippy shapes, with the six adopted lints and -D warnings;
    • toyos-libc-copies test and clippy.
  • Build. --build-only --boot-config tests/images/testcases.toml at b2946a6 exits 0. It compiles std, libc and every system program. The rust-tests crate is compiled only by the guest run.
  • Guest. Run 43156966054 at b2946a6 (libc_children virt_user_copy_faults a_spawn_from_an_image_frees_it_when_the_child_ends testcases_members_exit_0) was queued when this review was written, so there is no result to read. It is the right row set:
    • testcases_members_exit_0 builds and runs the two conflicted guest tests and compares 206_libc_refusals and 212_full_table with their .expect;
    • libc_children covers posix_spawn → starting_block → E2BIG/errno;
    • virt_user_copy_faults builds the aarch64 image over the shared spawn path.

SEND BACK. The code is correct. B1 is a records-only fix by the author, and B2 is a body correction by the orchestrator. Neither needs a new guest run. Landing also waits on 43156966054 being green.

Japabu added a commit that referenced this pull request Nov 12, 2026
… and std's word for too big is E2BIG's

#1271 made the testcases shared boot a guest row (testcases_members_exit_0),
which runs every Rust member and the C corpus on QEMU, so the records'
"no guest row runs the corpus" and "the member runs only on metal" were
false. The full-table record now owes metal only the testcases-watchdog
boot, which no guest row runs. The record of the stream state's TCP half
owes stream_state_is_one's run at main to that guest row, since what it
holds is no bench's.

std answers Failure::TooBig ArgumentListTooLong, std's own kind for E2BIG,
which libc answers, rather than InvalidInput.

The MAX_ARGS test's doc says its names table is MAX_NAMES bytes, MAX_NAMES's
doc keeps only its capacity, and the record of a dropped empty argument
names startup::bytes as the writer that skips it.

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

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification at c40194e, from b2946a6, which the rebase review read:

  • B1: the full-table record no longer says no guest row runs the corpus. Its metal debt is narrowed to the T14 testcases-watchdog boot, and the body's bullet matches.
  • B2: the body states cargo test -p netstack run at the head and names the names table, not labels, as a TooBig source.
  • S1: the one code change. Failure::TooBig arises only from spawn::Refused::TooBig (failure.rs:241), so std's ArgumentListTooLong beside libc's E2BIG is right for every source. The testcases --build-only at the head compiled it for both architectures.
  • S2–S4: comments and one record word.
  • The A process holds its stdio stream state once, with its descriptor table #1226 record correction is in scope as well: the same false claim, in text this PR's author wrote.

Everything except S1's arm is unchanged code, so run 43156966054 at b2946a6 stays the landing run. #1227 is queued when it is green.

Japabu and others added 7 commits November 12, 2026 07:54
…its word

libc and std each kept a table per source of refusal: the kernel's word,
a file server's, the descriptor table's, a stream's, netstack's, a
spawn's. Several fell through: libc answered EINVAL for a kernel word it
did not name and EIO for half of netstack's, std answered Other for a
device's failure, for netstack's limit and for a lookup that found no
address, getaddrinfo answered -1 for everything, and fread, fwrite and
fseek set no errno at all.

toyos::Failure is the one vocabulary. sdk/toyos/src/failure.rs reads every
word of SyscallError, fs::Error, files::Error, fd::Error, NetError, a
stream's Refused, spawn::Refused, ImageRefused and dl::Refused into it by
name, with no catch-all, so a word added to any of them fails to compile
until it is read. libc keeps one table from Failure to errno
(errcode::of) and std one to io::ErrorKind (sys::failed), both exhaustive.
A call whose POSIX page has another word for a refusal names that refusal
and sends every other through the table (mprotect's and madvise's ENOMEM
for pages not held). getaddrinfo answers POSIX's EAI words from the same
failure.

What a caller sees change:
- libc: a bad address is EFAULT and an unknown kernel word EIO, where both
  were EINVAL; no netstack is ENOTCONN and netstack's limit EAGAIN, where
  both were EIO; a launch that started nothing is EIO, not EACCES;
  fread, fwrite, fseek and ftell set errno.
- std: a device's failure is Uncategorized, as every other platform says
  it, not Other; netstack's limit is QuotaExceeded; a name with no address
  is NotFound, apart from every failed lookup; kill, wait and try_wait
  answer the kernel's word.
- the shell says "not found" for NotFound alone and prints any other
  refusal of a spawn.

The pipe ABI gains three words at netstack's boundary, 10 to 12:
ERR_NETWORK_UNREACHABLE, ERR_HOST_UNREACHABLE and ERR_TRY_AGAIN. netstack
answers a connect an ICMP destination unreachable ended by what its code
names (the network, the port as a refusal, else the host), a prohibited or
time-exceeded one as the host, a lookup with no way out as the network and
one whose lease changed as try again. osbench's lookup retry, the one
caller that waited on the old word, waits on the new ones too.

std's fd_error stays as a one-line name over the table: the rust fork's
BorrowedFd::try_clone_to_owned calls it, and renaming it is a fork commit
this change does not need.

Records: closes libc-answers-einval-for-every-error-it-does-not-name,
libc-loses-the-kernels-word-on-two-write-paths,
std-maps-a-device-error-to-other-not-uncategorized,
std-net-maps-every-other-syscall-error-to-other (its syscall_err was
already gone), std-lookup-host-answers-no-address-as-other,
the-pipe-abi-has-no-word-for-an-unreachable-host-or-a-lookup-to-try-again
and the-shell-says-not-found-for-every-spawn-error.
a-full-kernel-handle-table-answers-enomem-not-emfile stays: the kernel
says one word for a full table and for no memory, and the owner ruled no
kernel change for POSIX alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
… restates a match

spawn::Refused::Invalid carried every refusal a spawn made of itself, and
Failure read all of them as TooBig, whose contract, a spawn carrying more
than one carries, was false for most: an image that does not lay out
answered E2BIG where POSIX's word is ENOEXEC. The variant is split. An
image no program lays out is NotExecutable (ENOEXEC, std InvalidData), a
spawn or a launch past what it carries is TooBig (E2BIG), and the rest stay
Invalid, which is now InvalidArgument (EINVAL). libc's POSIX-name test
holds the three.

Helper::spawn's block is one function, starting_block, which states the
working directory and lays out the block, and the MAX_ARGS test calls it,
so the limit and what a spawn composes cannot drift. Block::new takes the
block's bytes, startup::bytes is public, and startup's Refused is gone. The
two guest tests that build a block call startup::bytes first.

Failure::ALL is gone with the four tests that held what exhaustive matches
already hold: every failure listed once, every kernel word and every file
protocol word a failure, and every failure's errno one errno.h defines.
The pipe-word test, libc's POSIX-name tests and the netdb.h test stay.

MAX_ARGS's comments say the count is ToyOS's: POSIX leaves whether pointers
and terminators count to the implementation.

std keeps the specific text of a stream's own handle moved into a spawn
(Misuse::Moved) and of a stream this process holds no end of
(Misuse::NotThisProcess, through stream_failed for TcpStream's calls).

Records the reviewer's sighting: a connect with no route answers
ERR_NOT_CONNECTED, where POSIX's word is ENETUNREACH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Block::new took any byte slice once it stopped composing the block itself,
so a Block no longer promised a laid-out block of at most MAX_BLOCK bytes,
which Program::starting_with relies on when it reserves from the block's
end up to the program. A public caller could make the SDK's spawn overflow
that subtraction, or hand a child pointers it cannot follow.

startup::bytes now answers Said, whose field is private and which only it
makes, and Block::new takes a Said. starting_block answers one, and the
callers read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The corpus runs only on the T14's testcases boot, and no bench can boot.
The full-table record, whose corpus case is 212_full_table, carries what
main must show there when a bench returns: every member green,
212_full_table's ENOMEM, 206_libc_refusals' _SC_ARG_MAX of 2088939 and the
watchdog boot green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
… and std's word for too big is E2BIG's

#1271 made the testcases shared boot a guest row (testcases_members_exit_0),
which runs every Rust member and the C corpus on QEMU, so the records'
"no guest row runs the corpus" and "the member runs only on metal" were
false. The full-table record now owes metal only the testcases-watchdog
boot, which no guest row runs. The record of the stream state's TCP half
owes stream_state_is_one's run at main to that guest row, since what it
holds is no bench's.

std answers Failure::TooBig ArgumentListTooLong, std's own kind for E2BIG,
which libc answers, rather than InvalidInput.

The MAX_ARGS test's doc says its names table is MAX_NAMES bytes, MAX_NAMES's
doc keeps only its capacity, and the record of a dropped empty argument
names startup::bytes as the writer that skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the sdk-one-error-table branch from c40194e to 31855f8 Compare November 12, 2026 06:03
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Nov 12, 2026
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification at 31855f8, the rebase onto main ee15a2e (#1258). I read the range-diff against c40194e:

The landing run is 43156966054 at b2946a6. The same code plus compile-settled arms passed every row except two testcases members. Those two are main's #1252 defects, independent of this PR (see #1280, now queued, which fixes them and uses this run as its control). Queueing.

@Japabu
Japabu added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 29c5f2d Nov 12, 2026
5 checks passed
@Japabu
Japabu deleted the sdk-one-error-table branch November 12, 2026 06:23
Japabu added a commit that referenced this pull request Nov 12, 2026
…he switch carries FS and GS (#1264)

No conflicts. #1227 removed libc's `refuse_files`: every refusal now goes through
`set_errno`, which reads `toyos::Failure`'s one table. A refused map of a
file answers through it too. A file its server maps no pages of still
answers `ENODEV`, as POSIX names that refusal of `mmap`.

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