Skip to content

Step 9's file half: the kernel keeps no working directory; a spawn states it in the child's PWD - #1182

Merged
Japabu merged 4 commits into
mainfrom
small-kernel-getcwd
Nov 5, 2026
Merged

Japabu merged 4 commits into
mainfrom
small-kernel-getcwd

Conversation

@Japabu

@Japabu Japabu commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Step 9's file half (#1152) left SYS_GETCWD (21). The kernel copied each spawn's starting directory out of SpawnArgs::cwd_ptr/cwd_len, checked that it was absolute and at most MAX_PATH (kernel/pure/cwd.rs), and kept it in ProcessData::cwd for the life of the process. Its only job was to answer the SDK once, and the SDK has owned the working directory since #1152.

Result

The kernel keeps no working directory. The starting directory now travels in data the parent already hands the child: its environment, under POSIX's own name for it, PWD.

  • Parent: toyos::spawn::Helper::spawn is the one helper spawn that std's Command, libc's posix_spawn and the supervisor's launcher all reach. It writes PWD=<dir> in place of any PWD the given environment held (toyos::files::starting_in). It refuses a directory that is not absolute, is longer than MAX_PATH or holds a NUL, which are the checks the kernel used to make, now made at the SDK boundary.
  • Child: the owning SDK copy reads its spawn-time environment (SYS_GET_ENV, which the kernel keeps unchanged) the first time it needs its working directory. It starts there, cleaned of . and .. (starting_directory).
    • A spawn that states no PWD starts its child at the root of its view. That covers the kernel's start of the supervisor and every raw-syscall spawn in the tree, all of which passed / before.
    • A PWD that is not absolute UTF-8 panics the child loudly, as the old assert on the kernel's answer did. Only a raw-syscall parent can write one.
  • Visible difference: a helper's child always sees PWD, even one spawned after Command::env_clear(), which on Linux, macOS and Windows would see no PWD. That is the carrier: the spawn states the child's directory there.
  • Deleted: SYS_GETCWD, syscall::getcwd, SpawnArgs::cwd_ptr/cwd_len, ProcessData::cwd, sys_getcwd, the spawn's cwd copy, and kernel::cwd with its tests.
    • The raw spawns in the guest tests drop their cwd fields.
    • abuse_spawn_argv drops its working-directory refusals; those refusals are now the SDK's, held by host tests.
  • Unchanged: the std fork needs no change, because its ToyOS platform code is sdk/std in this tree (sys/process.rs gets one comment). No fork or ~/.cargo/git/checkouts consumer names the removed items.
  • Issues: the small-kernel track's item 1 is now only the file half's windows, and its record names the environment (ProcessData::env, SYS_GET_ENV) as userland data the kernel still holds, with a startup memory object as the likely exit. The isolation stage 1 record no longer lists the kernel's working directory. The cwd paragraph of the-capability-end-state-is-twelve-answers.md, already stale (spawn_cwd.rs, kernel/src/syscall/fs.rs), now says what is true.

Decision: why the environment

The brief left the carrier open: environment, endowment labels or the startup block.

  • Environment: adds no kernel or ABI part. It deletes the kernel's last path text for a process, and the name is the standard one, so an existing program reading PWD sees the truth.
  • Endowment labels: would need a handle to label and a string convention.
  • Startup block: would keep a cwd field in SpawnArgs that the kernel still copies, and would change every entry point's stack layout.

One tradeoff: the text sits inside the opaque environment bytes the kernel keeps for SYS_GET_ENV. The kernel never interprets those bytes, but they are not literally free of path text. Moving the environment out of the kernel is a separate question.

Size

kernel/src:

revision files lines
before #1152 230 59,012
main 223 56,823
this PR 223 56,800

kernel/pure goes from 25,513 lines to 25,477. That completes the lines half of the file half's measurement.

Checks and evidence

Evidence: https://github.com/ToyOSOrg/evidence/tree/e10645d67f35e7d754073b0f717f913cb5f9cc50/getcwd

