Skip to content

Step 8's exit: views hold only what a row gives, each app's /tmp its own - #1181

Merged
Japabu merged 4 commits into
mainfrom
step8-empty-views
Nov 5, 2026
Merged

Japabu merged 4 commits into
mainfrom
step8-empty-views

Conversation

@Japabu

@Japabu Japabu commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Draft. Step 8's exit, the small-kernel track's stage 4 and the isolation track's stage 1. Evidence: https://github.com/ToyOSOrg/evidence/tree/b2e7fc5ad3193b5078866adfa6bca9e7cd0f836f/step8-r1

What main already had

After #1152 (e11b6a784) the kernel serves no path. SYS_SPAWN refuses a spawn without an image, /tmp is the MEMORY file server's, /system is SYSTEM's, and libc and std both resolve through toyos::files. Two items of step 9's file half remain: SYS_GETCWD, a spawn's starting directory kept as text and checked only for syntax, and the half's measurement. The record's "the kernel serves /tmp to every process" was stale.

What still stood against the empty view was toyos_manifest's temporary, the machine's whole /tmp added to every package's view. The other open part of the exit was the escape suite's rename race.

What changes

  • A package's /tmp is its own. Its MEMORY grant is rooted at toyos_manifest::app_temporary (/tmp/apps/<name> of the machine's), which the supervisor makes with the app's home before the app runs. This follows the owner's "Per session" ruling of 2026-10-03: each app has its own /tmp. A view is now only what its row gives. A declared row still sees the whole tree but /state, which is stage 2's to narrow.
  • The resolver owns the root check. fileserver::resolve refuses a start outside the grant's root by component. Before, main.rs checked it beside the resolver and the resolver expected it. New tests cover the rename races: a held directory renamed out of the root, renamed to a name that only shares the root's spelling, or left outside after the root itself was renamed, names nothing. One renamed inside the root resolves where it now is.
  • Routing is pure. toyos::files' routing is a function of the view, so "a process given an empty view names nothing" is the host test an_empty_view_names_nothing_but_an_empty_root.
  • app_view checks /tmp. The app's /tmp write lands in /tmp/apps/appview. Its /tmp lists only that write. Neither the machine's /tmp file nor another app's is reachable by path.
  • The records of both tracks are updated.

Kernel size: unchanged. kernel/src has 223 .rs files and 56,823 lines, before and after. No kernel path was superseded here, because #1152 had already deleted them.

