Skip to content

A process holds its endowment state once, in the copy that owns its descriptor table - #1282

Draft
Japabu wants to merge 1 commit into
mainfrom
endowment-state-is-one-per-process
Draft

Japabu wants to merge 1 commit into
mainfrom
endowment-state-is-one-per-process

Conversation

@Japabu

@Japabu Japabu commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

std links its own copy of toyos, and a program that uses the SDK links another. Each copy kept its own record of what the process had done with its endowments: which names were taken, its launcher and the services it adopted.

  • A program's toyos::endow never saw what std adopted. adopted_services_stay's adopter listed nothing after std had adopted fs:/system. That was the red A sent handle of another kind is InvalidArgument whatever its rights, and adopted_services_stay checks only its own copy of toyos #1280 found and recorded in issues/a-programs-own-toyos-never-sees-the-services-std-adopted.md, which this PR closes.
  • The latent case: two owners of one handle. Nothing kept two copies from taking one label. Each copy's take-once state was its own, so the second copy to take a label got the same handle again, a second owner whose drop is a second close. The launcher was such a label: each copy that launches took launcher for itself (toyos::endow::launcher), so a program that launched through both std and its own copy would have held it twice. No test showed it, and nothing in the tree is known to have done it. It is recorded here, not as an issue, because this PR removes it.

Resulting behaviour

The process's endowment state now lives once, in the copy that owns the descriptor table (std's in a Rust program, libc's in a C one), as #1226 made the stdio state single.

  • That copy defines toyos_endow_take, toyos_endow_holds, toyos_endow_launcher, toyos_endow_adopt and toyos_endow_adopted, and every copy, that one included, reaches the state through them. Only plain values cross: a label's bytes, a handle's number, and #[repr(C)] entries of a name and a connector.
  • Each copy still reads the names itself, since they are the startup block's and the same in every copy.
  • A name answers once in the process. A second take, from any copy, is refused. The owner resolves a label against its own reading of the table, not an index the caller computed.
  • The launcher is taken once, kept for the process's life and lent to every copy, which wraps it and never closes it.
  • The adopted set is one set. What std adopts is what a program's toyos::endow::services and connector see, and a lookup through any copy ends the adoption. toyos::endow::adopt's and std's adopt_services' docs say so.
  • Off ToyOS, a copy holds a state of its own, as the owner holds the process's. A host build only links it: host binaries such as the file server's link endowment code that never runs there, since reading the startup block reads a guest address. A first form declared toyos_endow_* on the host as crate::fd declares toyos_fd_*, and the file server's host build failed to link it (r15/ below).

The module header of toyos::endow now states the rule, which is the closed record's durable line. The manual does not describe the SDK's copies, so it is unchanged.

Checks and evidence

Evidence branch pr-1282, at 480a96b61. Each log ends with its exit code; local paths are masked. The head is b9c8c16, on main 29c5f2d. It differs from bd5851c, where the guest run below was dispatched, only in sdk/toyos/src/endow.rs, by review r1's fixes, which change nothing a ToyOS copy does.

Host tests hold the state's rules on a State and a table directly (cargo test -p toyos, r1-fix/):

  • a_name_is_the_handle_endowed_at_its_place_and_is_taken_once;
  • a_service_stays_in_the_table;
  • the_launcher_is_taken_once_and_answered_again: a take of it is refused, and every later ask answers it;
  • the_adopted_set_is_made_once_and_ended_by_a_lookup.

The cross-copy property rests on two things. One is the arms' selection by target, by inspection: on ToyOS outside tests, held is the extern arm, so a ToyOS copy cannot hold a local state, and a missing owner fails to link. The other is adopted_services_stay in the guest row below, whose check was red at main 302fb36 (run 43156966054) without this change. A first form's host "two-copy" tests and their control exercised a test fixture, not the copies; review r1 found that, and they are gone.

Review r1's fixes, at b9c8c16 (r1-fix/): cargo test -p toyos, clippy on toyos (all targets and fd-table), --build-only and the file server's host suite all exit 0. They also put the state's predicate in one module, bound Once on Send + Sync with AdoptedSet's Sync stated, keep the launcher as Option<RawHandle>, and drop the public docs' links to the crate-private state.

Host checks (r16/) at bd5851c, a clean tree, which review r1's fixes change only in endow.rs:

  • --build-only;
  • the host suites of toyos-abi, toyos, the kernel library and the crates that link the SDK on the host (the libraries, shell-copies, toybox-copies, disks, diskserver, fileserver, netstack, nvme, pkg, session, soundserver and sshserver);
  • clippy on toyos (all targets and fd-table), the libraries and toyos-abi, and the kernel's shapes.

All exit 0. The first form's run (r15/) passed all but the file server's host tests, which failed to link toyos_endow_*; that is what made a host build hold a local state.