The branch was rebuilt after review to reword one commit message. 926544f82 and 13e944b3b carry exactly the trees of the measured baedeb48d and 34c11dea7, so every result below still applies. Head 0e98591f7 changes only one guest-test comment and an issue record on top of them; that guest binary compiles with the harness's toolchain.

  • Host: cargo run -- --ci host at baedeb48d exited 0, 108 steps green. The earlier run on the first cut was red on one clippy lint (slow_vector_initialization in owner.rs), which is fixed in 0b66e1691.
  • New host tests in sdk/toyos/src/files.rs:
    • a_child_starts_where_its_spawn_says_and_nowhere_its_environment_said
    • a_spawn_stating_no_directory_starts_its_child_at_the_root
    • a_directory_no_child_can_read_back_is_never_stated
  • Negative control: mutate-keep-pwd.patch makes starting_in keep an inherited PWD. With it applied, the first test is red (exit 101; the child would read the inherited /elsewhere). After the restore it is green again.
  • Image: cargo run -- --build-only at baedeb48d exited 0.
  • Guest rows: run 41993283834 at 34c11dea7, 9 of 9 green. They cover spawn and the launcher's directory on x86-64, plus aarch64, because the ABI and spawn are shared code:
    • x86-64: console_image_boots (every supervisor row is spawned with a directory), libc_children (posix_spawn), session_lifecycle, a_launch_gives_each_unsent_stdio_slot_a_null, a_row_reaches_its_own_state_and_no_other, a_spawn_from_an_image_frees_it_when_the_child_ends
    • aarch64: virt_user_mode, virt_smp, virt_readonly_copyout
  • Metal (T14), boots of 34c11dea7: these are the only checks that a child starts in a directory other than / end to end, since no filtered QEMU row asserts it (the shared-boot binaries now run only on metal). The orchestrator ran proctreecase and testcases (rc=0 each). Judged offline with --metal-readback ... app_view std_fs at 34c11dea7: exit 0, 3 passed, 0 failed, 2 boot(s).
    • app_view passed: the launched package reported cwd=/home/toy/Games.
    • std_fs and std_fs_write exited 0, including the new check, in which a child spawned with current_dir under an inherited PWD=/system prints its own directory.
    • The echo broken-pipe panic in that boot's log comes from std_fs's existing pipe_is_not_a_directory, which runs before the new check.
    • windowscase was not run: one mask_windows reading is no comparison.

Unresolved

  • The file half's windows comparison remains: the median of at least five interleaved boots per arm, by stage 6 step 2's rule, against the kernel before Consolidate storage around native bcachefs and userland filesystems #1152. One mask_windows boot is not that comparison. This change touches no masked or preemption-off path (it deletes a syscall arm and a field), so I expect no movement, but that is an inference.

🤖 Generated with Claude Code

…'s PWD

Step 9's file half left SYS_GETCWD (21): the kernel copied each spawn's
starting directory out of SpawnArgs, judged it absolute and at most
MAX_PATH (kernel/pure/cwd.rs), kept it in ProcessData for the process's
life and answered it once to the SDK. The working directory has been
the SDK's own state since #1152; the kernel only carried its first value.

That value now travels in data the parent already hands the child, its
environment, under POSIX's own name for it, PWD. The SDK's helper spawn,
which std's Command, libc's posix_spawn and the supervisor's launcher
all go through, writes PWD=<dir> in place of any PWD the environment
held (toyos::files::starting_in) and refuses a directory that is not
absolute, is past MAX_PATH or holds a NUL, as the kernel refused it
before. The child's SDK reads its spawn-time environment once and starts
there (starting_directory); a spawn that states none, such as the
kernel's start of the supervisor, starts its child at the root of its
view, which is where every raw-syscall spawn in the tree put it.

Deleted: SYS_GETCWD, syscall::getcwd, SpawnArgs::cwd_ptr/cwd_len, the
kernel's ProcessData::cwd, sys_getcwd, the spawn's cwd copy and
kernel::cwd. The raw spawns in the guest tests lose their cwd fields,
and abuse_spawn_argv its working-directory refusals, which are now the
SDK's and held by its host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 5, 2026
#1182 removes SYS_GETCWD, so the small-kernel track's first item is the
windows that half owes, beside its measured lines, and the isolation
track's stage 1 no longer lists the kernel's working directory as left.

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

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1182 at 34c11de (base dd05bf5)

Architecture

The design fits. The kernel loses a field, a pure module, a syscall and a SpawnArgs pair whose only job was to answer the SDK once. The starting directory now goes in data the parent already hands its child. One SDK function writes it and one reads it, so neither the kernel nor the ABI gains anything. I checked every spawn route:

  • std Command: goes through Helper::spawn (sdk/std/sys/process.rs:476) with child_cwd, which is absolute.
  • libc posix_spawn: goes through Helper::spawn (sdk/libc/src/spawn.rs:353) with the SDK's getcwd.
  • The supervisor's launcher: copies the caller's env, sets current_dir, and then std reaches the same helper. A caller's own PWD is replaced there.
  • The kernel's supervisor spawn and the raw-syscall tests: state no PWD, so the child starts at /.

