diff --git a/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md b/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md new file mode 100644 index 00000000000..4e389c509b5 --- /dev/null +++ b/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md @@ -0,0 +1,33 @@ +--- +status: expected-red +kind: defect +opened: 2026-09-28 +--- + +# `netd_refused_accept` hung waiting for a wake that never came + +Orchestrator Fast tier at PR #562's head `2a9c77ee`: `FAIL netd_refused_accept: +timed out after 2341s, with the guest still talking 8s ago`. The guest's last +line was `netd_refused_accept: waiting for a wake for the connection an accept +refused for room left, once room returned` — the `wake()` call at +`tests/toyos-rust-tests/src/bin/netd_refused_accept.rs:63`, blocked reading +`listener.notify`. #562 changes nothing this test runs: the guest binary, +netd, the netcase config and the harness function that drives it are +identical to main. + +Unconfirmed reading, not established from a kept console: the same run's +`the host's connections ended [Ok(585728), Ok(0), Ok(0), ...]` shows the +host's dial ending `Ok(585728)` rather than with the harness's 120 s +write-stall error. A reset would return the listener's socket to `Listen` +(`settle` in `userland/netd/src/listen.rs:73-85`, the `tcp::State::Closed` +arm re-`listen`s without producing an accept), after which netd owes the +test's `end(held.pop()...)` connection no wake. The guest console for this +run was not kept. + +Exit condition: the mechanism established from a kept console, fixed, and +`netd_refused_accept` green — which needs `netcase_against_host` +(`tests/toyos.rs:10113-10121`) to keep `result.serial` on its error path +instead of dropping it, since nothing else in the tree keeps this test's +console. + +**Owner**: whoever holds `issues/design-debt/toyos-has-its-own-network-stack.md`. diff --git a/src/redlist.rs b/src/redlist.rs index 88d243fd6fc..4fc2b50761a 100644 --- a/src/redlist.rs +++ b/src/redlist.rs @@ -56,6 +56,10 @@ pub const DISABLED: &[Disabled] = &[ test: "log_ring_keeps_the_owners_slots", issue: "issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md", }, + Disabled { + test: "netd_refused_accept", + issue: "issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md", + }, Disabled { test: "partition_claim_departure", issue: "issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md", diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 09e7bf0eeb9..e3c4ea6aeae 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -513,20 +513,6 @@ pub const GUEST_QUIET: Duration = Duration::from_secs(15); /// The compositor prints its interval line whatever else has stopped, so /// silence alone cannot end a desktop wait, and a suite that never ends is /// worse than one that reds. -/// -/// **Not [`budget`]-scaled, and that is the point of the pair.** Width is what a -/// ceiling on a *slow* guest has to be corrected for, and the silence bound -/// above is what a slow guest is now judged by — it keeps talking, so it is -/// never judged by this at all. What is left for this number to catch is a guest -/// that is stuck *and* chatty, which is a state the width does not produce; -/// scaling it would only make that state cost an hour at width 12. The longest -/// guest action any caller waits on is eight seconds of audio. -/// -/// It is also the real ceiling of any failing wait on a shared boot, because -/// the kernel's own 10 s cadence keeps the quiet clock above reset: a settle -/// predicate that could never come true was measured ending here at 302 s, -/// not at 15 (PR #96's verification). Price a new waiting check against this -/// number, not the one above. pub const GUEST_WEDGED: Duration = Duration::from_secs(300); pub fn guest_liveness() -> Liveness { @@ -657,10 +643,7 @@ impl std::fmt::Display for WaitVerdict { /// the instant it passed — so a merely-slow guest reported exactly what a wedged /// one did. `launcher_refusals` was killed at `192s "still talking 1s ago"` on a /// loaded `smp:2` runner its `vcpus/cores` factor clamps to 1, a guest making -/// steady progress called wedged by a clock. So a guest still *talking* is now -/// never ended by `ceiling`: the per-test budget bites only a guest that has -/// *also* gone quiet for [`GUEST_QUIET`], and a talking one runs to the -/// [`GUEST_WEDGED`] backstop below. +/// steady progress called wedged by a clock. /// /// **`elapsed > ceiling` stays a necessary condition, and that is what keeps /// this safe.** Silence alone is not a wedge on this suite's boots: a healthy @@ -815,8 +798,7 @@ pub fn ceiling_self_check() -> Result<(), String> { // all four directions. A talking guest past its budget is slow, not // wedged; a silent one within its budget is idle, not wedged; the wedge // guard still fires, and fast; and the backstop still catches a guest - // that talks forever. Staged with a ceiling below [`GUEST_WEDGED`] so the - // backstop is a distinct, higher number — the shape every real test has. + // that talks forever. const TIGHT: Duration = Duration::from_secs(153); let bstop = TIGHT.max(GUEST_WEDGED); assert!(TIGHT < bstop, "the case needs a ceiling below the backstop"); @@ -867,6 +849,19 @@ pub fn ceiling_self_check() -> Result<(), String> { )); } + // 3c. **The other side of `ceiling.max(GUEST_WEDGED)`**: a ceiling *above* + // `GUEST_WEDGED` must itself be the backstop, not get clamped down to + // the floor. `CEILING` (380 s, from case 1) is such a ceiling; a guest + // talking past `GUEST_WEDGED` (300 s) but still short of `CEILING` is + // not yet at its backstop and must run on. + if ceiling_verdict(None, GUEST_WEDGED + Duration::from_secs(50), CEILING, talking, 40).is_some() + { + return Err(String::from( + "a guest talking past GUEST_WEDGED but short of a higher ceiling was ended anyway — \ + the backstop did not follow a ceiling above GUEST_WEDGED", + )); + } + // 4. **What the verdict carries, which is the half that was missing.** Every // arm above names a death in one sentence; until 2026-08-18 that sentence // was the whole of what a failure arm had, and a `DOUBLE FAULT on CPU 1` @@ -3290,8 +3285,8 @@ impl QemuInstance { /// is the case whose paint "never arrived in the window" while the guest was /// alive — the budget-scaled deadline undercounts a later moment in the run /// exactly as the serial ceiling did. Only a screen *frozen* for - /// [`GUEST_QUIET`] past the deadline, or the [`GUEST_WEDGED`] backstop, ends - /// the wait; `done` firing ends it at once, so a passing caller is untouched + /// [`GUEST_QUIET`] past the deadline ends the wait; `done` firing ends it at + /// once, so a passing caller is untouched /// and a real bug (the paint that should not be there, and stays) still fires /// its assertion, a frozen-screen `GUEST_QUIET` later. ///