Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`.
4 changes: 4 additions & 0 deletions src/redlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 17 additions & 22 deletions tests/common/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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.
///
Expand Down
Loading