Guest rows, one run at bd5851c, the landing run: 43170266393, pending. The branch endow-hold-bd5851cf0 keeps that commit fetchable until it runs.

They run on x86-64 QEMU, each one of MACHINE_TESTS. No metal is staged or requested; the benches are down.

Risks

  • A copy that never links an owner would fail to link. Every ToyOS program links std or libc, which own the descriptor table; the tree's no_std crates are libraries.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1282 at bd5851c

Commit bd5851c on main 29c5f2d. I read the commit, the whole diff, all of sdk/toyos/src/endow.rs, #1226's fd.rs and log/stdio.rs owner/target/host arms, startup::endowed, files::owner, std's adopt_services, the supervisor's path up to adopt_services, and every caller of adopt, services, connector, service, launcher and holds. I also read evidence 5a0bd9833 (host/, r15/test-fileserver.log, r16/). Guest run 43170266393 is still queued, with headSha bd5851c confirmed. It is yours to judge.

Architecture

The change follows #1226's pattern and the approved mechanism:

  • Owner and access path. The fd-table copy holds one State and defines toyos_endow_{take,holds,launcher,adopt,adopted}. In every ToyOS copy held is the extern arm (cfg(all(target_os = "toyos", not(test)))), so a ToyOS copy cannot hold a local state, and a missing owner fails to link.
  • Label resolution. The owner resolves a label against its own parse of the startup block. Every ToyOS program links std or libc. loader is the only no_main binary, it is UEFI and links no toyos, and no system/ or tests/guest/ program is no_std. A no_std library links the program's copy, which reaches the owner like any other copy. libc's copy in a Rust program does the same.
  • ABI between copies. Only plain values cross. Adopted names are copied into the owner's never-dropped _kept. Connectors pass by number, and the caller gives them up with into_raw only after a true. On false, Err(services) returns them to the caller, and std closes them as its doc says. I found no double close or leak. The empty-set dangling pointer is handled at count == 0.
  • Concurrency. Take is an AcqRel swap per slot. Adopt and the sealing lookup race through one Once, and only the builder sets made. The launcher is a Once over take, so a direct take of launcher that wins makes launcher() None in every copy. Nobody takes launcher directly (git grep). No Once closure re-enters another.
  • Sealing. "A lookup through any copy ends the adoption" leaves the supervisor intact. Nothing before its adopt_services (say_last_words_on_panic, Log::open, Boot::take, boot::read, say!, mint) calls services or connector. files::owner::directories is in the owner copy only, as before. Every guest boot runs this path, so the pending run covers it.
  • Host static. State is Sync through Once<T: Send> and the AdoptedSet: Send impl. It is sound because the set never changes after it is built. It hides no missing owner on ToyOS, because the arm is selected by target.
  • Issue close. The record's exit is met. Its rule is in the module header, and no citation of its slug or path remains (git grep). The manual describes no SDK copies, so it needs no change.

Size: endow.rs +379/−50 (tests about +50; production about +280, mostly the five entries in three arms plus State/AdoptedSet/Adopted). The removed issue file is −37. The production growth is the cost of the approved per-operation ABI. The test-only plumbing in B1 is not.

Findings

B1 sdk/toyos/src/endow.rs:193-196, 207-214, 601-614, 637-652, 771-800 — the two "two-copy" tests and their negative control exercise a test fixture, not the per-copy mechanism. The commit message claims more than they show.

  • What the fixture is. "Two copies" on the host are two Endowments sharing a &'static State through a #[cfg(test)] field. The production property is "every ToyOS copy reaches the owner's one State". It lives in the held extern arm and owner, and cfg(test) compiles neither.
  • The control. host/mut-per-copy.patch changes one line, inside another_copy, a test helper. Going red shows that the fixture shares a state. A regression that gave ToyOS copies a local state again, such as a widened host-arm cfg, would leave both tests green.
  • The first test is redundant. a_name_answers_once_whichever_copy_takes_it is a_name_is_the_handle_endowed_at_its_place_and_is_taken_once with a second reference added.
  • The fixture shapes production code. It adds a test-only field to a public type, a per-table leak in of, a cfg(test)/cfg(not(test)) split inside the host held (which also gives take/holds/launcher and adopt/adopted different states under test), and a table parameter that every ToyOS-arm held function receives and discards (_: &Endowments).
  • Consequence. The record says host tests and a control hold "a name answers once whichever copy takes it" and "every copy is lent the one launcher". They do not. What holds those properties is the cfg selection (by inspection) and, for the adopted set, adopted_services_stay with main as its control.
  • Required.
    • Test State's rules directly on a leaked State and a test table: take once, the launcher kept and refused to a take but answered again, adopt once and sealed by a lookup. The third test already works this way.
    • Delete the #[cfg(test)] state field, another_copy, the state() switch and the discarded parameters. Then held takes what the externs take, and the host arm calls STATE.take(Endowments::get(), label) as owner does.
    • Correct the commit message's evidence lines. The cross-copy property is held by inspection of the arms and by the guest test, not by a host control.
    • Cheapest checks after the fix: cargo test -p toyos, the two SDK clippy shapes and --build-only. Behaviour on ToyOS is unchanged, so guest run 43170266393 stays valid if it is green.

