From f48024a32c6ec5e73d063530957f0c009782e475 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 12 Nov 2026 05:54:34 +0200 Subject: [PATCH 1/4] mio's selector watches a registration made while another thread selects The mio fork's ToyOS selector armed a watch for each registration from a copy it took at the start of every select, then parked. A registration another thread made meanwhile had no watch until that select returned for some other reason, so a select with no timeout and nothing else ready waited for good. Fork commit 3e070f6 gives the selector an event that every select watches: a register or reregister made while a select waits on its copy signals it, and the select arms its registrations again from a fresh copy. A select now returns only with an event or at its timeout, an empty one included. Both lockfiles that name the fork move to it. `mio_registers_while_parked` is a new member of the testcases boot: the main thread polls with no timeout, a second thread waits for the kernel's roster to show it blocked, then registers a waker through a cloned registry and wakes it, and the poll must return that waker's token. A poll that returns without it, or never returns, ends the process by name past a 10 s ceiling. The issue record is closed. Its owed metal run of `mio_waker_coalesces` (#1261) is part of the testcases boot of main that issues/nothing-fails-when-a-devices-release-or-close-stops-answering-its-polls.md owes whole, every shared member that polls passing, which now holds `mio_registers_while_parked` too. Found while reading the selector and recorded, not fixed: issues/mios-selector-fails-every-select-past-63-registrations.md. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- ...ation-made-while-another-thread-selects.md | 36 ----------- ...ails-every-select-past-63-registrations.md | 22 +++++++ tests/guest/rust-tests/Cargo.lock | 2 +- tests/guest/rust-tests/Cargo.toml | 2 +- .../src/bin/mio_registers_while_parked.rs | 62 +++++++++++++++++++ 6 files changed, 87 insertions(+), 39 deletions(-) delete mode 100644 issues/mios-selector-arms-no-registration-made-while-another-thread-selects.md create mode 100644 issues/mios-selector-fails-every-select-past-63-registrations.md create mode 100644 tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs diff --git a/Cargo.lock b/Cargo.lock index 648b9e6e73e..2048adbd97f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#4108eff31632d3b8ed7e0871bec6e7842aac78aa" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#3e070f61d683d034def4f20e625cef4c44d5ef8f" dependencies = [ "libc", "toyos-abi", diff --git a/issues/mios-selector-arms-no-registration-made-while-another-thread-selects.md b/issues/mios-selector-arms-no-registration-made-while-another-thread-selects.md deleted file mode 100644 index a59cf7b6dc1..00000000000 --- a/issues/mios-selector-arms-no-registration-made-while-another-thread-selects.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -status: open -kind: defect -severity: [hang] -opened: 2026-11-11 ---- - -# mio's selector arms no registration made while another thread selects - -The mio fork's ToyOS selector (`src/sys/toyos/selector.rs`) arms a watch for -each registration at the start of every `select`, from a copy of the list it -takes under the selector's lock, and then parks in `inbox_submit`. A -registration that another thread makes while that `select` is parked is not in -the copy. Its handle gets no watch until the parked `select` returns for some -other reason. With no timeout and nothing else ready, the `select` waits for -good, while the new source's event happens and is never reported. Upstream -mio's `Registry::try_clone` documents a registry that may be cloned to other -threads and registers with the same `Poll`, which is the case this misses. - -The registering thread cannot arm the watch itself: a ring has one submitter, -so its `inbox_submit` while the `select` is parked is refused `Busy`. - -Found by reading, not exercised. - -**Exit**: a registration made while another thread is parked in `select` is -watched before that `select` would otherwise return, for example by the -registration waking the selector so that it arms the watch again; a test -registers a source from a second thread while the first is parked in a -`select` with no timeout, makes the source ready, and the `select` returns its -event. - -- **Owed on metal:** the selector's generation drop (#1261, fork `4108eff`) in - a ToyOS binary. - - Row: `mio_waker_coalesces`, a member of the `testcases` boot of main. - - Bench: the T14. - - Main must show it passing, built from mio `4108eff`. diff --git a/issues/mios-selector-fails-every-select-past-63-registrations.md b/issues/mios-selector-fails-every-select-past-63-registrations.md new file mode 100644 index 00000000000..a3f120beabb --- /dev/null +++ b/issues/mios-selector-fails-every-select-past-63-registrations.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-11-12 +--- + +# mio's selector fails every select past 63 registrations + +The mio fork's ToyOS selector (`src/sys/toyos/selector.rs`) makes its ring with +64 submission slots, and every `select` queues one watch per registration and +one for a change to them before it hands them over in one `inbox_submit`. Its +`queue` does not look at the ring's head, so a 65th watch overwrites the first +before the kernel has taken it, and the kernel refuses a submit of more than +its ring holds `InvalidArgument` (`kernel/src/inbox/mod.rs`, +`submit_submissions`). So while more than 63 sources are registered, every +`select` fails: a tokio runtime on ToyOS cannot serve a 64th socket. + +Found by reading, not exercised. + +**Exit**: a `select` arms every registration however many there are, in +submissions the ring holds, and a test registers more sources than the ring has +slots and sees each one's event. diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 244a6db9e86..8afaf1ec8a8 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -465,7 +465,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#4108eff31632d3b8ed7e0871bec6e7842aac78aa" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#3e070f61d683d034def4f20e625cef4c44d5ef8f" dependencies = [ "libc", "log", diff --git a/tests/guest/rust-tests/Cargo.toml b/tests/guest/rust-tests/Cargo.toml index ee37ce88278..caadd33706e 100644 --- a/tests/guest/rust-tests/Cargo.toml +++ b/tests/guest/rust-tests/Cargo.toml @@ -32,7 +32,7 @@ toyos-pci = { path = "../../../kernel/pci" } libloading = { git = "https://github.com/ToyOSOrg/rust_libloading", branch = "toyos-sdk-0.12" } cpal = { git = "https://github.com/ToyOSOrg/cpal", branch = "toyos-0.18.0" } # Its ToyOS selector and waker, which no program in the image builds, for -# `mio_waker_coalesces`. +# `mio_waker_coalesces` and `mio_registers_while_parked`. mio = { git = "https://github.com/ToyOSOrg/mio", branch = "toyos", features = ["os-poll"] } memmap2 = "0.9" # `ring`'s primitives, for `ring_kat`'s known answers and as `crypto_parity`'s diff --git a/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs b/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs new file mode 100644 index 00000000000..c5c5484bb8f --- /dev/null +++ b/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs @@ -0,0 +1,62 @@ +//! mio's ToyOS selector watches a registration another thread makes while a +//! poll is parked: a waker registered from a second thread once the first is +//! parked in a poll with no timeout, then woken, is the event that poll +//! returns. +//! +//! The poll is parked with nothing else registered: a `Poll` holds one waker, +//! and the waker is the one source this boot can make ready, having no +//! netstack. Red where the poll returns without the waker's event, or never +//! returns: the second thread then ends the process past [`CEILING`], by name. + +#[path = "../roster.rs"] +mod roster; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc; +use std::time::Duration; + +use mio::{Events, Poll, Token, Waker}; +use toyos::endow::{Endowments, SYSCAP_LABEL}; +use toyos::syscap::SysCap; + +const LATE: Token = Token(7); +/// A hang's ceiling: the waker is woken once the poll is parked, and the poll +/// is owed nothing else. +const CEILING: Duration = Duration::from_secs(10); + +/// Set just before the main thread polls. +static POLLING: AtomicBool = AtomicBool::new(false); + +fn main() { + let cap: SysCap = Endowments::get() + .take(SYSCAP_LABEL) + .expect("test-runner endows every binary it spawns a system capability"); + let mut poll = Poll::new().expect("a poll"); + let registry = poll.registry().try_clone().expect("a registry for the second thread"); + let (returned, on_return) = mpsc::channel::<()>(); + + let late = std::thread::spawn(move || { + // Past `POLLING` the main thread blocks only in the poll: parked, or + // on the selector's lock with its copy of the registrations taken. + roster::await_true(|| { + POLLING.load(Ordering::SeqCst) + && roster::my_threads(&cap).iter().any(|&(is_thread, state)| !is_thread && state == roster::BLOCKED) + }); + let waker = Waker::new(®istry, LATE).expect("a waker registered while the poll is parked"); + waker.wake().expect("a wake"); + if on_return.recv_timeout(CEILING).is_err() { + eprintln!("mio_registers_while_parked: a waker registered and woken while the poll was parked was not its event within {CEILING:?}"); + std::process::exit(1); + } + waker + }); + + let mut events = Events::with_capacity(8); + POLLING.store(true, Ordering::SeqCst); + poll.poll(&mut events, None).expect("a poll"); + returned.send(()).expect("the registering thread waits for the poll's return"); + let tokens: Vec = events.iter().map(|event| event.token()).collect(); + let _waker = late.join().expect("the registering thread"); + assert_eq!(tokens, [LATE], "a poll parked while another thread registered and woke a waker"); + println!("mio_registers_while_parked: a waker registered while the poll was parked was its event"); +} From f9e3135d22b4fa3bf06e2bec47b3add543419be5 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 12 Nov 2026 05:57:09 +0200 Subject: [PATCH 2/4] mio_registers_while_parked says which of its two reds is whose, and why its order holds Comments only. Co-Authored-By: Claude Opus 5.5 --- .../rust-tests/src/bin/mio_registers_while_parked.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs b/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs index c5c5484bb8f..685fae2182d 100644 --- a/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs +++ b/tests/guest/rust-tests/src/bin/mio_registers_while_parked.rs @@ -5,8 +5,9 @@ //! //! The poll is parked with nothing else registered: a `Poll` holds one waker, //! and the waker is the one source this boot can make ready, having no -//! netstack. Red where the poll returns without the waker's event, or never -//! returns: the second thread then ends the process past [`CEILING`], by name. +//! netstack. Red where the poll returns without the waker's event, and where +//! it never returns: the second thread then ends the process past [`CEILING`], +//! by name. #[path = "../roster.rs"] mod roster; @@ -36,8 +37,9 @@ fn main() { let (returned, on_return) = mpsc::channel::<()>(); let late = std::thread::spawn(move || { - // Past `POLLING` the main thread blocks only in the poll: parked, or - // on the selector's lock with its copy of the registrations taken. + // Past `POLLING`, until its poll returns, the main thread blocks only + // inside the poll, so a registration made from here lands after the + // poll's copy of the registrations. roster::await_true(|| { POLLING.load(Ordering::SeqCst) && roster::my_threads(&cap).iter().any(|&(is_thread, state)| !is_thread && state == roster::BLOCKED) From 696ca7acdb88d04a5c6fe4b04e39fb18abd8be38 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 12 Nov 2026 07:27:40 +0200 Subject: [PATCH 3/4] issues: a fork registered in .cargo/local.toml dirties the nested lockfiles A fork patch registered in .cargo/local.toml, as the implementer rules say, reaches every cargo invocation under the checkout. rust/library, rust/src/bootstrap and sdk/libc then write [[patch.unused]] into their lockfiles, and the build refuses rust/src/bootstrap as dirty (build/llvm.rs:329). This was seen during this PR and reproduced at f9e3135d2. It is recorded here, not fixed. Co-Authored-By: Claude Opus 5.5 --- ...he-nested-lockfiles-and-stops-the-build.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 issues/a-fork-registered-in-local-toml-dirties-the-nested-lockfiles-and-stops-the-build.md diff --git a/issues/a-fork-registered-in-local-toml-dirties-the-nested-lockfiles-and-stops-the-build.md b/issues/a-fork-registered-in-local-toml-dirties-the-nested-lockfiles-and-stops-the-build.md new file mode 100644 index 00000000000..4752ca1e4a1 --- /dev/null +++ b/issues/a-fork-registered-in-local-toml-dirties-the-nested-lockfiles-and-stops-the-build.md @@ -0,0 +1,40 @@ +--- +status: open +kind: tooling +opened: 2026-11-12 +--- + +# A fork registered in `.cargo/local.toml` dirties the nested lockfiles, and the build then stops + +`.cargo/config.toml` includes the untracked `.cargo/local.toml`. That is where +an implementer registers a fork clone under edit, as a `[patch]` of its git +source and of crates-io (`.claude/agents/implementer.md`, "A fork"). Cargo +reads that configuration for every invocation under the checkout, including +those in workspaces the patch has nothing to do with: `rust/library`, +`rust/src/bootstrap` and `sdk/libc`. Each of those resolves without the +patched crate and writes `[[patch.unused]]` entries for it into its own +`Cargo.lock`. `build/llvm.rs`'s `refuse_uncommitted_bootstrap` then finds +`rust/src/bootstrap` holding changes no commit does, and the build panics +there. So a checkout with a fork registered as the instructions say builds no +image and no sysroot until the registration is removed and the nested +lockfiles are restored. Meanwhile `git status` shows `rust` and +`sdk/libc/Cargo.lock` modified. + +Seen at `3c5c55584` with the mio fork registered: +- a sysroot build wrote the entries into all three lockfiles; +- `cargo run -- --build-only` then panicked at `build/llvm.rs:329`. + +Reproduced at `f9e3135d2` without a sysroot build: +- `cargo metadata` in `rust/src/bootstrap` and in `sdk/libc` writes the entries; +- `cargo run -- --build-only` exits 101 at the same assertion + (https://github.com/ToyOSOrg/evidence/tree/a3ad7b166e2d80021bf2d633adcd704096c66f17/pr-1276/local-toml). + +Building one crate with the patch given as `cargo --config` leaves every other +lockfile alone. + +**Exit**: a fork registered as the instructions say reaches the builds that +consume it, and no lockfile outside those workspaces changes. Alternatively, +the instructions name a registration that has that property. + +**Owner**: the orchestrator, for the build system and the implementer's fork +rules. From 924b72173e520ca1d4465e8e1f0b8f9c1cbea448 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 12 Nov 2026 07:46:47 +0200 Subject: [PATCH 4/4] Review r1 of #1276: a refused registration watch is reported, and the 63-registration record counts streams right B2: fork commit ed29321 ends the spin a refused registration watch made of a select. The kernel refuses a watch it will never fire: a handle closed while it was watched, one without the right to wait on it, or an interest its object has no watch for. Such a watch now ends its registration and is reported once, as an error in the directions the registration reports, so nothing is armed again in a loop. The same commit makes the selector's change event and a waker's event one type (S1), and makes its take as loud as its signal (S2). Both lockfiles that name the fork move to it. B3: a TCP stream registered for both directions takes two registrations, so the record's tokio runtime fails at its 31st stream, not its 64th socket. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- ...ios-selector-fails-every-select-past-63-registrations.md | 6 ++++-- tests/guest/rust-tests/Cargo.lock | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2048adbd97f..08a130960c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#3e070f61d683d034def4f20e625cef4c44d5ef8f" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#ed29321759dfc6c981926cb0c59f7deb9936b12c" dependencies = [ "libc", "toyos-abi", diff --git a/issues/mios-selector-fails-every-select-past-63-registrations.md b/issues/mios-selector-fails-every-select-past-63-registrations.md index a3f120beabb..5a6f6211076 100644 --- a/issues/mios-selector-fails-every-select-past-63-registrations.md +++ b/issues/mios-selector-fails-every-select-past-63-registrations.md @@ -12,8 +12,10 @@ one for a change to them before it hands them over in one `inbox_submit`. Its `queue` does not look at the ring's head, so a 65th watch overwrites the first before the kernel has taken it, and the kernel refuses a submit of more than its ring holds `InvalidArgument` (`kernel/src/inbox/mod.rs`, -`submit_submissions`). So while more than 63 sources are registered, every -`select` fails: a tokio runtime on ToyOS cannot serve a 64th socket. +`submit_submissions`). So while more than 63 registrations stand, every +`select` fails. A TCP stream registered for both directions takes two, its +receive bell and its bell (`src/net/tcp/toyos_stream.rs`, `register`), so a +tokio runtime with its waker and one listener fails at its 31st stream. Found by reading, not exercised. diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 8afaf1ec8a8..71a6eafb9ca 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -465,7 +465,7 @@ dependencies = [ [[package]] name = "mio" version = "1.2.0" -source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#3e070f61d683d034def4f20e625cef4c44d5ef8f" +source = "git+https://github.com/ToyOSOrg/mio?branch=toyos#ed29321759dfc6c981926cb0c59f7deb9936b12c" dependencies = [ "libc", "log",