Checks

  • cargo run -- --ci host at 2d86471a9: exit 0, 108 steps green.
  • Negative controls, each a checked patch applied, built, run and reverted in one script at 2d86471a9:
    • Global-tree resolver (grant root ignored): 6 of 13 resolve tests red, namely every escape test, both halves of the suite. Exit 101.
    • Start checked by string prefix: a_held_directory_renamed_out_of_the_root_names_nothing red. Exit 101.
    • Unheld path routed as a directory: both new routing tests red. Exit 101.
  • QEMU guest rows at 2d86471a9 (run 41981594527): a_launch_gives_each_unsent_stdio_slot_a_null and the_data_partition_beside_the_stick_is_designated both PASS. std_fs, hierarchy_paths and state_view are members of the shared testcases boot, not QEMU rows. An earlier dispatch that named them booted nothing (run 41977385238). No aarch64 rows: no kernel, loader or architecture code changed.
  • 914ef657e and 1d470454a change only the issue records, so every check above still applies.
  • Review r1 (68bc9c0bc): app_temporary's comment now claims only that no other package's view names an app's /tmp. APP_TEMPORARIES replaces the supervisor's path split and its expect. beneath and data_dir take &str. cargo run -- --ci host at 68bc9c0bc: exit 0, 108 steps green (https://github.com/ToyOSOrg/evidence/tree/dcc78cff0f2ff7d0ea65cdd1be1254a552f5d77a/step8-r3). The guest and T14 results above still apply: the change does not alter which directories the supervisor makes or what any view holds, which the unchanged manifest view tests pin.
  • Metal, T14, images built at 2d86471a9 and run by the orchestrator: both boots passed. Judged by re-running the staging command with the readbacks in place: exit 0, "5 passed, 0 failed, 2 boot(s)". That is app_view on proctreecase, and std_fs, std_fs_write, hierarchy_paths and state_view on testcases. app_view logged "the app's /tmp write is in /tmp/apps/appview" and "its /tmp lists only what it wrote". Evidence: https://github.com/ToyOSOrg/evidence/tree/f03b51b3ff3fb1141991e7f8610991bc0263537d/step8-r2

Unresolved

  • Not this PR, and recorded as remaining work: SYS_GETCWD is a later small-kernel step (item 1 of that track's list). Isolation stage 1 already decides that a spawn names a directory the child is given, not text the kernel keeps; what is open is only how the child learns that directory's name. A declared row's own view, which still holds the whole tree but /state, is the isolation track's stage 2. Both records name both items.
  • An app's /tmp lives inside the machine's /tmp, so a login session's programs can read it, as they can read the app's home. A session's own /tmp remains stage 3's.
  • An app's /tmp is not emptied when the app ends. The machine's /tmp is not emptied either.

🤖 Generated with Claude Code

Japabu and others added 3 commits November 5, 2026 06:57
Step 8's exit (the small-kernel track's stage 4; the isolation track's
stage 1). On main, after #1152, the kernel serves no path: `/tmp` and
`/system` are file-server grants, and std and libc both resolve through
`toyos::files`. What still stood between a view and the exit was
`toyos_manifest`'s `temporary`, the machine's whole `/tmp` added to every
package's view, and the escape suite's rename race.

- A package's `/tmp` is its own: the MEMORY grant rooted at
  `toyos_manifest::app_temporary` (`/tmp/apps/<name>` of the machine's),
  made by the supervisor with the app's home before it runs. This is the
  owner's "Per session" ruling of 2026-10-03, under which each app has its
  own `/tmp`. A view is now only what its row is given.
- fileserver's resolver refuses a start outside the grant's root itself,
  by component, where `main.rs` checked it beside the resolver and the
  resolver `expect`ed it. The escape suite gains its rename races: a held
  directory renamed out of the root, by a name that only shares the root's
  spelling, or with the root itself renamed away, names nothing; one
  renamed inside the root resolves where it now is.
- `toyos::files`' routing is a pure function of the view, so the exit's
  "a process given an empty view names nothing" is a host test.
- `app_view` asks the app's `/tmp` is its own.

The kernel is unchanged: 223 `.rs` files, 56,823 lines in `kernel/src`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A declared row's own view is the isolation track's stage 2, and
SYS_GETCWD is a later step of the small-kernel track.

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

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1181 at 914ef65 (base e11b6a7)

Architecture

The design fits the approved track. The owner's "Per session" ruling of 2026-10-03 (issues/every-program-sees-only-the-files-it-was-given.md:54) does say that each app has its own /tmp. A grant rooted at apps/<name> on MEMORY implements that with the grant machinery DATA already uses, so it needs no new mechanism. Moving the start-in-root check from main.rs into resolve is a real simplification. It removes an expect that depended on a check made somewhere else, and it makes the held-directory rename case testable on the host. Turning route into a pure function moves code without changing behaviour, and the change brings a test that the old code shape could not have. No kernel code changes. Size: about +60 production lines (manifest beneath/app_temporary, namespace::route, the resolver check, make_app_dirs) less about 15 deleted, plus about 130 lines of tests.

Escaping a view. I traced the following:

  • .. goes through done.pop(), which refuses at the root.
  • Relative links are expanded against the components already resolved, so the same floor applies to them.
  • Absolute links go back to the client's own table.
  • Lookups inside one request cannot race, because the server runs one thread.
  • Held directories. each_dir updates every connection's descriptions on a rename, including exported ones. The resolver then refuses any start that is not equal to the root or beneath it as a whole component. That covers held directories moved out, a sibling that only shares the root's spelling (home/toy2), and the root renamed under a held directory.
  • Held files used as at get NotADirectory.
  • Planted symlinks. If a session plants a symlink at /tmp/apps/<name> after the supervisor's is_dir check, the app still cannot escape. MemoryVolume::node never follows a link in a path component (it returns NotADirectory), and the resolver never looks up the root itself.

I found no escape.

The negative controls in step8-r1/controls/ match their patches and the claimed reds (6/13, 1/13 and 2/4, exit 101, each restored). The prefix-start control is the one that exercises the component comparison (home/toy2).

Step 8's exit. The exit is "an escape suite red on a global-tree resolver and green on fileserver's; a process given an empty view names nothing". This PR meets it as the track defines it. The two remaining items are recorded with owner and exit:

  • SYS_GETCWD is item 1 of "What is left" in the small-kernel track (owner: the orchestrator). Its exit is step 9's "sdk/abi has no ... SYS_GETCWD (21)".
  • Declared rows' per-row views are isolation stage 2. Its exit is "the machine boots with every program in a declared view, and nothing still sees the global tree". Stage 2's text records that declared rows still see the whole tree.

The issue edits are accurate.

Evidence gap (not a blocker here, by the review's scope). No run so far exercises the change end to end: supervisor make_app_dirs, then a MEMORY grant rooted at apps/<name>, then an app writing /tmp. The two QEMU rows in run 41981594527 launch declared rows (toybox, the data designation), not an /apps package, so they never reach make_app_dirs or the new grant. Only app_view on proctreecase covers it. Landing should wait for that metal result.

Findings

BLOCKER

  1. lib/manifest/src/lib.rs:97-98: the doc comment on app_temporary is false at a security boundary. It says the directory is one that "no other program's view names at all". Every declared row's view holds the whole MEMORY tree (whole_tree, and Program::view's non-package arm), so the shell, the terminal, every service and the supervisor all name it as /tmp/apps/<name>. The PR body's own Unresolved section says a login session can read it. A reader would take that sentence as an isolation guarantee the code does not give. Fix by deleting the clause or stating the truth, for example: "An installed package's own /tmp, a directory of the machine's: its view names it /tmp; no other package's view names it."

SUGGESTION

  1. system/supervisor/src/main.rs:1513: temporary.rsplit_once('/').expect("an app's /tmp is beneath /tmp") gets /tmp/apps back by splitting the string app_temporary produced. A toyos_manifest constant for /tmp/apps, used by app_temporary and by the supervisor, would state the parent once and remove the expect.
  2. lib/manifest/src/lib.rs data_dir: let path = dir.clone(); beneath("data", &path, dir, write) exists only to satisfy the borrow checker. beneath could take dir: &str and allocate it once, or data_dir could call beneath("data", &dir.clone(), dir, write). This is minor.
  3. lib/manifest/src/lib.rs:426: "no other package or app's /tmp" says the same thing twice, since a package is the app. "no other package's /tmp" is enough.
  4. A note for the orchestrator on the PR body. Its Unresolved section calls SYS_GETCWD's removal an open ABI choice of "where the text travels (environment, endowment labels or the startup block)". The approved stage 1 design already settles that: "SYS_SPAWN's working directory becomes one of the client's directories, and a spawn names a directory the child is given, never a path judged at spawn time" (issues/every-program-sees-only-the-files-it-was-given.md, stage 1). The record this PR writes ("a spawn still states its child's working directory as text the kernel keeps") agrees with that design, but the PR body presents it as undecided. The design question still open is how the child learns the name of the directory it was given, not whether the directory travels as text.

Verdict

SEND BACK, for the one false doc comment. Once it is fixed, landing also depends on the pending proctreecase metal run of app_view.

…ORARIES

`app_temporary`'s comment claimed no other program's view names an app's
`/tmp`. A declared row holds the whole MEMORY tree, so the claim holds of
other packages alone, and the comment now says so.

`APP_TEMPORARIES` names the directory every app's `/tmp` is made in, so the
supervisor makes it without splitting a path and `expect`ing the split.
`beneath` and `data_dir` take `&str`. `Program::view`'s comment no longer
says package and app for one thing.

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

Japabu commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1181: 1d470454a..68bc9c0bc (head 68bc9c0)

Assessment

This round changes one commit and two files, with 15 lines added and 14 removed. It adds no new mechanism. The manifest now states the parent of every app's /tmp once, as APP_TEMPORARIES, and the supervisor no longer works it out from a string.

Previous blocker resolved. At lib/manifest/src/lib.rs:100-101, the comment on app_temporary now claims only that "no other package's view names" the directory. That is true. A package's view (Program::view, Some arm) holds:

  • system();
  • its own MEMORY grant rooted at app_temporary(name);
  • its package directory;
  • its home folder;
  • its granted folders, each a data_dir on DATA.

None of those can name another package's apps/<other> on MEMORY. A declared row still sees the whole tree, and the PR body's Unresolved section already records that.

Behaviour is unchanged.

  • app_temporary produces the same string as before (format!("{APP_TEMPORARIES}/{name}") with APP_TEMPORARIES = "/tmp/apps").
  • The supervisor's old rsplit_once('/').0 on /tmp/apps/<name> returned /tmp/apps, because a package name contains no /. The files closure therefore still makes [/tmp/apps, /tmp/apps/<name>], in the same order and after the home folders.
  • The &str change to beneath/data_dir only moves where the allocation happens. Every caller passes the same text.
  • The manifest view tests at lines 1003 and 1028 pin the MEMORY grant ("memory", "/tmp", "apps/gbae", true), and the diff does not touch them. Host CI at 68bc9c0 is green (evidence step8-r3).

Reusing the 2d86471 guest and metal evidence is sound. Since that revision, the only code changes are this round's, and they are byte-for-byte the same in what the supervisor creates and what any view grants. Neither changed file contains anything those boots could observe differently. The QEMU rows and the T14 boots (app_view on proctreecase and the four testcases members) therefore still establish what they did. No rerun is needed.

PR body. The Review r1 bullet describes this round accurately. The earlier note to the orchestrator about SYS_GETCWD has been reflected in the Unresolved section. I found nothing false.

Findings

None.

Verdict

LAND

@Japabu
Japabu marked this pull request as ready for review November 5, 2026 07:06
@Japabu
Japabu added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit dd05bf5 Nov 5, 2026
10 checks passed
@Japabu
Japabu deleted the step8-empty-views branch November 5, 2026 07:28
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