Repository navigation
A process holds its stdio stream state once, with its descriptor table - #1226
Conversation
c981194 to
ab77bd2
Compare
std, a program's own copy of toyos and libc each linked their own copy of log::stdio's statics: what descriptors 1 and 2 were found to name, the ring's mapping, the line each stream is assembling and the pid. So a line said in pieces through two runtimes was two records, a ring was mapped and its handle duplicated once per copy and stream, and a redirect was seen only by the copy asked to make it: libc's dup2 forgot libc's answer, and std's dup2 forgot nobody's. The state now lives in the copy that owns the descriptor table (the fd-table feature: std's in a Rust program, libc's in a C one), which defines toyos_stdio_* beside toyos_fd_*; every copy reaches it through those. The table's owner calls stdio's changed() after each install, dup, dup2, replace and close, so whichever copy asked for a redirect, the next write asks the descriptor again. forget() is gone from the public interface, and with it libc's and the supervisor's calls. A ring is mapped through the descriptor's own handle for the length of the ask, with no duplicate kept: a mapping outlives its handle. The line's hold no longer waits. Held::with answers None where another write holds the line, and that write's bytes go out as records of their own, so a real-time thread never spins on a preempted holder and the message of a panic raised under the hold is written rather than waited for. The hold is let go on unwind. The ask of a descriptor is serialised by a futex lock, never held across a panic, and the asking thread's own write meanwhile goes to the descriptor. fd.rs is touched in its owner module only: the five toyos_fd_* definitions that change what a descriptor names each call changed() outside the table's lock, and the module header says so. Records: closes a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in- another and a-stream-write-spins-on-a-lock-its-own-panic-can-hold. The stream-state record keeps its TCP half under a slug that says so. The two-mappings record stays: one process still maps its ring once per stream, and telling that two handles name one object is a kernel answer or a change to every launcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner marked "no thread is asking a descriptor" with 0, and a process's main thread is Tid(0). Every main-thread write found itself the asker and went to its descriptor as bytes, never to the ring, and a ring's handle takes no byte write. On every guest row of the previous head the system fileserver ended with code 101 at boot without ever asking getpid, which a found ring does, and the supervisor ended waiting for os-release. The mark is now Tid::MAX, the one tid the kernel never issues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r does The table's owner calls stdio's changed() from every build with fd-table, host builds included, while the stream state was compiled for ToyOS alone, so `cargo clippy -p toyos --lib --features fd-table`, the host lint shape for the table's owner, did not compile. The stream state is now gated like the table, on fd-table, and that shape lints it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them does A Rust program links libc's archive only where it asks for it, and this member's extern block named none, so `write` and `dup2` were undefined at link and the testcases image did not build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7561f88 to
b149ff2
Compare
…l staging; masked
Review r1 of #1226 at b149ff2 (on main 45935f4)ArchitectureThe stream state now lives in the copy that owns the descriptor table, and every copy reaches it through Lock order against #1234's settle path is sound.
The stdio state stays correct across dup2, close and spawn.
The FindingsBLOCKER: sdk/toyos/src/log/stdio.rs:499-505.
Tests and evidence
Host evidence is sufficient for Round-1 guest runs are verified. Run 43036786936 at c981194 failed, run 43049938522 at ab77bd2 succeeded, and together they form a real negative control for the tid fix. LAND-pending conditions after the fix:
Line counts. Production grows about +145 lines (stdio.rs about +140, fd.rs +15, libc −11, supervisor −1). Tests grow +24 host lines and +105 guest lines. Records go −100/+39. On the open recordLeaving SUGGESTION: issues/a-programs-two-streams-map-its-one-ring-twice.md. Two improvements to the record:
SuggestionsSUGGESTION: sdk/toyos/src/fd.rs:580-589. Coordination note: #1227 deletes SEND BACK |
changed() took ASKING to forget a stream's answer and finish its partial line into the old ring, but did not name itself the asker. A panic raised there, as push's clock stamp raises on a clock page that carries none, wrote its message to stderr, whose word changed() had just made UNASKED, and so waited on the lock its own thread held: the hang the closed record named. The earlier commit's "never held across a panic" was false for changed(). One guard, Asking, now takes ASKING and names its thread the asker until it drops, and both answer() and changed() hold the lock only through it. A write by the asker to an unasked stream goes to the descriptor, so the panic's message goes to the descriptor the change just made. dup2 of a descriptor onto itself changes nothing, so it no longer makes the stream ask again (finishing a partial line as unended and mapping the ring once more). The open record of a program's two mappings of its ring names the chosen exit: every launcher hands the ring once with the second slot its alias, userland only. It also records that each redirect onto a ring maps it again, 2 MiB kept for the process's life. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to review r1 (head 49bcdc3)
Checks at 49bcdc3: |
Review r2 of #1226 at 49bcdc3 (on main 45935f4)Scope: r1's findings and the changed code, ArchitectureUnchanged from r1. The fix adds no mechanism: marking the asker moves into the lock's guard. r1 findingsBLOCKER,
SUGGESTION, SUGGESTION, the open record: taken.
New findingsNone. Evidence
LAND-pending:
LAND |
stream_state_is_one runs only on the T14's testcases boot, and no bench can boot. The record of the stream state's TCP half, which names the stream state as one per process, carries what main must show there when a bench returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator: landing under the benches-down rule. Review r2 LAND; head 1c3788d is 49bcdc3 plus one record (verified). Guest run 43080395535 at 49bcdc3: success. Owed on metal recorded (T14 |
…toyos Correction to the previous commit's message: the adopter's early listing did not refuse its adoption. std links its own copy of `toyos`, and each copy keeps its own adopted services. std's `adopt_services` filled std's copy; the adopter's `toyos::endow::services()` read its own, which adopted nothing, so the check that it held `fs:/system` after adopting went red (run 43156966054, `assert_failed::<Vec<&str>, [&str; 1]>`), and the listing before it had sealed only the copy std never consults. - The adopter checks again that it was endowed no service, through its own copy, which never adopts. The check after the adoption goes: the helper's spawn proves the adoption, since std reads its image through `/system`, which std holds only by adopting it. - The two doc lines the wrong diagnosis added go. std's `adopt_services` says what ends its adoption: std's own resolution of a file or lookup of a service. - `process_tree`'s `a_pipe_is_no_place` no longer explains its refusal by the order the kernel no longer has, and `WrongType`'s doc says that `get_sent` never builds it. - `issues/a-programs-own-toyos-never-sees-the-services-std-adopted.md` records the two adopted sets, the defect #1226 closed for stdio's state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adopter, which std's `adopt_services` gives `fs:/system`, then checked that `toyos::endow::services()` listed it. std links its own copy of `toyos`, and each copy keeps its own adopted services: std's adoption filled std's copy, and the program's listing read its own, which adopted nothing. So the check went red in `testcases_members_exit_0` (run 43156966054, `assert_failed::<Vec<&str>, [&str; 1]>`). - The adopter still checks, through its own copy, that it was endowed no service: that copy never adopts, so the check holds at any point. The check after the adoption goes. The helper's spawn proves the adoption, since std reads the helper's image through `/system`, which std holds only by adopting it. - std's `adopt_services` says what ends its adoption: std's own resolution of a file or lookup of a service. - `issues/a-programs-own-toyos-never-sees-the-services-std-adopted.md` records the two adopted sets, the defect #1226 closed for stdio's state. - The owed-on-metal record for #1252 says `testcases_members_exit_0` runs the members #1252 adapted on QEMU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
Problem
std, a program's own copy of
toyosand libc each linked their own copy oflog::stdio's statics: what descriptors 1 and 2 were found to name, the ring's mapping, the partial line of each stream and the pid. A line said in pieces through two runtimes became two records, a ring was mapped and its handle duplicated once per copy and stream, and a redirect was seen only by the copy that made it (libc'sdup2forgot only libc's answer, std'sdup2forgot nobody's). The line's hold spun, so a panic raised under it waited on itself.Resulting behaviour
fd-tablefeature: std's in a Rust program, libc's in a C one). That copy definestoyos_stdio_*besidetoyos_fd_*, and every copy reaches the state through them. It is gated like the table, onfd-table, so the host lint shape for the table's owner (cargo clippy -p toyos --lib --features fd-table) compiles and lints it too.changed()after each install, dup, dup2, replace and close, once its writer's lock is let go. Adup2of a descriptor onto itself changes nothing and calls nothing. Whichever runtime asks for a redirect, the next write asks the descriptor again.stdio::forgetis gone, along with libc's and the supervisor's calls to it.ASKING. Its holder is named the asker for as long as it holds it (one guard,Asking, used by both). A write by the asker to a stream not yet answered goes to the descriptor, so the message of a panic raised under the lock (in an ask, or inchanged()stamping the partial line it finishes) is written rather than waited for. The ask's own refusal panics only after the lock is let go. "No asker" isTid::MAX, the one tid the kernel never issues; a main thread isTid(0).changed(), which held it across the partial line's clock stamp without naming itself the asker (review r1's blocker). TheAskingguard fixes it.sdk/toyos/src/fd.rsis touched only inmod owner(the fivetoyos_fd_*definitions that change what a descriptor names) and in one sentence of the module header. The branch is rebased onto #1234's table. Eachchanged(fd)runs afterchange(..)returns, never inside its closure. An ask holdsASKINGandfd::holdmay take the writer's lock, so the only order isASKINGthen the writer's lock.Coordination
#1227 deletes libc's
fd_errno, which this branch'sdup2insdk/libc/src/posix_io.rsstill calls. Whichever of #1226 and #1227 lands second writesrefuse(crate::errcode::of(e))there.Records
a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another,a-stream-write-spins-on-a-lock-its-own-panic-can-hold.a-rust-program-holds-two-copies-of-its-stream-state: the stream half is fixed. Its TCP half stays open asa-tcp-stream-is-reached-only-by-the-copy-of-the-sdk-that-holds-its-end.a-programs-two-streams-map-its-one-ring-twicestays open. One process still maps its ring once per stream. The record now names the chosen exit, every launcher handing the ring once with the second slot its alias (userland only), and records that each redirect onto a ring maps it again, 2 MiB kept for the process's life. This PR does not build it.Tests
log::stdio: two writers on one hold keep every line whole, a write under the stream's own hold returns as its own records, and a hold that a panic unwinds through is released.stream_state_is_one. Stderr is made a test-owned log ring. It asserts that pieces of one line from std, libc and the SDK form one record, that after a redirect through libc both std and the SDK land in the new ring, and that after a redirect through the SDK libc lands there. It needs the guest because the property is which copy of a static a linked image's three runtimes reach, and no host build links them. It runs on the T14'stestcasesboot. Like every discovered member, it does not run under QEMU.Checks
Evidence, masked, with a README per round listing each command, exit code and revision: r1, r2, r3.
Head 1c3788d is 49bcdc3 plus the owed-on-metal record, a records-only commit, so every check below at 49bcdc3 holds for it. It is on main 45935f4 (#1234's table) and merges cleanly with main e558cc1, which touches none of its files.
Lands on: review LAND, the host checks below and guest run 43080395535 green at 49bcdc3, a code-identical head.
cargo test -p toyos(the table's model tests andlog::stdio's among them),cargo clippy -p toyos --all-targetsandcargo clippy -p toyos --lib --features fd-table, with the adopted lints and-D warnings: all exit 0. Onlysdk/toyoshas host-built code here; libc, std, the supervisor and the test member are guest-built.issues/a-tcp-stream-is-reached-only-by-the-copy-of-the-sdk-that-holds-its-end.md): the T14'stestcasesshared boot withtestcases-watchdog, at main after this PR.mainmust showstream_state_is_onegreen (three runtimes' pieces form one record, and each redirect is seen by the other runtimes), no othertestcasesmember regressed, andtestcases-watchdoggreen. That member runs only on metal and has never run.cargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcases, exit 2) built the x86_64 testcases image: the sysroot with std's and libc's owner, every rust-tests member and the C corpus. The staged images are deleted.libc_children stream_holders a_launch_gives_each_unsent_stdio_slot_a_null session_lifecycle a_supervisor_that_fails_early_says_why_on_the_uart the_uart_carries_the_log_again_once_the_console_carrier_goes virt_user_mode virt_jobs_at_el2. These rows exercise a service's ring as stdio, redirects, libc's children and the supervisor's own lines. Every boot runschanged(), because the supervisor dup2s its ring onto 1 and 2.Tid::MAXfix.stream_state_is_onewas unlinked (notoyos_clink attribute).changed()holdingASKINGacross a panic without being the asker.--ci host. The merge queue runs it.🤖 Generated with Claude Code