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/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/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/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/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/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..25d447d835b --- /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 f231c43e (`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. 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. 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/syscall/proc.rs b/kernel/src/syscall/proc.rs index 8f3d825ddcf..1fc6adcd132 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/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 { diff --git a/src/release.rs b/src/release.rs index d8f40c589b2..7b31cfa4763 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; use crate::toolchain::HOSTED_ARCH; @@ -131,7 +132,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 @@ -206,7 +208,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 29c8ab9606a..0ec13d44336 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -837,8 +837,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), ]; 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" 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}", 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{}",