S1 sdk/toyos/src/endow.rs:31, 35, 238, 461, 473, 531, 540 — cfg(any(test, feature = "fd-table", not(target_os = "toyos"))) appears seven times. Putting State, AdoptedSet, their impls and index_of's use into one cfg'd module would give the predicate one home.

S2 sdk/toyos/src/endow.rs:538-541 — the comment justifies unsafe impl Send for AdoptedSet by "reached only by shared reference", which is an argument for Sync. It compiles because Once<T: Send> is Sync (line 403, older than this PR), a weaker bound than OnceLock's Send + Sync. This is the first !Sync value placed in a Once. Implementing Sync with that comment, and bounding Once on Send + Sync, would state the real requirement.

S3 sdk/toyos/src/endow.rs:188, 286, 308 — the public docs of Endowments, launcher and adopt link [State]. That type is crate-private and is compiled out of a program's own ToyOS copy, so a reader gets a private or broken link that adds nothing. Removing the parentheticals loses nothing. Two smaller points nearby:

  • launcher: Once<u32> with a HANDLE_INVALID sentinel (467-469, 495-496) could be Once<Option<RawHandle>>, leaving the sentinel at the extern boundary only.
  • OwnedHandle::from(connector).into_raw() (319) is connector.into_raw().

Evidence and PR body (orchestrator)

  • Host checks. r16 is at bd5851c on a clean tree, and every listed command exits 0 (the logs agree). r15's test-fileserver shows the arm64 host link failure on _toyos_endow_take/_toyos_endow_adopted, which justifies the host arm. The ToyOS held arm is compiled by --build-only and linted by no shape, as stdio's target arm is not either. That gap predates this PR.
  • "Negative control … turns both two-copy tests red". True of the patch, but the patch edits only the test helper (B1). The body should not present it as a control for the per-copy defect.
  • "That check is red on main (run 43156966054)". The check was red at main 302fb36. A sent handle of another kind is InvalidArgument whatever its rights, and adopted_services_stay checks only its own copy of toyos #1280 removed it, so it does not exist on main 29c5f2d. endow.rs is identical between the two, and std's fs.rs differs only in doc lines, so that run still serves as the control for the restored line. The wording should name the revision.
  • Guest rows. Pending. testcases_members_exit_0 carries the one real cross-copy check, through adopted_services_stay.

SEND BACK

…escriptor table

std links its own copy of `toyos`, and a program that uses the SDK links
another. Each copy kept its own record of what the process had done with
its endowments: which names were taken, its launcher and the services it
adopted.
- A program's `toyos::endow` never saw what std adopted:
  `adopted_services_stay`'s adopter listed nothing after std had adopted
  `fs:/system`.
- Nothing kept two copies from taking one label, which then had two
  owners. The launcher was such a label: each copy that launches took it
  for itself, so a program that launched through both std and its own copy
  would have held it twice.

That state now lives once, in the copy that owns the descriptor table
(std's in a Rust program, libc's in a C one), as #1226 made the stdio state
single. That copy defines `toyos_endow_take`, `_holds`, `_launcher`,
`_adopt` and `_adopted`, and every copy reaches the state through them.
Each copy still reads the names itself, since they are the startup block's.
- A name answers once in the process: a second take, from any copy, is
  refused.
- The launcher is taken once, kept for the process's life and lent to every
  copy.
- The adopted set is one set. What std adopts is what a program's
  `toyos::endow::services` and `connector` see, and a lookup through any
  copy ends the adoption.

Off ToyOS a copy holds a state of its own, as the owner holds the
process's. A host build only links it: a host binary such as the file
server's links endowment code that never runs there, since reading the
startup block reads a guest address.

Host tests hold the state's rules: a name is taken once, the launcher is
kept and refused to a take, and the adopted set is made once and ended by a
lookup. That every ToyOS copy reaches the owner's one state rests on the
arms' selection by target, by inspection, and on `adopted_services_stay`.
Its adopter again checks that its own copy lists the `/system` std
adopted. That check was red at main 302fb36 (run 43156966054), where
std's adoption filled std's copy alone. The adopter lists only after
adopting, since a listing ends the adoption.

`issues/a-programs-own-toyos-never-sees-the-services-std-adopted.md` is
closed. Its rule now heads `toyos::endow`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the endowment-state-is-one-per-process branch from bd5851c to b9c8c16 Compare November 12, 2026 07:16

This branch has not been deployed

No deployments
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