From 80e657c9e4328fd5e51e3fb8fd65be65bb640296 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 01:11:29 +0200 Subject: [PATCH 1/8] release: stage the toolchain install and publish in a TempDir #529 (e48604c0) made every guest, tcg and audio lane red on anything left in its private $TMPDIR. `release::install` downloaded the toolchain tarball to `$TMPDIR/toyos-toolchain.tar.zst` and never removed it, so every such lane on the nightly (run 36273557690) failed its last step naming `toyos-toolchain.tar.zst`. Both release paths now stage in a `toyos_tmpdir::TempDir`, removed on every way out: the install's tarball, and the publish's TOOLCHAIN, notes.md and tarball. `src/release.rs` leaves `TEMP_DIR_ALLOWED`, which closes issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md. This file is one of the trees the toolchain tag hashes, so the tag moves and the nightly's `build` job publishes the new one. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...ages-in-tmpdir-outside-the-scratch-guard.md | 18 ------------------ src/release.rs | 6 ++++-- src/sourcegate.rs | 2 -- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md diff --git a/issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md b/issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md deleted file mode 100644 index 3612553b542..00000000000 --- a/issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-26 ---- - -# The release path stages in `$TMPDIR` outside the scratch guard - -`src/release.rs` takes `std::env::temp_dir()` twice: the toolchain install -downloads its tarball to `$TMPDIR/`, and the publish writes -`TOOLCHAIN`, `notes.md` and the built tarball straight into `$TMPDIR`. Nothing -removes them, on success or on failure, and two runs on one host share the -same fixed names. Every other host scratch is a `toyos_tmpdir::TempDir`, -which is gone with its holder; `src/sourcegate.rs`'s -`every_host_scratch_is_the_guard` counts these two as named exceptions. - -**Exit**: both paths stage in a `toyos_tmpdir::TempDir`, and `src/release.rs` -leaves `TEMP_DIR_ALLOWED`. diff --git a/src/release.rs b/src/release.rs index d9b08fefc38..329267fa93c 100644 --- a/src/release.rs +++ b/src/release.rs @@ -18,6 +18,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use sha2::{Digest, Sha256}; +use toyos_tmpdir::TempDir; /// What the tag hashes, as `git rev-parse HEAD:` names them. The last is /// this file. @@ -129,7 +130,8 @@ pub fn install(root: &Path) -> Result { ) })?; let token = std::env::var("GH_TOKEN").expect("checked above"); - let tarball = std::env::temp_dir().join(ASSET); + let staging = TempDir::new("toolchain-install"); + let tarball = staging.join(ASSET); let into = root.join("rust/build"); fs::create_dir_all(&into).map_err(|e| format!("{}: {e}", into.display()))?; // The retry is on the transfer and the unpack together: a truncated body is @@ -204,7 +206,7 @@ pub fn ensure_published(root: &Path) -> Result { install nowhere" )); } - let tmp = std::env::temp_dir(); + let tmp = TempDir::new("toolchain-publish"); let manifest = manifest(root, &tag)?; let notes = notes(root, &tag, &manifest)?; fs::write(tmp.join("TOOLCHAIN"), &manifest).map_err(|e| e.to_string())?; diff --git a/src/sourcegate.rs b/src/sourcegate.rs index 9424b0e3bac..5deaf2397f3 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -828,8 +828,6 @@ const TEMP_DIR_ALLOWED: &[(&str, usize)] = &[ // A failed golden comparison leaves its dump for the reader to copy over // the golden file: outliving the test is its purpose. ("toyos-keymap/tests/tables.rs", 1), - // `issues/build/the-release-path-stages-in-tmpdir-outside-the-scratch-guard.md`. - ("src/release.rs", 2), ]; From 05839b769e8d538b870d09aa21759f9ec143de2d Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 01:20:50 +0200 Subject: [PATCH 2/8] kernel: a thread join keeps the answer its collect gave `fpu_isolation` has been red since #513 (46056bfa, "One way to wait"): every `check` child's `thread_join` on its probe thread answered NotFound. `sys_thread_join`'s wait predicate was `wait_thread_zombie`, which collects: it takes the zombie out of the table. Before #513, `completion::wait_until` returned on the `Gone(Closed)` that `thread_exit` posted without running the predicate again, so the loop's own collect found the zombie. #513's `watch::wait_until` runs the predicate after every wake; it collected the zombie and returned, and the loop's next collect found no such thread. Every joiner that parked before its thread exited got NotFound. std's and libc's joins drop the answer, so only `fpu_isolation`, which asserts it, went red. The predicate now keeps the first answer it collects, and the syscall returns that answer rather than asking again. Red before, at fd62f567: `cargo test --test toyos-build -- --nightly fpu_isolation` EXIT=1, `thread_join failed, left: 18446744073709551614`. Green after: EXIT=0. Files issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...-libc-drop-the-answer-thread-join-gives.md | 21 +++++++++++++++ kernel/src/arch/syscall/proc.rs | 27 ++++++++++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md diff --git a/issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md b/issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md new file mode 100644 index 00000000000..2ab35f48043 --- /dev/null +++ b/issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# std and libc drop the answer `thread_join` gives + +`rust/library/std/src/sys/thread/toyos.rs`'s `Thread::join` and +`userland/libc/src/pthread.rs`'s `pthread_join` call +`toyos_abi::syscall::thread_join` and discard what it returns. A join the +kernel refuses — `NotFound` for a tid it never had or already collected, +`Gone` for a cancelled wait — reads as a thread that finished. std's unix +`join` panics on the same refusal, and `pthread_join` returns 0 for it. + +It hid a kernel defect: from #513 until the fix beside this file, +`sys_thread_join` answered `NotFound` to every joiner that parked before its +thread exited, and only `fpu_isolation`, which asserts the answer, saw it. + +**Exit**: std's `join` fails loudly on a refused join and `pthread_join` +returns the error, each with a guest test that joins a tid twice. diff --git a/kernel/src/arch/syscall/proc.rs b/kernel/src/arch/syscall/proc.rs index 14f3f1003b0..f91d32e782a 100644 --- a/kernel/src/arch/syscall/proc.rs +++ b/kernel/src/arch/syscall/proc.rs @@ -153,12 +153,24 @@ pub(super) fn sys_thread_join(tid: u64) -> u64 { // None means never existed or already collected; the predicate below answers both. let target = process::thread_sched(caller, tid); let parkable = crate::scheduler::Parkable::at_entry(); - loop { - match process::wait_thread_zombie(tid, caller) { - Ok(Some(_)) => return 0, - Ok(None) => {} - Err(()) => return SyscallError::NotFound.to_u64(), - } + // Collecting takes the zombie out of the table, so the first answer is kept: asked + // again, the same join finds no such thread. + let answer = core::cell::Cell::new(None); + let settled = || { + answer.get().is_some() + || match process::wait_thread_zombie(tid, caller) { + Ok(None) => false, + Ok(Some(_)) => { + answer.set(Some(0)); + true + } + Err(()) => { + answer.set(Some(SyscallError::NotFound.to_u64())); + true + } + } + }; + while !settled() { let Some(sched) = target.as_ref() else { // Nothing to arm on and no zombie: wait_thread_zombie will never answer differently. return SyscallError::NotFound.to_u64(); @@ -170,13 +182,14 @@ pub(super) fn sys_thread_join(tid: u64) -> u64 { tid.raw() as u64, WaitClass::Other, Deadline::never(), - || matches!(process::wait_thread_zombie(tid, caller), Ok(Some(_)) | Err(())), + settled, ) .is_err() { return cancelled(); } } + answer.get().expect("a settled join holds its answer") } pub(super) fn sys_nanosleep(nanos: u64) -> u64 { From 17bb26416ef2ec65cbb7928c094f41c52a12234c Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 01:27:03 +0200 Subject: [PATCH 3/8] blockd judge: a reissue keeps the order of the writes that overlap `blockd_survives_its_death` (from #525, 7345e3fe) read QEMU's trace and required the blockd after a kill to write the acknowledged, unflushed writes in the order the device first saw them. The client promises less: `toyos-blockring`'s client reissues in first-acknowledged order, and requests in flight together are unordered. Two writes that do not overlap can go out as A then B and be acknowledged B then A. On the nightly of PR #524 (run 36273557690, guest (2)) the first lifetime wrote sectors [282600, 281592] and the reissue wrote [281592, 282600]. A local run of the same test wrote [282600, 281592] both times. The same writes, reordered only between writes that do not overlap. The judge now requires the same writes as a multiset. Among the writes that overlap any one of them, it requires the order they first went out in. Overlapping writes are never in flight together, so for them the device's order is the acknowledged order. Negative control: the client with `lost()`'s reissue compiled out (the `mutate-no-reissue-after-loss` body), applied as a checked patch and restored. EXIT=1: "the blockd after it wrote [281592, 280584] first". Green after: `cargo test --test toyos-build -- --nightly blockd_survives_its_death` EXIT=0. Before the change it was green alone 6/6 locally, so its red is the CI run's ordering, recorded above. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- tests/common/blockd.rs | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/tests/common/blockd.rs b/tests/common/blockd.rs index 12ebab5f4b4..10390e75b8f 100644 --- a/tests/common/blockd.rs +++ b/tests/common/blockd.rs @@ -263,10 +263,12 @@ fn trace_events(trace: &Path) -> Result, String> { /// whose writes to `span` did not end in a flush and after which another /// lifetime wrote to it; its last write is the one blockd withheld the answer /// to, and the ones before it since its last flush are the acknowledged ones. -/// The lifetime after it must write exactly those, in that order, before any -/// other write to `span` — which is what reissue means, and what a client -/// that forgot them cannot do by accident. Every Flush in the trace is -/// blockd's: the kernel's driver issues none. +/// The lifetime after it must write exactly those before any other write to +/// `span`, each after every one of them it overlaps that came before it — +/// which is what reissue means, and what a client that forgot them cannot do +/// by accident. Two writes that do not overlap may be in flight together, so +/// the device's order between them is neither lifetime's to keep. Every Flush +/// in the trace is blockd's: the kernel's driver issues none. fn reissued_after(trace: &Path, span: Span) -> Result, String> { let mut lives: Vec> = Vec::new(); for did in trace_events(trace)? { @@ -281,9 +283,12 @@ fn reissued_after(trace: &Path, span: Span) -> Result, String> { } let wrote = |life: &[Did]| life.iter().any(|d| matches!(d, Did::Wrote { .. })); let volume: Vec<&[Did]> = lives.iter().map(Vec::as_slice).filter(|l| wrote(l)).collect(); - let tail = |life: &[Did]| -> Vec { + let writes = |dids: &[Did]| -> Vec<(u64, u64)> { + dids.iter().filter_map(|d| match d { Did::Wrote { lba, sectors } => Some((*lba, *sectors)), _ => None }).collect() + }; + let tail = |life: &[Did]| -> Vec<(u64, u64)> { let after = life.iter().rposition(|d| *d == Did::Flushed).map_or(0, |at| at + 1); - life[after..].iter().filter_map(|d| match d { Did::Wrote { lba, .. } => Some(*lba), _ => None }).collect() + writes(&life[after..]) }; let died: Vec = (0..volume.len().saturating_sub(1)).filter(|&i| !tail(volume[i]).is_empty()).collect(); let [died] = died[..] else { @@ -295,7 +300,7 @@ fn reissued_after(trace: &Path, span: Span) -> Result, String> { )); }; let mut unflushed = tail(volume[died]); - let withheld = unflushed.pop().expect("a tail is not empty"); + let (withheld, _) = unflushed.pop().expect("a tail is not empty"); // An empty prefix equals anything, so a loss with nothing acknowledged // before it would pass with no write to reissue. if unflushed.is_empty() { @@ -304,12 +309,17 @@ fn reissued_after(trace: &Path, span: Span) -> Result, String> { acknowledged was left to reissue" )); } - let next: Vec = volume[died + 1] - .iter() - .filter_map(|d| match d { Did::Wrote { lba, .. } => Some(*lba), _ => None }) - .take(unflushed.len()) - .collect(); - if next != unflushed { + let next: Vec<(u64, u64)> = writes(volume[died + 1]).into_iter().take(unflushed.len()).collect(); + let overlaps = |a: (u64, u64), b: (u64, u64)| a.0 < b.0 + b.1 && b.0 < a.0 + a.1; + // Every write that overlaps one of them, in the order it went out. + let around = |w: (u64, u64), ws: &[(u64, u64)]| ws.iter().copied().filter(|&o| overlaps(o, w)).collect::>(); + let (mut want, mut got) = (unflushed.clone(), next.clone()); + want.sort_unstable(); + got.sort_unstable(); + let same = want == got && unflushed.iter().all(|&w| around(w, &unflushed) == around(w, &next)); + let lbas = |ws: &[(u64, u64)]| ws.iter().map(|w| w.0).collect::>(); + let (unflushed, next) = (lbas(&unflushed), lbas(&next)); + if !same { return Err(format!( "blockd died with the writes at sectors {unflushed:?} acknowledged and no flush after them \ (and {withheld} withheld); the blockd after it wrote {next:?} first" From 9583d913de1901440f732008c23c82023b34401c Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 01:35:34 +0200 Subject: [PATCH 4/8] partclaim: judge the kernel's records through the shutdown's drain `partition_claim_departure` went red on the nightly of PR #524 (run 36273557690, guest (12)) with `the kernel never said "usb-storage: disk 0 came back on port 3 slot "`. The guest's own lines were all there, including the flush refusal that follows that record. Beside other guests it also reds as `silent: 0 flushes were told of the loss, not 1` (issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md, filed at #511). The cause is two console paths. The runner's `===TEST_END===` reaches the console through `logd`, and the kernel's records through `klogd` (tests/common/qemu.rs already says so for the start marker). A record the kernel made before the test ended can arrive after the marker. The window closes on the marker, so `guest_verdict` judged a kernel log that did not yet hold it. It is not #525's; the test and its judge predate it. Reproduced on this host at fd62f567, with 18 CPU spinners on 14 cores beside the run: 1 red in 6 (`silent: 0 flushes were told of the loss, not 1`). The capture shows the kernel's record of the return and then the claimant's `refused with Io` and `PASS`, but not the TOLD record the kernel wrote before answering Io. The harness's re-run alone was green. `guest_verdict` now takes the shutdown's drain and judges the kernel's records from the test's start through it. The shutdown runs before the verdict at all three call sites. With the fix, under the same load: 8 of 8 green (EXIT=0 each). `cargo test --test toyos-build -- --nightly partition_claim`: 3 passed, EXIT=0. Closes the departure finding. The gives-up finding stays open: it also names a global fsync-deadman race this does not touch. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...rture-told-no-flush-beside-other-guests.md | 24 ------------------- tests/common/partclaim.rs | 24 +++++++++++-------- 2 files changed, 14 insertions(+), 34 deletions(-) delete mode 100644 issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md diff --git a/issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md b/issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md deleted file mode 100644 index 782f1fc481f..00000000000 --- a/issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-09-26 ---- - -# `partition_claim_departure` told no flush of the loss beside other guests - -Fast tier at `a58abf50` (PR #511's merged head; another worktree's FAT suite -ran on the host at the same time): `departure: 0 flushes were told of the -loss, not 1`. The guest's own lines say the opposite: `usb-storage: disk 0 -came back on port 3 slot 1 as the same device ... so the flush of each writer -whose writes they were fails`, then `partition_claimant: the departing -partition's flush, over a write the device lost, refused with Io` and -`partition_claimant: PASS`. The harness's re-run alone was green (`departure: -1 told`), and so was `cargo test --test toyos-build -- -partition_claim_departure` alone afterwards (EXIT=0). `cargo run -- ---known-red` answers NO. - -Not shown: which line the judge counts as "told", and why it found none in a -capture that holds the claimant's refusal. - -**Exit**: the judge's count shown to read the line the guest wrote on a -loaded host, or the red reproduced with its cause. diff --git a/tests/common/partclaim.rs b/tests/common/partclaim.rs index fb8a2905297..7828bc1b2c7 100644 --- a/tests/common/partclaim.rs +++ b/tests/common/partclaim.rs @@ -186,8 +186,8 @@ pub fn partition_claim( // did to the neighbours is the half of the verdict it cannot give itself. let run = format!("test_rs_partition_claimant main {esp} {log} {root}"); let result = qemu.run_test(&run, Duration::from_secs(180)); - let guest = guest_verdict(&result, REFUSALS).and_then(|kernel| main_kernel_lines(&kernel)); let tail = shut_down(qemu); + let guest = guest_verdict(&result, &tail, REFUSALS).and_then(|kernel| main_kernel_lines(&kernel)); no_panic("on the way down", &tail)?; let after = std::fs::read(&nvme).map_err(|e| format!("read the disk back: {e}"))?; @@ -262,13 +262,13 @@ pub fn partition_claim_gives_up( no_panic(role, &boot)?; let result = qemu.run_test(&format!("test_rs_partition_claimant {role}"), Duration::from_secs(180)); - let kernel = guest_verdict(&result, refusals).map_err(|e| format!("{role}: {e}"))?; + let tail = shut_down(qemu); + let kernel = guest_verdict(&result, &tail, refusals).map_err(|e| format!("{role}: {e}"))?; for want in wants { if !kernel.contains(want) { return Err(format!("{role}: the kernel never said {want:?}:\n{kernel}")); } } - let tail = shut_down(qemu); no_panic(role, &tail)?; for want in wants { let line = kernel.lines().find(|l| l.contains(want)).unwrap_or_default(); @@ -343,8 +343,9 @@ pub fn partition_claim_departure( } /// One departure boot running the guest's `role`, which says `refusals` -/// refusals: the kernel's log while it ran, what the shutdown said, and the -/// stick's partitions — `DEPARTING`, `STAYING`, `EARLIER`. +/// refusals: the kernel's log from the test's start through the shutdown, +/// what the shutdown said, and the stick's partitions — `DEPARTING`, +/// `STAYING`, `EARLIER`. fn departed( test_config: &Path, c_bins: &[(String, Vec)], @@ -392,22 +393,25 @@ fn departed( ); }, ); - let kernel = guest_verdict(&result, refusals).map_err(|e| format!("{role}: {e}"))?; + let tail = shut_down(qemu); + let kernel = guest_verdict(&result, &tail, refusals).map_err(|e| format!("{role}: {e}"))?; for want in [MOVE_NOW, CAME_BACK] { if !kernel.contains(want) { return Err(format!("{role}: the kernel never said {want:?}:\n{kernel}")); } } - let tail = shut_down(qemu); no_panic(role, &tail)?; Ok((kernel, tail, spans)) } /// What the guest said: exit 0, `refusals` refusals said by name — an exit /// code alone is also what a binary that asserted nothing leaves — and the -/// kernel's log while it ran, which is returned. -fn guest_verdict(result: &qemu::TestResult, refusals: usize) -> Result { - let kernel = format!("{}{}", result.before, result.serial); +/// kernel's log from the test's start through the shutdown's `tail`, which is +/// returned. The runner's end marker reaches the console through `logd` and +/// the kernel's records through `klogd`, so a record the kernel made before +/// the test ended can arrive after the marker; the shutdown's drain carries it. +fn guest_verdict(result: &qemu::TestResult, tail: &str, refusals: usize) -> Result { + let kernel = format!("{}{}{tail}", result.before, result.serial); if result.exit_code != Some(0) { return Err(format!( "the guest failed:\n{}\nkernel log while it ran:\n{kernel}", From f231c43eb235d91fe5c51b815ef4ebdf59434f48 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 01:42:28 +0200 Subject: [PATCH 5/8] issues: close #524's two filings of main's nightly reds issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md is fixed by 80e657c9: the install stages in a `toyos_tmpdir::TempDir`. issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md is fixed by 05839b76: the join keeps the answer its collect gave. The landing it left unbisected is #513, whose `watch::wait_until` re-runs the predicate that collects. Nothing cites either file. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...ves-the-toolchain-tarball-in-its-tmpdir.md | 19 ------------ ...s-thread-join-answers-not-found-on-main.md | 31 ------------------- 2 files changed, 50 deletions(-) delete mode 100644 issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md delete mode 100644 issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md diff --git a/issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md b/issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md deleted file mode 100644 index ec08efc3e01..00000000000 --- a/issues/build/a-guest-lane-leaves-the-toolchain-tarball-in-its-tmpdir.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-26 ---- - -# A guest lane leaves the toolchain tarball in its `$TMPDIR` - -`src/ci.rs`'s guest lane points `$TMPDIR` at a `TempDir` and ends on -`nothing left in $TMPDIR`. Its `the toolchain` step is `release::install`, which -downloads `toyos-toolchain.tar.zst` to `std::env::temp_dir()`, unpacks it, -and never removes it. So every guest, tcg and audio job reds on -`left … : toyos-toolchain.tar.zst` whatever its suite did. Seen on all fifteen -of those jobs in the nightly on PR #524's branch (run 36266825579). Neither -side is that branch's: both are main's, the step from `ba68664b` (#529), the -install from before it. Main has had no nightly since #529 landed. - -**Exit**: the tarball goes into a `TempDir`, or is removed once it is -unpacked, and a guest lane's last step is green on a nightly. diff --git a/issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md b/issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md deleted file mode 100644 index 22f81548621..00000000000 --- a/issues/kernel/fpu-isolation-s-thread-join-answers-not-found-on-main.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-26 ---- - -# `fpu_isolation`'s probe thread is joined as not found, on main - -`fpu_isolation` (nightly tier) is red on `main` at `17eb66a4`, and on every -branch built on it. Each `check` child spawns `thread_probe`, which records -its entry state and calls `SYS_THREAD_EXIT`. `thread_join` on that tid then -answers `18446744073709551614` (−2), where 0 is expected -(`tests/toyos-rust-tests/src/bin/fpu_isolation.rs:346`). The kernel logs the -thread's `exit … code=0`, so the thread ran, and `sys_thread_join` answers -`NotFound` from `wait_thread_zombie`'s `Err(())`: no zombie for a thread that -existed. The leak arm therefore reports `a process started with the previous -one's FP registers` in all three rounds; that is the arm's wording, and not -what failed. - -Measured by `cargo test --test toyos-build -- fpu_isolation --nightly`: -- at `17eb66a4`: EXIT=1, the harness's alone re-run red the same way; -- on PR #524's branch at `235c5a5b`, and on its nightly (run 36266825579, - `guest (12)`): the same. - -It passed on main's nightly at `e8d7c9c0` (run 36228604597, `guest (11)`). -Thirteen landings fall between the two, and none is bisected. One of them, -#513 ("One way to wait", `46056bfa`), rewrote `sys_thread_join`'s wait. - -**Exit**: `fpu_isolation` green, and a test that joins a thread which has -already exited by the time of the join, red on the tree this issue was -filed against. From 635a9a14c939a7f608b76760acbeb3f33afd4bd9 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 03:07:31 +0200 Subject: [PATCH 6/8] exit_wait_storm's judge: the parent is the lowest pid, read from before the marker too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exit_wait_storm` is red on main's nightly at fd62f567 (run 36278449733, guest (1), red alone as well): `the kernel accounted children exiting with [0, 0, 1, 2, 3, 4, 5, 7, …]`. Locally at this branch it also reds as `the parent made 0 call(s) of syscall 25`. The judge took the parent to be the first `spawn:` line of the binary in the test's window. The runner's `===TEST_START===` reaches the console through `logd` and the kernel's records through `klogd`. The harness moves only the lines from the *last* matching spawn record in `before` into the window. When the parent and its first children were spawned before the marker arrived, the window opened at a child's spawn. The judge then called that child the parent, counted the real parent's exit (code 0) as a child's, and read a child's syscall profile as the parent's. A probe printed every storm spawn line with the half it landed in, applied as a checked patch and restored. On a red run: pids 7 (the parent), 8 and 9 in `before`, 10 onward in `serial`. The judge now reads `before` and the window together. It takes the parent as the lowest pid among the storm's spawn lines, since pids are never reused and the parent is made first, and it counts as children only the pids above it. - Red before, locally: 3 of 4 runs red (one run whose harness re-run was red too, and the probe loop's red). - Green after: `cargo test --test toyos-build -- --nightly exit_wait_storm` 8 of 8, EXIT=0 each. This was green on main's nightly at 3f46a019 (run 36111884575) and flaky at e8d7c9c0 (run 36228604597). The two-path console came with #492 (b0adc600) between them; not bisected. Also files the two older nightly reds this branch does not fix, with what was measured: issues/boot-media/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md issues/boot-media/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...went-away-panics-the-boot-at-roots-hold.md | 38 +++++++++++++++++++ ...-boot-leaves-no-i8042-line-on-the-panel.md | 27 +++++++++++++ tests/toyos.rs | 37 ++++++++++-------- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 issues/boot-media/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md create mode 100644 issues/boot-media/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md diff --git a/issues/boot-media/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md b/issues/boot-media/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md new file mode 100644 index 00000000000..53cd4f21fe5 --- /dev/null +++ b/issues/boot-media/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md @@ -0,0 +1,38 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# A disk whose port went away panics the boot at ROOT's hold + +`usb_transport_break` (nightly tier) fails its boot with: + + PANIC: panicked at src/rootfs.rs:177:19: + boot: the partition ROOT was read from, , cannot be held: Unusable + +`rootfs::hold_source` (#506, 265a0fce) runs after the USB gate. It asks +`gpt::claimable` for ROOT's partition, and that reads every registered disk's +table. The test arms `usb-port-gone`, which ends the gate's last read on its +disk as a port that read disconnected and leaves the disk to its port's +teardown. That disk's table read then fails, `table_refused` answers +`Unusable`, and `hold_source` panics, although ROOT is on the other stick. + +Measured at 9583d913 merged with 5e446e5c: +- `cargo test --test toyos-build -- --nightly usb_transport_break` EXIT=1, + red alone as well. +- The same run with `usb-port-gone` removed from the test's `PARAMS`, as a + checked patch that was restored: the boot no longer panics, and the test + fails only on the port-gone assertion it no longer staged. + +The nightlies agree: +- Red the same way on main's nightly at e8d7c9c0 (run 36228604597). +- Red at fd62f567 (run 36278449733, guest (7)). +- On the nightly at 3f46a019 (run 36111884575, before #506) it failed for a + different reason. + +`cargo run -- --known-red usb_transport_break` answers NO. + +**Exit**: a disk that left during boot is not a disk ROOT's hold must read, +or the boot's refusal is what `usb_transport_break` expects. Either way +`usb_transport_break` is green on a nightly. diff --git a/issues/boot-media/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md b/issues/boot-media/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md new file mode 100644 index 00000000000..a8fce9a39c1 --- /dev/null +++ b/issues/boot-media/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# `screen_diag_boot` finds no `i8042:` line on the panel + +`screen_diag_boot` (nightly tier) fails with `"i8042:" is not on screen five +seconds after the boot finished`. The decoded panel ends at `Boot: complete`, +and no i8042 line is painted in the five seconds after it. The i8042 health +line (`report_health`, `to_screen`) never reaches the panel on the diag image. + +- Green on main's nightly at 3f46a019 (run 36111884575). +- Red at e8d7c9c0 (run 36228604597). +- Red at fd62f567 (run 36278449733, guest (9)). +- Red alone at 9583d913 merged with 5e446e5c: `cargo test --test toyos-build + -- --nightly screen_diag_boot` EXIT=1. + +The range 3f46a019..e8d7c9c0 holds fourteen landings and is not bisected. +The candidates that touch the idle path, the console or the log are #502 +(a288537b), #492 (b0adc600) and #506 (265a0fce). + +`cargo run -- --known-red screen_diag_boot` answers NO. + +**Exit**: the landing named by a build at it and at its parent, and +`screen_diag_boot` green on a nightly. diff --git a/tests/toyos.rs b/tests/toyos.rs index 839da4b2331..478d3e3f777 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -3587,18 +3587,23 @@ const STORM_CALLS: [(u64, usize); 3] = [ (toyos_abi::syscall::SYS_THREAD_JOIN, STORM_CHILDREN), ]; -/// The parent is the *first* `spawn:` line, since every later one is a child of -/// it; `spawned_pid` reads the last and would answer with a child. +/// What the storm's judge reads: the window and the lines before its start +/// marker. The marker reaches the console through `logd` and the kernel's +/// records through `klogd`, so the parent's spawn record and its first +/// children's can arrive ahead of it, and the window then opens at a child's. +fn storm_log(result: &TestResult) -> String { + format!("{}{}", result.before, result.serial) +} + +/// The parent is the lowest pid among the storm's `spawn:` lines: pids are +/// never reused and it is made before any child. `spawned_pid` reads the last +/// and would answer with a child. fn storm_parent(log: &str) -> Option { let want = format!("/system/bin/test_rs_{STORM} "); log.lines() - .find(|l| l.contains("spawn: ") && l.contains(&want))? - .split("pid=") - .nth(1)? - .split_whitespace() - .next()? - .parse() - .ok() + .filter(|l| l.contains("spawn: ") && l.contains(&want)) + .filter_map(|l| l.split("pid=").nth(1)?.split_whitespace().next()?.parse().ok()) + .min() } /// Wait for the parent's accounting line: every child's and every thread's @@ -3608,9 +3613,9 @@ fn settle_exit_wait_storm(qemu: &mut QemuInstance, result: &mut TestResult) { /// A liveness ceiling and never a verdict. const ACCOUNTED: Duration = Duration::from_secs(5); - let Some(pid) = storm_parent(&result.serial) else { return }; + let Some(pid) = storm_parent(&storm_log(result)) else { return }; let want = accounting_of(pid); - if result.serial.contains(&want) { + if storm_log(result).contains(&want) { return; } let more = qemu.drain_until(ACCOUNTED, |l| l.contains(&want)); @@ -3625,7 +3630,8 @@ fn check_exit_wait_storm(result: &TestResult) -> bool { if !check_rust_result(result) { return false; } - let Some(parent) = storm_parent(&result.serial) else { + let log = storm_log(result); + let Some(parent) = storm_parent(&log) else { eprintln!( "FAIL rs::{STORM}: no `spawn: /system/bin/test_rs_{STORM}` line reached the capture, so \ nothing here says what the kernel saw{}", @@ -3635,13 +3641,14 @@ fn check_exit_wait_storm(result: &TestResult) -> bool { }; let died = format!("exit: test_rs_{STORM} pid="); let mut codes: Vec = Vec::new(); - for line in result.serial.lines() { + for line in log.lines() { let Some(rest) = line.split(died.as_str()).nth(1) else { continue; }; let mut fields = rest.split_whitespace(); let Some(Ok(pid)) = fields.next().map(str::parse::) else { continue }; - if pid == parent { + // Its children are the pids made after it. + if pid <= parent { continue; } if let Some(Ok(code)) = @@ -3662,7 +3669,7 @@ fn check_exit_wait_storm(result: &TestResult) -> bool { return false; } let want = accounting_of(parent); - let Some(line) = result.serial.lines().find(|l| l.contains(want.as_str())) else { + let Some(line) = log.lines().find(|l| l.contains(want.as_str())) else { eprintln!( "FAIL rs::{STORM}: the kernel never accounted the parent — no `{want}` line \ reached the capture, so nothing here says which calls it made{}", From 0bc804d8a70313fb25d5c30bbe60f70a687b865d Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 03:08:00 +0200 Subject: [PATCH 7/8] issues: home_budget_refusal_retried is red on every nightly Red on main's nightlies at e8d7c9c0 and fd62f567 and on #524's, in two shapes; green alone on the dev host. No issue carried it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- ...refusal-retried-is-red-on-every-nightly.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md diff --git a/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md b/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md new file mode 100644 index 00000000000..5e8088979a1 --- /dev/null +++ b/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md @@ -0,0 +1,31 @@ +--- +status: open +kind: defect +opened: 2026-09-27 +--- + +# `home_budget_refusal_retried` is red on every nightly + +`home_budget_refusal_retried` (nightly tier) is red, and red alone, on the +nightlies of three trees, each time with the same two shapes: +- main at e8d7c9c0 (run 36228604597); +- PR #524's branch at 8c5be843 (run 36273557690); +- main at fd62f567 (run 36278449733, guest (12)). + +The two shapes: `no fsync: /home/... durable on attempt line — the retry +never ran`, and `Boot timed out waiting for ===READY===`. In the second, the +console ends at the loader's `Applied 5483 relocations`, with +`fsync-budget-spent` on the boot parameter line. It was green at 3f46a019 +(run 36111884575). + +It is green alone on a dev host at 635a9a14 (`cargo test --test toyos-build +-- --nightly home_budget_refusal_retried` EXIT=0). +`cargo run -- --known-red home_budget_refusal_retried` answers NO. + +`fsync-budget-spent` is the machine-wide actuator that +issues/boot-media/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md +names as racing whichever fsync the boot reaches first. Not shown: whether +that race is this test's cause. + +**Exit**: the cause shown on a red run's log, and the test green on a +nightly. From 67a430c89f761f99f88281de28e9cd68fde3ae83 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 03:08:10 +0200 Subject: [PATCH 8/8] issues: the home budget issue names the tree its local run was at Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK --- .../home-budget-refusal-retried-is-red-on-every-nightly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md b/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md index 5e8088979a1..25d447d835b 100644 --- a/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md +++ b/issues/filesystem/home-budget-refusal-retried-is-red-on-every-nightly.md @@ -18,7 +18,7 @@ console ends at the loader's `Applied 5483 relocations`, with `fsync-budget-spent` on the boot parameter line. It was green at 3f46a019 (run 36111884575). -It is green alone on a dev host at 635a9a14 (`cargo test --test toyos-build +It is green alone on a dev host at f231c43e (`cargo test --test toyos-build -- --nightly home_budget_refusal_retried` EXIT=0). `cargo run -- --known-red home_budget_refusal_retried` answers NO.