SpawnArgs { is now built only in sdk/toyos/src/spawn.rs and in the guest tests.

PWD grants nothing. The text is resolved by the child's own client, against the view it was given (#1181). The .. steps are lexically cleaned (absolute("/", dir)), and the server still resolves under the grant's root. A raw parent's arbitrary PWD therefore names, at worst, a path the child could already name. A malformed PWD (not absolute, not UTF-8, or longer than MAX_PATH) panics the child the first time it uses its working directory. Only a raw-syscall parent can write one, so this matches the old assert and is no silent fallback. POSIX behaviour stays out of the kernel. The SDK reusing POSIX's variable name for its own spawn protocol is userland's choice.

Deletion is complete. Outside issues/, git grep finds no SYS_GETCWD, syscall::getcwd, sys_getcwd, cwd_ptr/cwd_len, kernel::cwd or ProcessData::cwd. The only remaining hit is the stale comment in the blocker below. Neither rust/library nor ~/.cargo/git/checkouts names any of the removed items. The two SYS_GETCWD mentions left in issues/ are an exit criterion and the dated 2026-08-20 audit, which also still lists SYS_CHDIR. Both are correct as history.

Size. The diff is +136/−170. kernel/src has 223 .rs files and 56,800 lines, and kernel/pure has 25,477 lines, both as the PR states. The SDK grows by 38 production lines and 30 test lines, which replace the 35-line kernel module and its guest refusals.

The env bytes the kernel keeps for SYS_GET_ENV. This is acceptable for this step. Storing the environment predates this PR, and the kernel never interprets those bytes; HOME and argv already carry path text the same way. What the approved constraint removes is a working directory as kernel state, and that is gone. Whether the environment should leave the kernel is a separate small-kernel question; see suggestion 1.

Evidence. I checked the following:

  • host3.log is --ci host at baedeb4: exit 0, 108 steps.
  • 34c11de changes only issue markdown, so that run still covers the code.
  • The negative control, mutate-keep-pwd.patch, turns the first host test red: it reads /elsewhere and exits 101. The test is green again after the restore.
  • Guest run 41993283834 ran at headSha 34c11de and succeeded.

That run never starts a child anywhere but /. The non-root start is held end to end only by app_view (proctreecase) and the new std_fs check (testcases). Both run only as shared metal members (discover_rust_tests), and neither has run yet. The PR states this gap. Those two readings must be green before this lands. The plumbing between the tested functions (env_ptr taken from starting_in's result, and owner::cwd reading get_env) is straightforward, and I verified it by inspection.

Findings

BLOCKER

  1. tests/guest/rust-tests/src/bin/query_modules_size.rs:9 — The comment says "The contract now is sys_getcwd's and sys_readdir's". This PR deletes sys_getcwd, and sys_readdir was already gone. The comment states a current contract by pointing at two functions that no longer exist. Delete the sentence, or state the contract directly, which line 10 already does.

SUGGESTION

  1. issues/the-kernel-is-small-interrupts-post-and-threads-wait.md — Consider adding one line that names ProcessData::env and SYS_GET_ENV as kernel-held userland data that a startup memory object could carry instead. Then a later reader will not take "the kernel keeps no working directory" to mean that the kernel holds no path text. This is optional: it is not a compromise this PR introduces.
  2. sdk/toyos/src/files.rs:497 (starting_in) — A child of Command::env_clear() now sees PWD, unlike on Linux, macOS or Windows. It is a natural consequence of the chosen carrier and probably harmless. A sentence in the PR body would make the visible difference explicit.
  3. baedeb4's commit message calls the std_fs addition "The one QEMU check". It runs only on metal (a shared member), as the PR body correctly says. Since you own the branch, consider rewording the commit message before it lands.

Verdict

SEND BACK, for one blocker: a one-line stale comment. With that fixed and the pending metal readings for app_view and std_fs green, I see nothing else in the way.

Japabu and others added 3 commits November 5, 2026 09:40
A check that a child std spawns with current_dir starts there, through
the SDK's PWD and the child's SDK reading it: the binary runs itself in
a fresh /tmp directory under an inherited PWD naming another, and the
child prints its working directory. std_fs is a shared member, so this
runs on metal (testcases), not on a filtered QEMU run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1182 removes SYS_GETCWD, so the small-kernel track's first item is the
windows that half owes, beside its measured lines, and the isolation
track's stage 1 no longer lists the kernel's working directory as left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s kernel-held

query_modules_size's header pointed its contract at sys_getcwd and
sys_readdir, neither of which exists now; the next line states the
contract itself. The small-kernel record names the environment
(ProcessData::env, SYS_GET_ENV) as userland data the kernel still holds,
with a startup memory object as its likely exit, so "the kernel keeps no
working directory" is not read as "the kernel holds no path text".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the small-kernel-getcwd branch from 34c11de to 0e98591 Compare November 5, 2026 08:40
@Japabu

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator verification of the r1 fix (34c11de..0e98591), which changes no code:

@Japabu
Japabu marked this pull request as ready for review November 5, 2026 08:40
@Japabu
Japabu added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 15c26bf Nov 5, 2026
10 checks passed
@Japabu
Japabu deleted the small-kernel-getcwd branch November 5, 2026 09:05
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