diff --git a/issues/build/a-suites-first-run-after-the-fork-pin-moves-races-its-own-checkout.md b/issues/build/a-suites-first-run-after-the-fork-pin-moves-races-its-own-checkout.md new file mode 100644 index 00000000000..19139ad49dd --- /dev/null +++ b/issues/build/a-suites-first-run-after-the-fork-pin-moves-races-its-own-checkout.md @@ -0,0 +1,40 @@ +--- +status: open +kind: tooling +opened: 2026-10-03 +--- + +# A suite's first run after the fork pin moves races its own checkout + +`sysroot::fork_checkout` (`src/sysroot.rs`) moves a linked worktree's `rust/` +to the commit the tree pins when the checkout is behind it, with +`git checkout --detach`. The guest suite boots its tests on twelve threads and +each reaches `fork_checkout` on its own, so the first run after a merge that +moved the gitlink runs that checkout in every thread at once. The same unlocked +function reds a new worktree's first run while `git worktree add` is still +writing: +`issues/build/a-worktrees-first-wide-run-reds-on-the-fork-checkout-it-is-still-making.md`. + +## Measured + +A worktree at `bf28c1e38` whose `rust/` stood at `c4c65e3e8` under a pin of +`95960d6c2`, after a merge of `origin/main`: `cargo test --test toyos-build` +exited 1 with 18 of 26 red in 177 s. Every red is one of two sentences. One is +`git ["checkout", "--detach", "-q", "95960d6c2…"] in …/rust failed`, under +git's `Unable to create '…/modules/rust/worktrees/rust1/index.lock': File +exists`. The other is `…/rust is at c4c65e3e8… with uncommitted work`, from a +thread that read the checkout's status while another thread was moving it. The +checkout ended at the pin, and the next run of the same command did not repeat +either. + +## Owner + +The toolchain stage of +`issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md`. + +## What would close it + +One mover: the checkout is moved under a lock every thread of the process +takes, or once before the suite starts its workers, and +`cargo test --test toyos-build` on a worktree whose checkout is behind its pin +moves it and boots its tests. diff --git a/issues/build/hard-lockup-bound-ms-is-read-by-nothing-but-its-own-assertion.md b/issues/build/hard-lockup-bound-ms-is-read-by-nothing-but-its-own-assertion.md new file mode 100644 index 00000000000..b3b0e5edb03 --- /dev/null +++ b/issues/build/hard-lockup-bound-ms-is-read-by-nothing-but-its-own-assertion.md @@ -0,0 +1,28 @@ +--- +status: open +kind: tooling +opened: 2026-09-30 +--- + +# `toyos_tco::HARD_LOCKUP_BOUND_MS` is read by nothing but its own assertion + +`toyos-tco/src/lib.rs` declares `HARD_LOCKUP_BOUND_MS`, +`hard_lockup_bound_ms(WEDGE_BOUND_MS)`, as "the one a T14 boot runs under", +and the `const _` beside it is its one reader. Its last other reader was the +metal profile's list of declared ceilings, which went with +`tests/metal-profile.toml`. The T14's `hardlockup` arm no longer runs under it +either: `metal::bound_for` arms that image with `toyos_tco::STAGED_BOUND_MS`, so its +lockup bound is `hard_lockup_bound_ms(STAGED_BOUND_MS)`. + +`git grep HARD_LOCKUP_BOUND_MS -- ':!issues'` lists `toyos-tco/src/lib.rs` +alone. + +## Owner + +`issues/hardware/the-t14-boots-toyos-unattended.md`, the track that holds the +hard-lockup detector and its bound. + +## What would close it + +The constant and its assertion deleted, with whatever `toyos-tco`'s version +owes for it; `hard_lockup_bound_ms`'s own test already holds the half. diff --git a/issues/build/host-scale-reads-host-speed-off-boots-that-stop-at-different-markers.md b/issues/build/host-scale-reads-host-speed-off-boots-that-stop-at-different-markers.md new file mode 100644 index 00000000000..d4dfce6f0bc --- /dev/null +++ b/issues/build/host-scale-reads-host-speed-off-boots-that-stop-at-different-markers.md @@ -0,0 +1,44 @@ +--- +status: open +kind: tooling +opened: 2026-10-03 +--- + +# `host_scale` reads host speed off boots that stop at different markers + +`budget_smp` (`tests/common/qemu.rs`) pays a ceiling out for a slower host by +`host_scale`: the run's fastest boot against `REFERENCE_BOOT_MS`, 1424 ms. +`record_boot` takes each boot's time to its own ready marker, and those differ: +`virt_early_panic`'s and `virt_early_fault`'s is `EARLY PANIC:`, which the +kernel prints when it panics early in bring-up; the other AArch64 boots' is +`control registers: SCTLR_EL1=`; an x86-64 boot's is `===READY===`, after +userland starts. The boots also run under different accelerators +(`Profile::accel`). So the scale says which boots a run held, not how fast the +host is: a run with an early-panic boot in it pays 1× on nearly any host. With +the suite's width no longer multiplying a ceiling, `host_scale` and +`oversubscription` are the only host corrections `budget_smp` makes. + +## Measured + +On the dev host, an arm64 Mac, one run after the other at `e5daa61ff`, load +average 1.7 to 2.8: + +| command | the boot | its fastest boot | scale | +|---|---|---|---| +| `cargo test --test toyos-build -- virt_early_panic` | `Virt`, HVF, to `EARLY PANIC:` | 479 ms | 1.00× | +| `cargo test --test toyos-build -- virt_smp` | `VirtEl2`, TCG, to `SCTLR_EL1=` | 1180 ms | 1.00× | +| `cargo test --test toyos-build -- machine_shutdown` | x86-64, TCG, to `===READY===` | 3316 ms | 2.33× | + +The whole suite at `e5daa61ff`, 12 wide on the same host, read 566 ms and paid +1.00×. + +## Owner + +`issues/build/the-tooling-is-a-review-prompt-and-three-workflows.md`, whose +harness item keeps a piece of the harness only where it sees what reading +cannot. + +## What would close it + +`host_scale` reads only boots of one profile that stop at one marker, or it is +deleted with `budget_smp`'s promise of it. diff --git a/issues/build/run-test-and-its-ceiling-verdict-serve-only-debugs-run.md b/issues/build/run-test-and-its-ceiling-verdict-serve-only-debugs-run.md new file mode 100644 index 00000000000..085aff79e40 --- /dev/null +++ b/issues/build/run-test-and-its-ceiling-verdict-serve-only-debugs-run.md @@ -0,0 +1,36 @@ +--- +status: open +kind: tooling +opened: 2026-10-03 +--- + +# `run_test` and its ceiling verdict serve only `--debug`'s `run` + +`QemuInstance::run_test`, `run_test_hooked` and `run_test_paced` +(`tests/common/qemu.rs`), and `ceiling_verdict`, the verdict their read loop +ends a wait on, have one caller: interactive debug mode's `run ` +(`tests/toyos.rs`, `qemu.run_test(test_name, Duration::from_secs(60))`). Every +guest test of the suite waits through `await_guest`, `drain_until` or a +screendump wait instead. So the checks that hold `ceiling_verdict` to its arms +(`tests/checks/qemu.rs`, and `a_stall_stays_red` in `tests/checks.rs`) and the +`run_test` shape `driven_binaries` reads (`tests/checks.rs`) guard that one +path. + +## Measured + +`git grep -c 'run_test(\|run_test_hooked(\|run_test_paced(' -- tests/toyos.rs tests/common` +counts 40 lines in `tests/toyos.rs` and 34 across sixteen other `tests/common` +modules at `06788146b^`, and one in `tests/toyos.rs`, the debug loop's, at +`06788146b`, the commit that cut the guest suite; the five in +`tests/common/qemu.rs` are the three functions' own. + +## Owner + +`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`, whose +first cut left them that one caller. + +## What would close it + +`--debug`'s `run` reads its test through a wait the suite uses, and +`run_test`, `run_test_hooked`, `run_test_paced`, `ceiling_verdict` and the +checks and readers that exist for them are deleted. diff --git a/issues/build/the-talking-boots-reboot-outruns-its-log-stream.md b/issues/build/the-talking-boots-reboot-outruns-its-log-stream.md deleted file mode 100644 index 9f5aa185cc7..00000000000 --- a/issues/build/the-talking-boots-reboot-outruns-its-log-stream.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-29 ---- - -# The talking boot's `reboot` outruns its log stream, so `lan_talk` reds when the backlog has not crossed - -`metaltalk::converse` (`src/metaltalk.rs`) waits for the stream to connect, -pings, runs one command and fires `reboot`; nothing waits on the stream -carrying the lines `metaltalk::judge` then requires, this boot's -`Boot: complete` among them. `logd` serves the boot from its first line, so -the verdict is a race between the backlog crossing and the `reboot` the host -itself sends. - -## Measured - -The full T14 run of `main` at `7e151819` -(EXIT=1), boot `lantalkcase`: - -``` -toyos-metal: the boot did not say over its own cable what a talking boot owes: - 217 line(s) arrived over the cable and none is this boot's `Boot: complete` - FAIL lantalkcase: toyos-metal exited exit status: 1 - FAIL lan_talk: toyos-metal exited exit status: 1 -``` - -Its readback: -`talk.txt` says the ping, the command (`status 0`, 495 ms) and the `reboot` -all succeeded, with `talk_stream_end open`. `stream.log` is `kernel.log`'s -first 217 lines, ending at `0.493 ... xHCI: configuration set`; -`Boot: complete (1167ms)` is `kernel.log:289`. `kernel.log` has -`logd: serving this boot's log to 192.168.1.47:54752` at 20.996 s and init's -`power: the machine stops` at 21.591 s: the stream had 595 ms. - -## Exit condition - -`converse` fires `reboot` only once the stream has carried `Boot: complete` -or its bound has passed, and two consecutive T14 runs pass `lan_talk`; then this file is deleted. diff --git a/issues/diagnostics/netds-lease-probe-answers-a-question-its-lines-already-answer.md b/issues/diagnostics/netds-lease-probe-answers-a-question-its-lines-already-answer.md new file mode 100644 index 00000000000..ae672c12040 --- /dev/null +++ b/issues/diagnostics/netds-lease-probe-answers-a-question-its-lines-already-answer.md @@ -0,0 +1,36 @@ +--- +status: open +kind: tooling +opened: 2026-09-16 +--- + +# netd's lease probe answers a question its own lines already answer + +netd's `--exit-with-lease` brings the card up and serves exactly as the +shipping boot does for a bounded window, leaves `/log/lease.txt` on the log +volume one durable line at a time (the bring-up's own words, every link change, +the lease, what the driver and the MAC counted each way), and ends with +`toyos_i219::lease::Verdict`'s code. It existed because every line netd printed +about the same things was a console write, and on the T14 a console write +reached no file. Every program's lines reach `/log` through its log ring now, +and the talking boot's `lan_dhcp_lease` judge reads the lease off netd's own +lines. What still arms the probe is `tests/lanleasecase`, the `lan_lease_report` +metal row: a T14 flash of its own, because netd ends inside it. + +## Owner + +The I219 bring-up's author, and after it the network track. + +## What would close it + +The probe deleted: `tests/lanleasecase/system.toml`, its row and its gate in +`src/build.rs`, the `lan_lease_report` metal row in `tests/toyos.rs` with +`LANLEASECASE`, `lan::leased_on_metal`, `Readback::log_volume_file` and +`tests/common/volumes.rs`, netd's `--exit-with-lease` and +`userland/netd/src/report.rs`, `toyos-i219/src/lease.rs`'s report lines, and +`toyos_i219::phy::Outcome`, whose tests then assert the refusal itself. + +An earlier form of this arm, `--exit-with-phy-outcome` on `d409139f^:tests/lanphycase`, +ended right after the bring-up with the PHY's outcome; its codes are still +`Verdict::NotLeased`'s, so a code read off one of its boots means what +`toyos_i219::phy::Outcome` says. diff --git a/issues/diagnostics/the-cable-judge-reads-three-netd-records-that-cannot-arrive-on-the-t14.md b/issues/diagnostics/the-cable-judge-reads-three-netd-records-that-cannot-arrive-on-the-t14.md deleted file mode 100644 index bb3853a34b0..00000000000 --- a/issues/diagnostics/the-cable-judge-reads-three-netd-records-that-cannot-arrive-on-the-t14.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-13 ---- - -# The cable judge reads three netd records that cannot arrive on the T14 - -The cause was that userland stdout and stderr were console writes and `/log` -carried kernel records only; every program's lines reach `/log` now, through -its log ring (`issues/kernel/logging-records-from-every-producer-and-a-kernel-that-waits-on-nobody.md`). -This is what that cost the one judge written as if it were otherwise. - -`tests/common/lan.rs`'s `on_metal` refuses a boot that carries no `netd: MAC …`, -no `netd: … link up` and no `netd: ready` record, and judges -`lan.lancase.link_up_ms` and `lan.lancase.lease_ms` out of those lines. **It -cannot go green on the machine it was written for**, whatever the driver does. - -Metal run 36 held `tests/lancase` open for twenty seconds with netd alive and -driving the I219, and its readback carries exactly one line with `netd` in it — -the kernel's own `spawn:` record -(`/Users/jan/.claude/jobs/2280e09e/tmp/readbacks/lancase-placement-run36/kernel.log:314`): - -``` -$ grep -a -c 'netd' .../lancase-placement-run36/kernel.log -1 -``` - -The readings that do cross are the kernel's: the hand-over record, -`pcidev: slot N took its first message on vector 0xNN`, and the end-of-boot -`irq:` census. - -## What would close it - -Either §1 of the track above lands, or the judge stops reading netd's lines and -asks netd through its port — the `metalprobe` pattern, a job that exits with a -code encoding the link and the lease. The first is a kernel change and the -second is the harness's; neither is the I219 driver's. diff --git a/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md b/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md deleted file mode 100644 index 731723d4006..00000000000 --- a/issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-16 ---- - -# `tests/lanleasecase` is a third T14 flash, and it earns it only while netd's console line cannot cross - -`tests/lanleasecase` is `tests/lancase` with one `args` row — netd's -`--exit-with-lease`, under which netd brings the card up and serves exactly as -the shipping boot does for a bounded window, leaves `/log/lease.txt` on the log -volume one durable line at a time (the bring-up's own words, every link change, -the lease, what the driver and the MAC counted each way), and ends with -`toyos_i219::lease::Verdict`'s code. It exists because every line netd prints -about the same things is a console write, and on the T14 a console write -reaches no file -(`issues/diagnostics/the-cable-judge-reads-three-netd-records-that-cannot-arrive-on-the-t14.md`): -the kernel's `exit: netd pid=N code=N` record and a file netd writes itself are -the two words of a process that cross. It is the `lan_lease_report` metal row, -a third image flashed to the stick and a third boot of the machine. - -## Owner - -The I219 bring-up's author, and after it the network track. - -## What would close it - -Userland's lines reaching the stick, which they do through each program's log -ring (`issues/kernel/logging-records-from-every-producer-and-a-kernel-that-waits-on-nobody.md`), after -which the shipping `lancase` arm carries netd's own lines about the lease and -the probe answers a question already answered. Then the arm is: -`tests/lanleasecase/system.toml`, its row in `src/build.rs`'s `ALL_CONFIGS`, -the `lan_lease_report` metal row in `tests/toyos.rs` with `LANLEASECASE` and -`lan::leased_on_metal`, -`userland/netd/src/report.rs` and `toyos-i219/src/lease.rs`'s report lines — -and netd's `--exit-with-lease` with `tests/e1000leasecase` and the -`lan_lease_report` QEMU registration, the arm that proves the channel. - -An earlier form of this arm, `--exit-with-phy-outcome` on `d409139f^:tests/lanphycase`, -ended right after the bring-up with the PHY's outcome; its codes are still -`Verdict::NotLeased`'s, so a code read off one of its boots means what -`toyos_i219::phy::Outcome` says. diff --git a/issues/filesystem/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md b/issues/filesystem/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md new file mode 100644 index 00000000000..b9f870c0183 --- /dev/null +++ b/issues/filesystem/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md @@ -0,0 +1,34 @@ +--- +status: open +kind: defect +opened: 2026-09-30 +--- + +# Listing `/home` itself answers "entity not found", and the listing bound on a served directory has no guest test + +`readdir_bound` asserted that `read_dir("/home")` is refused once `/home` holds +more entries than the listing bound, and it accepted any error. Before the file +servers the refusal was the bound's; since them it is `NotFound`, whatever the +count: + +``` +634-whole.log (main before #536): [readdir]: /home refused at 32769 entries (out of memory) +536r21-whole.log (#536's branch): [readdir]: /home refused at 32769 entries (entity not found) +536r21-whole.log: [readdir]: /home's own root is still refused (entity not found) +``` + +So on `main` the arm created 32,769 files through the DATA server (364 s and +442 s of `readdir_bound` on #536's two whole runs, against 46-181 s before it) +to reach an answer an empty `/home` gives too. The arm is deleted; what is left +untested is the one refusal the DATA server has for a listing too large, the +`WINDOW_BYTES` check in `userland/fsd/src/main.rs`'s `READDIR`. + +## Owner + +The storage track, which moved `/home` behind the DATA server. + +## What would close it + +`read_dir("/home")` on a machine with a DATA volume answers `/home`'s entries, +and a guest test lists a served directory past `WINDOW_BYTES` and is refused +with `ResourceExhausted` by name rather than by any error. diff --git a/issues/filesystem/where-everything-lives.md b/issues/filesystem/where-everything-lives.md index 2695ecbc044..2dc667a356a 100644 --- a/issues/filesystem/where-everything-lives.md +++ b/issues/filesystem/where-everything-lives.md @@ -117,4 +117,4 @@ until that lands nothing verifies it. `--exit-with-lease` bench report the metal loop reads off the stick's FAT log volume, as it reads `/log/metal-*.bin`, and the DATA volume is not readable there. It goes with -`issues/diagnostics/the-lanleasecase-boot-is-a-third-t14-flash-for-one-exit-code.md`. +`issues/diagnostics/netds-lease-probe-answers-a-question-its-lines-already-answer.md`. diff --git a/issues/hardware/netds-i219-drops-a-transmit-burst-past-its-ring.md b/issues/hardware/netds-i219-drops-a-transmit-burst-past-its-ring.md new file mode 100644 index 00000000000..8f56254dea3 --- /dev/null +++ b/issues/hardware/netds-i219-drops-a-transmit-burst-past-its-ring.md @@ -0,0 +1,39 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# netd's I219 drops a transmit burst past its ring, and only a retransmit timeout recovers it + +`I219::tx` (`userland/netd/src/i219.rs`) writes a frame the driver has no +descriptor for into a scratch buffer and drops it: smoltcp's `TxToken` cannot +refuse. But `Device::transmit` can, by answering `None`, and then smoltcp keeps +the bytes in the socket and sends them on a later poll. netd's `transmit` +(`userland/netd/src/main.rs`) answers `Some` every time. `toyos_i219::TX_RING` +is 16, so 15 frames can be in flight. Every frame after those 15 in one poll is +lost, and TCP gets it back only when its retransmission timeout fires. For +smoltcp 0.12 that timeout starts at 700 ms (`RTTE_INITIAL_RTT` + +4 × `RTTE_INITIAL_DEV`). + +## Measured + +The T14 run of PR #609 at `7d2e15c0`, boot `lantalkcase`. `logd` admitted the +host's stream at 19.141 s. At 19.180 s netd said `9 frame(s) dropped with no +transmit descriptor free`, and `reboot` came at 19.403 s. The host's +`stream.log` is `kernel.log`'s first 217 lines, which is 21890 bytes, and +15 × 1460 = 21900 falls inside line 218. The run of `main` at `7e151819` +stopped at the same 217th line, with 595 ms between the admit and the stop. +Both windows are shorter than the first retransmit timeout. The same count appears +on the `lanswapcase` boot: 11, 18 and 23 frames dropped. + +## Owner + +Stage 2 of `issues/hardware/the-lan-is-not-yet-production-grade.md`, a gigabit +driver that holds. + +## Exit + +`transmit` answers `None` when no descriptor is free, and a descriptor coming +free wakes netd's poll. A host test pushes more than `TX_RING` frames' worth +through one poll and asserts that `tx_dropped` stays 0 and every byte leaves. diff --git a/issues/hardware/the-cable-judge-spends-two-premises-nothing-has-measured.md b/issues/hardware/the-cable-judge-spends-two-premises-nothing-has-measured.md index 9a682f4e52d..ac9f6844a72 100644 --- a/issues/hardware/the-cable-judge-spends-two-premises-nothing-has-measured.md +++ b/issues/hardware/the-cable-judge-spends-two-premises-nothing-has-measured.md @@ -23,8 +23,6 @@ judge refuses a boot whose leased address is not the one the loop pinged, and the one the loop pinged is what Ubuntu held on that MAC. A server that hands the MAC a different address under ToyOS reds the arm for a fact about the router rather than about the boot — a red naming the wrong thing, not a false green. -Closed by a lancase run whose lease record and whose `ping_addr` are compared, -which is the first thing that run prints. The router-lease measurement falsifies this premise: `issues/hardware/the-benchs-router-leases-toyos-another-address-than-ubuntu.md`. diff --git a/issues/hardware/the-lanicscase-boot-is-a-second-t14-flash-for-one-question.md b/issues/hardware/the-lanicscase-boot-is-a-second-t14-flash-for-one-question.md deleted file mode 100644 index 6f44546b60c..00000000000 --- a/issues/hardware/the-lanicscase-boot-is-a-second-t14-flash-for-one-question.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-13 ---- - -# `tests/lanicscase` is a second T14 flash, and it earns it only while the card is silent - -`tests/lanicscase` is `tests/lancase` with one `args` row — netd's -`--provoke-message`, which writes one enabled cause to `ICS` so the kernel's -`pcidev: slot N took its first message` record says whether delivery works at -all. It is the `lan_message_delivery` metal row, a second image flashed to the -stick and a second boot of the machine. - -**A count of no messages is two facts** — a part nothing made speak and a -message that reached no CPU — and only this arm separates them. On a card that -raises its own `LSC` it records exactly what `tests/lancase` records, so from -that moment it is a boot of the machine that answers a question already -answered. - -## Owner - -The I219 bring-up's author, and after it the network track. - -## What would close it - -The shipping `lancase` arm recording `pcidev: slot N took its first message` -without the actuator. Then the actuator has no question left and the arm is -four files: `tests/lanicscase/system.toml`, its row in `src/build.rs`'s -`ALL_CONFIGS`, `tests/toyos.rs`'s `lan_message_delivery` row, its `METAL_ONLY` -entry and `LANICSCASE` with the judge `lan::provoked_on_metal`. - -Metal run 57 recorded `pcidev: slot 0 took its first message on vector 0x28` -after the I219's hand-over on that slot and vector, so the second fact is read: -a message this function raises reaches a CPU. The arm stands until `lancase` records one without the actuator. diff --git a/issues/hardware/the-t14-stopped-answering-ssh-between-two-lan-boots.md b/issues/hardware/the-t14-stopped-answering-ssh-between-two-lan-boots.md index a3fa1ee4d41..c972207b023 100644 --- a/issues/hardware/the-t14-stopped-answering-ssh-between-two-lan-boots.md +++ b/issues/hardware/the-t14-stopped-answering-ssh-between-two-lan-boots.md @@ -6,10 +6,9 @@ opened: 2026-09-29 # The T14 stopped answering `ssh` between two LAN boots, and the next two arms died before their flash -`lan_lease_report` (boot `lanleasecase`) and `lan_swap` (boot `lanswapcase`) -are the two arms the metal suite flashes after `lanicscase`. On the run below -neither reached the stick: the first `ssh` of each timed out, so neither boot -happened and neither judge ran. The suite measured nothing about either test, +On the run below neither `lan_lease_report` (boot `lanleasecase`) nor `lan_swap` +(boot `lanswapcase`) reached the stick: the first `ssh` of each timed out, so +neither boot happened and neither judge ran. The suite measured nothing about either test, and why the bench's `ssh` went away is not established by anything kept. ## Measured diff --git a/issues/kernel/virt-mask-windows-read-nine-censuses-and-eight-windows-lines-for-one-cpu.md b/issues/kernel/virt-mask-windows-read-nine-censuses-and-eight-windows-lines-for-one-cpu.md new file mode 100644 index 00000000000..db4911a6be6 --- /dev/null +++ b/issues/kernel/virt-mask-windows-read-nine-censuses-and-eight-windows-lines-for-one-cpu.md @@ -0,0 +1,41 @@ +--- +status: open +kind: defect +opened: 2026-10-03 +--- + +# `virt_mask_windows` read nine census lines and eight windows lines for one CPU + +`virt_mask_windows` (`tests/toyos.rs`) judges the capture `judge_virt_job` +answers, which ends where `===TEST_END unmap_touch` arrived, with +`irqcensus::windows`: every `irq: cpu` census line has its CPU's windows line +beside it. + +## Measured + +A whole guest suite at `bf28c1e38`, whose `virt_mask_windows`, kernel and +`tests/common/irqcensus.rs` are `origin/main`'s at `c59e09ed6`: +`cargo test --test toyos-build`, 12 wide on the 14-core dev host at load +average 43 to 59, exited 1 with this one red in 66.8 s: + +``` +FAIL virt_mask_windows: census lines per cpu {0: 9, 1: 9, 2: 9, 3: 9, 4: 9, 5: 9, 6: 9, 7: 9} and windows lines per cpu {0: 9, 1: 9, 2: 9, 3: 9, 4: 9, 5: 9, 6: 9, 7: 8}: a census went out without its windows, or windows without their census + FAIL virt_mask_windows (10s) +``` + +The whole run before it and the one after it, same tree and same command, +passed it in 8 s and 7 s. The PL011 capture of the red was not kept: the run's +`target/red-run-serial` holds the x86-64 guests' 16550 logs only. + +Two candidates, neither measured: the capture ends at the marker with cpu7's +last windows line still behind it, or the kernel printed a census without its +windows. + +## Owner + +The windows instrument's author (`issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md`). + +## What would close it + +The cause, from a capture of a red: either the judge reads a capture that ends +on a whole report, or the kernel's report is shown to pair its two lines. diff --git a/src/bin/toyos-metal.rs b/src/bin/toyos-metal.rs index 362d20dc830..1d6b1710770 100644 --- a/src/bin/toyos-metal.rs +++ b/src/bin/toyos-metal.rs @@ -6,7 +6,7 @@ #[macro_use(eprintln)] extern crate toyos_build; -use toyos_build::metal::{run, Args}; +use toyos_build::metal::{run, Args, REFUSAL_HEAD}; fn main() { let words: Vec = std::env::args().skip(1).collect(); @@ -18,6 +18,6 @@ fn main() { } Err(refusal) => refusal, }; - eprintln!("toyos-metal: {refusal}"); + eprintln!("{REFUSAL_HEAD}{refusal}"); std::process::exit(if refusal.about_the_boot() { 1 } else { 2 }); } diff --git a/src/build.rs b/src/build.rs index 1c6fd4bf6ef..53d39c9d844 100644 --- a/src/build.rs +++ b/src/build.rs @@ -3103,8 +3103,6 @@ mod tests { "diag/system.toml", "console/system.toml", "tests/jobcase/system.toml", - "tests/lancase/system.toml", - "tests/lanicscase/system.toml", "tests/lanleasecase/system.toml", "tests/lantalkcase/system.toml", "tests/latencycase/system.toml", @@ -3276,12 +3274,10 @@ mod tests { assert!(one_claimant_per_device(&bad).is_err()); } - /// netd's two actuators that only its Intel driver answers, spelled here - /// and held to netd's own declarations by - /// [`netd_declares_the_flags_this_gate_spells`]. - const PROVOKE_MESSAGE: &str = "--provoke-message"; + /// netd's actuator that only its Intel driver answers, spelled here and + /// held to netd's own declaration by + /// [`netd_declares_the_flag_this_gate_spells`]. const EXIT_WITH_LEASE: &str = "--exit-with-lease"; - const INTEL_ACTUATORS: [&str; 2] = [PROVOKE_MESSAGE, EXIT_WITH_LEASE]; /// netd's main module, which is where both halves of this gate's spelling /// live: nothing links the two crates, so the build system reads the source. @@ -3295,15 +3291,13 @@ mod tests { /// build system, so the flags both ends spell are held to netd's own /// declarations by reading its source. #[test] - fn netd_declares_the_flags_this_gate_spells() { + fn netd_declares_the_flag_this_gate_spells() { let (at, source) = netd_source(); - for flag in INTEL_ACTUATORS { - assert!( - crate::bootlog::declares(&source, &format!("\"{flag}\"")), - "{} declares no constant equal to \"{flag}\"", - at.display() - ); - } + assert!( + crate::bootlog::declares(&source, &format!("\"{EXIT_WITH_LEASE}\"")), + "{} declares no constant equal to \"{EXIT_WITH_LEASE}\"", + at.display() + ); } /// The four hex digits after `key` on this line. @@ -3344,32 +3338,22 @@ mod tests { cards } - /// netd's two Intel-only actuators — `--provoke-message` writes - /// §10.2.4.4's `ICS`, and `--exit-with-lease` reports the Intel driver's - /// bring-up beside the lease — and virtio's driver has neither, so a boot - /// config that arms one on a card netd opens with any other driver is a boot - /// that panics instead of answering the question it was flashed for. One - /// that arms both on one program is refused too: the probe ends the process - /// before the point the other acts at. + /// netd's Intel-only actuators — `--exit-with-lease` reports the Intel + /// driver's bring-up beside the lease — and virtio's driver has none, so a + /// boot config that arms one on a card netd opens with any other driver is + /// a boot that panics instead of answering the question it was built for. fn an_armed_intel_actuator_claims_a_card_the_driver_opens( cfg: &SystemConfig, cards: &[String], ) -> Result<(), String> { for (name, prog) in &cfg.programs { - let armed: Vec<&str> = INTEL_ACTUATORS - .into_iter() - .filter(|flag| prog.args.iter().any(|arg| arg == flag)) - .collect(); - let [flag] = armed[..] else { - if armed.is_empty() { - continue; - } - return Err(format!("`{name}` is armed with {armed:?}, which cannot share a boot")); - }; + if !prog.args.iter().any(|arg| arg == EXIT_WITH_LEASE) { + continue; + } if !prog.devices.iter().any(|d| cards.contains(d)) { return Err(format!( - "`{name}` is armed with `{flag}` and claims {:?}, none of which is one of \ - the {cards:?} netd opens with that driver", + "`{name}` is armed with `{EXIT_WITH_LEASE}` and claims {:?}, none of which \ + is one of the {cards:?} netd opens with that driver", prog.devices )); } @@ -3382,24 +3366,20 @@ mod tests { let (_, source) = netd_source(); let cards = netd_intel_cards(&source); assert!(!cards.is_empty(), "netd's `CARDS` names no card its Intel driver opens"); - let mut armed = [0usize; INTEL_ACTUATORS.len()]; + let mut armed = 0; for cfg in ALL_CONFIGS { let config = load(cfg); - for (count, flag) in armed.iter_mut().zip(INTEL_ACTUATORS) { - *count += config - .programs - .values() - .filter(|p| p.args.iter().any(|arg| arg == flag)) - .count(); - } + armed += config + .programs + .values() + .filter(|p| p.args.iter().any(|arg| arg == EXIT_WITH_LEASE)) + .count(); an_armed_intel_actuator_claims_a_card_the_driver_opens(&config, &cards) .unwrap_or_else(|e| panic!("{cfg}: {e}")); } // A walk that reached no armed program passes on having found nothing, // which is the one way this gate can rot while every config still loads. - for (count, flag) in armed.iter().zip(INTEL_ACTUATORS) { - assert!(*count > 0, "no shipped boot config arms `{flag}` at all"); - } + assert!(armed > 0, "no shipped boot config arms `{EXIT_WITH_LEASE}` at all"); let armed_on = |device: &str, args: &str| { let cfg: SystemConfig = toml::from_str(&format!( "init = []\n[programs.netd]\ndevices = [\"{device}\"]\nargs = [{args}]\n" @@ -3410,18 +3390,10 @@ mod tests { // The card netd drives with the other driver, and an Intel function it // drives with none: a vendor id is not what gives a part an `ICS` or a // PHY behind `MDIC`. - for flag in INTEL_ACTUATORS { - let one = format!("\"{flag}\""); - assert!(armed_on("pci:1af4:1041", &one).is_err(), "{flag}"); - assert!(armed_on("pci:8086:1502", &one).is_err(), "{flag}"); - assert!(armed_on(&cards[0], &one).is_ok(), "{flag}"); - } - for (at, one) in INTEL_ACTUATORS.into_iter().enumerate() { - for other in &INTEL_ACTUATORS[at + 1..] { - let both = format!("\"{one}\", \"{other}\""); - assert!(armed_on(&cards[0], &both).is_err(), "{one} beside {other}"); - } - } + let flag = format!("\"{EXIT_WITH_LEASE}\""); + assert!(armed_on("pci:1af4:1041", &flag).is_err()); + assert!(armed_on("pci:8086:1502", &flag).is_err()); + assert!(armed_on(&cards[0], &flag).is_ok()); } /// A device name the ABI does not know renders fine and leaves init with a diff --git a/src/metal.rs b/src/metal.rs index f54dadccedf..5f6381b9c75 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -60,7 +60,8 @@ pub fn return_secs() -> u64 { longest.div_ceil(1_000) + RETURN_ALLOWANCE_SECS } -const POLL_SECS: u64 = 5; +/// How often the loop asks whether the machine is there. +const POLL: std::time::Duration = std::time::Duration::from_secs(1); const PING_EVERY_SECS: u64 = 1; @@ -100,6 +101,24 @@ const LOADER: &str = r"\EFI\BOOT\BOOTX64.EFI"; const LID_KEYS: &[&str] = &["HandleLidSwitch", "HandleLidSwitchExternalPower", "HandleLidSwitchDocked"]; +/// What `toyos-metal`'s last statement on its stderr opens with, past the +/// printer's stamp, when it refuses: the harness reads the refusal back by it, +/// to name it beside the exit status. +pub const REFUSAL_HEAD: &str = "toyos-metal: "; + +/// The refusal a `toyos-metal` run's stderr ends on, from its [`REFUSAL_HEAD`] +/// line to the end, or `None` where it said none. Only a statement's first line +/// carries the stamp, so only that line is read past it. +pub fn said_refusal(stderr: &str) -> Option { + let lines: Vec<&str> = stderr.lines().collect(); + let at = lines + .iter() + .rposition(|line| crate::printer::unstamped(line).starts_with(REFUSAL_HEAD))?; + let mut said = vec![&crate::printer::unstamped(lines[at])[REFUSAL_HEAD.len()..]]; + said.extend(&lines[at + 1..]); + Some(said.join("\n").trim_end().to_string()) +} + /// Every way this loop refuses, by name. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Refusal { @@ -833,8 +852,17 @@ pub const FOREIGN_RECORD_ARM: &str = "blackbox-foreign-identity"; /// Whether this image is armed to stop itself, and so owes a sealed record /// rather than `Rebooting.`. -pub fn stages_a_wedge(armed: &[String]) -> bool { - armed.iter().any(|name| WEDGE_ARMS.contains(&name.as_str())) +pub fn stages_a_wedge(armed: &[impl AsRef]) -> bool { + armed.iter().any(|name| WEDGE_ARMS.contains(&name.as_ref())) +} + +/// The `boot-deadline=` bound an image armed with `armed` carries. +pub fn bound_for(armed: &[impl AsRef]) -> u64 { + if stages_a_wedge(armed) { + toyos_tco::STAGED_BOUND_MS + } else { + toyos_tco::WEDGE_BOUND_MS + } } /// Whether this image is armed so the pass after its reset clears its record @@ -1384,15 +1412,16 @@ impl Driver { } } + /// [`wait_on`] the machine's `ssh`. fn wait(&self, secs: u64, what: &'static str, answering: bool) -> Result { - let began = std::time::Instant::now(); - while began.elapsed().as_secs() < secs { - std::thread::sleep(std::time::Duration::from_secs(POLL_SECS)); - if self.ssh("probing", "true").is_ok() == answering { - return Ok(began.elapsed().as_secs()); - } - } - Err(Refusal::Silent { what, secs }) + let host = self.target.host.as_str(); + wait_on( + secs, + what, + answering, + |within| port_accepts(host, crate::metaltalk::SSH_PORT, within), + || self.ssh("probing", "true").is_ok(), + ) } /// The loader's own file, and then everything `logd` wrote, in name order: @@ -1471,6 +1500,44 @@ impl Driver { } } +/// Poll once a [`POLL`] whether a port `accepts` a dial bounded by what it is +/// handed, and say how long the machine took to answer as asked. **Coming back +/// is `ssh` itself answering**: a port that accepts is only asked whether `ssh` +/// `answers`, since a listener can come up before the service behind it. +/// **Going down is a dial refused or unanswered for [`CONNECT_SECS`]**, what +/// `ssh`'s own connect is given: a SYN lost while Ubuntu is still up would +/// otherwise read as down, and the stick be read before ToyOS booted it. +fn wait_on( + secs: u64, + what: &'static str, + answering: bool, + mut accepts: impl FnMut(std::time::Duration) -> bool, + mut answers: impl FnMut() -> bool, +) -> Result { + let dial = if answering { POLL } else { std::time::Duration::from_secs(CONNECT_SECS) }; + let began = std::time::Instant::now(); + while began.elapsed().as_secs() < secs { + let next = std::time::Instant::now() + POLL; + let listening = accepts(dial); + let answered = listening && (!answering || answers()); + if answered == answering { + return Ok(began.elapsed().as_secs()); + } + std::thread::sleep(next.saturating_duration_since(std::time::Instant::now())); + } + Err(Refusal::Silent { what, secs }) +} + +/// Whether `host`'s `port` accepts a connection `within`; a name that does not +/// resolve is a machine that is not there. +fn port_accepts(host: &str, port: u16, within: std::time::Duration) -> bool { + use std::net::ToSocketAddrs; + let Ok(mut addrs) = (host, port).to_socket_addrs() else { + return false; + }; + addrs.any(|addr| std::net::TcpStream::connect_timeout(&addr, within).is_ok()) +} + fn unstarted(what: &str, e: &std::io::Error) -> Refusal { Refusal::Remote { what: what.to_string(), @@ -2519,6 +2586,49 @@ mod tests { } } + /// **A port that accepts is not a machine that came back**, and a machine + /// going down is its port refusing. + /// Staged through [`port_accepts`] on a listener of this host's own, which + /// accepts and never speaks `ssh`, and on port 0, which nothing listens on: + /// a port the listener let go of can be another socket's by the time it is + /// dialled. + #[test] + fn coming_back_is_ssh_answering_and_not_its_port() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener"); + let port = listener.local_addr().expect("its address").port(); + let accepts = |within| port_accepts("127.0.0.1", port, within); + let mut asked = 0; + let back = wait_on(1, "come back", true, accepts, || { + asked += 1; + false + }); + assert_eq!(back, Err(Refusal::Silent { what: "come back", secs: 1 })); + assert!(asked > 0, "`ssh` was never asked of a port that accepts"); + assert_eq!(wait_on(1, "come back", true, accepts, || true), Ok(0)); + let up = wait_on(1, "go down", false, accepts, || unreachable!("`ssh` asked of one going down")); + assert_eq!(up, Err(Refusal::Silent { what: "go down", secs: 1 })); + let refuses = |within| port_accepts("127.0.0.1", 0, within); + assert_eq!(wait_on(1, "go down", false, refuses, || unreachable!()), Ok(0)); + } + + /// **The harness names the refusal, not only the exit status.** The + /// stderr is the T14's `lantalkcase` run's, cargo's own lines included and + /// the driver's statement under the printer's stamp, and the refusal it ends + /// on is read back whole, every finding with it. + #[test] + fn a_refusal_is_read_back_off_the_drivers_stderr() { + let stderr = " Blocking waiting for file lock on package cache\n\ + \x20 Finished `dev` profile [optimized + debuginfo] target(s) in 0.19s\n\ + \x20 Running `target/debug/toyos-metal --image /Users/jan/Dev/jan/toyos-t14lan/target/metal/lantalkcase/image.img --readback /Users/jan/Dev/jan/toyos-t14lan/target/metal/lantalkcase --fat32-check --talk /Users/jan/Dev/jan/toyos-t14lan/target/metal/lantalkcase/ssh/id_ed25519`\n\ + 14:02:11 toyos-metal: the boot did not say over its own cable what a talking boot owes:\n\ + \x20 217 line(s) arrived over the cable and none is this boot's `Boot: complete`\n"; + let said = Refusal::Talk(vec![ + "217 line(s) arrived over the cable and none is this boot's `Boot: complete`".into(), + ]); + assert_eq!(said_refusal(stderr), Some(said.to_string())); + assert_eq!(said_refusal(&stderr[..stderr.find(REFUSAL_HEAD).unwrap()]), None, "cargo's lines are no refusal"); + } + /// **An image with no bound on its own boot never reaches the stick.** /// /// The case, measured twice: a kernel that hung after its job list, and a @@ -2712,6 +2822,7 @@ mod tests { assert!(flashable(arm), "{arm} reaches no stick"); assert_eq!(judge_arms(&[arm.to_string(), bound.clone()]), Ok(()), "{arm}"); assert!(stages_a_wedge(&[arm.to_string()]), "{arm}"); + assert_eq!(bound_for(&[arm]), toyos_tco::STAGED_BOUND_MS, "{arm}"); // And with no bound behind it, the sharpest refusal names it as the // wedge it is rather than as a plain image. assert_eq!( @@ -2720,8 +2831,10 @@ mod tests { "{arm}" ); } - // The negative half: an arm that stops nothing is not judged as one. + // The negative half: an arm that stops nothing is not judged as one, + // and keeps the bound a wedge nobody staged is ended by. assert!(!stages_a_wedge(&["watchdog".to_string(), bound])); + assert_eq!(bound_for(&["watchdog"]), toyos_tco::WEDGE_BOUND_MS); } /// A row for a name the kernel no longer declares is a ruling about diff --git a/src/metaltalk.rs b/src/metaltalk.rs index 2482d7e4214..802a6fee3a8 100644 --- a/src/metaltalk.rs +++ b/src/metaltalk.rs @@ -49,6 +49,12 @@ pub const SSH_PORT: u16 = 22; const PING_TRIES: u32 = 10; const PING_WAIT: Duration = Duration::from_secs(1); +/// How long the stream gets to carry this boot's `Boot: complete` before +/// `reboot` is asked regardless, and the judge reds on the record's absence. A +/// liveness bound, inside the boot's own `boot-deadline=` that `reboot` has to +/// beat. +const CARRIED_WAIT: Duration = Duration::from_secs(30); + /// How long one connect waits for the machine's answer to its SYN. const CONNECT_WAIT: Duration = Duration::from_secs(5); @@ -277,6 +283,12 @@ impl Stream { self.wait_until(by, |lines| lines.iter().any(|l| l.contains(needle)).then_some(())).is_some() } + /// This boot's `Boot: complete` in milliseconds, read as [`judge`] reads + /// it, once a line carries it, or `None` after `by`. + pub fn wait_for_boot(&self, by: Duration) -> Option { + self.wait_until(by, |lines| lines.iter().find_map(|line| crate::bootlog::boot_millis(line))) + } + /// The peer, once a connection has carried a line, or `None` after `by` /// or once the dial has given up. pub fn wait_connected(&self, by: Duration) -> Option { @@ -954,11 +966,30 @@ pub fn converse( Err(why) => println!(" talk: `{command}` was not answered: {why}"), } - let reboot = ssh.fire(ssh_at, REBOOT); + let reboot = hand_back(stream, began, CARRIED_WAIT, || ssh.fire(ssh_at, REBOOT)); println!(" talk: `{REBOOT}` {reboot:?}"); Ok(Conversation { peer, ping, exec, reboot, exec_ms, stream_end: stream.end() }) } +/// Ask for `reboot` through `fire` once the stream has carried this boot's +/// `Boot: complete`, or once `by` has passed without it. `reboot` ends the +/// stream the judge reads that record off, and `logd` serves the boot from its +/// first line, so the backlog crosses at the network's pace and not the +/// conversation's. +fn hand_back(stream: &Stream, began: Instant, by: Duration, fire: impl FnOnce() -> T) -> T { + match stream.wait_for_boot(by) { + Some(ms) => println!( + " talk: the stream carried `Boot: complete` ({ms} ms), {} ms after it opened", + began.elapsed().as_millis() + ), + None => println!( + " talk: the stream carried no `Boot: complete` in {} ms; `{REBOOT}` is asked regardless", + by.as_millis() + ), + } + fire() +} + /// The keys a conversation is written under, one ` ` per line, in /// the loop's readback beside the host's other facts. const PEER: &str = "talk_peer"; @@ -1439,6 +1470,39 @@ mod tests { ); } + /// **`reboot` is not asked while the stream is stalled short of + /// `Boot: complete`**: it waits out its bound, and the record ends the wait + /// as the judge reads it. + #[test] + fn the_hand_back_waits_for_the_boot_record_the_judge_reads() { + const STALLED_AT: [&str; 2] = [ + "[2026-09-29 14:01:34 0.493 cpu0] xHCI: mass storage iface=0 in=0x81/512 out=0x2/512", + "[2026-09-29 14:01:34 0.493 cpu0] xHCI: configuration set", + ]; + const RECORD: &str = "[2026-09-29 14:01:35 1.166 cpu0] Boot: complete (1166ms)"; + let dir = toyos_tmpdir::TempDir::new("metaltalk-carried"); + let server = TcpListener::bind("127.0.0.1:0").unwrap(); + let at = server.local_addr().unwrap(); + let stream = Stream::connect(Peer::At(at), &dir.join("s.log"), false, Duration::from_secs(5)) + .expect("a loopback reader"); + let (mut conn, _) = server.accept().unwrap(); + for line in STALLED_AT { + writeln!(conn, "{line}").unwrap(); + } + assert!(stream.wait_for("configuration set", Duration::from_secs(5)), "the stalled lines were not read"); + let good = heard(Ok(Exec { stdout: owed(), status: Some(0) }), Ok("accepted".into())); + let bound = Duration::from_millis(100); + let began = Instant::now(); + let (waited, lines) = hand_back(&stream, began, bound, || (began.elapsed(), stream.lines())); + assert!(waited >= bound, "`reboot` was asked {waited:?} into a stall bounded at {bound:?}"); + assert!(judge(&good, &lines).is_err(), "the judge and the wait disagree on the stall"); + + writeln!(conn, "{RECORD}").unwrap(); + let lines = hand_back(&stream, Instant::now(), Duration::from_secs(5), || stream.lines()); + judge(&good, &lines).expect("`reboot` was asked before the stream carried the record"); + assert_eq!(stream.wait_for_boot(Duration::ZERO), Some(1166)); + } + /// The next connection `server` takes, or a panic naming `what` once a /// bound has passed with none: a reader that stopped dialing fails the test /// rather than hanging it. diff --git a/tests/checks.rs b/tests/checks.rs index be6246558e0..5139e977a76 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -157,8 +157,9 @@ mod checks { // gate asserting against a string nothing produces any more. let real = format!("{STALLED} waiting for the long tone to start — it went quiet"); let under_a_sentence = format!("the compositor stopped painting\n{real}"); - let past = qemu::GUEST_WEDGED + Duration::from_secs(1); - let backstop = qemu::ceiling_verdict(None, past, qemu::GUEST_WEDGED, Duration::from_secs(1), 900) + let ceiling = Duration::from_secs(30); + let past = ceiling * 2 + Duration::from_secs(1); + let backstop = qemu::ceiling_verdict(None, past, ceiling, Duration::from_secs(1), 900) .ok_or("a guest talking past the backstop was given no verdict")?; let cases: [(&str, Option<&str>, bool); 5] = [ ("an ordinary red", Some("the pointer never moved right"), false), diff --git a/tests/checks/qemu.rs b/tests/checks/qemu.rs index cbf76c6ecbe..760b4c6306e 100644 --- a/tests/checks/qemu.rs +++ b/tests/checks/qemu.rs @@ -32,8 +32,7 @@ pub fn host_scale_self_check() -> Result<(), String> { } // Finite in the worst case the suite can reach: eight vCPUs on a single // core is 8x, not unbounded — so a genuine hang still reports in bounded - // time. `budget_smp` composes this with `budget`'s own capped host_scale - // (<=8x) and the run's width, and on the `--jobs 1` runner width is 1. + // time. `budget_smp` composes this with the capped host_scale (<=8x). if oversub_ratio(8, 1) != (8, 1) { return Err(format!("the worst suite case must stay finite at 8x, got {:?}", oversub_ratio(8, 1))); } @@ -132,9 +131,9 @@ pub fn ceiling_self_check() -> Result<(), String> { if !stall.starts_with(STALLED) { return Err(format!("a genuine stall stopped reporting as one: {stall}")); } - // And the other end of the same guard: a guest still talking at the ceiling - // was working, and that is a different red. - let Some(slow) = ceiling_verdict(None, CEILING + Duration::from_secs(1), CEILING, talking, 900) + // And the other end of the same guard: a guest still talking at twice the + // ceiling was working, and that is a different red. + let Some(slow) = ceiling_verdict(None, CEILING * 2 + Duration::from_secs(1), CEILING, talking, 900) else { return Err(String::from("an expired guard on a talking guest returned no verdict")); }; @@ -152,8 +151,7 @@ pub fn ceiling_self_check() -> Result<(), String> { // guard still fires, and fast; and the backstop still catches a guest // 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"); + let bstop = TIGHT * 2; // (a) Past its 153 s budget, but talking — no verdict, it runs on. if ceiling_verdict(None, Duration::from_secs(192), TIGHT, Duration::from_secs(1), 500).is_some() { @@ -163,7 +161,8 @@ pub fn ceiling_self_check() -> Result<(), String> { )); } // (b) The backstop still bites a guest that is stuck *and* chatty: past - // `GUEST_WEDGED`, still talking, it is the one thing silence cannot catch. + // twice its ceiling, still talking, it is the one thing silence cannot + // catch. let Some(forever) = ceiling_verdict( None, bstop + Duration::from_secs(1), @@ -178,7 +177,7 @@ pub fn ceiling_self_check() -> Result<(), String> { } // (c) Negative control — the wedge guard still fires, and *fast*: a guest // silent past its budget is caught the moment it passes, at 154 s, not - // held to the 300 s backstop. + // held to the backstop. let Some(wedged) = ceiling_verdict(None, TIGHT + Duration::from_secs(1), TIGHT, GUEST_QUIET, 40) else { return Err(String::from( @@ -198,18 +197,22 @@ pub fn ceiling_self_check() -> Result<(), String> { would red healthy", )); } - - // 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", - )); + const SHORT: Duration = Duration::from_secs(5); + let every = || (0..).map(|n| VERDICT_POLL * n); + let first = |dying: Option<&str>, from: Duration| { + every().find_map(|quiet| ceiling_verdict(dying, from + quiet, SHORT, quiet, 40)) + }; + for since in every().take_while(|&t| t <= SHORT) { + let got = first(None, since); + if !got.as_deref().is_some_and(|v| v.starts_with(STALLED)) { + return Err(format!("silent from {since:?} under a {SHORT:?} ceiling: {got:?}")); + } + } + for died in every().take_while(|&t| t <= SHORT * 4) { + let got = first(Some(KERNEL), died); + if !got.as_deref().is_some_and(|v| v.contains("kernel panic")) { + return Err(format!("a kernel death at {died:?} under a {SHORT:?} ceiling: {got:?}")); + } } // 4. **What the verdict carries, which is the half that was missing.** Every diff --git a/tests/common/lan.rs b/tests/common/lan.rs index 936f2eab376..052e9eeff62 100644 --- a/tests/common/lan.rs +++ b/tests/common/lan.rs @@ -15,20 +15,10 @@ use toyos_i219::lease::{self, Event, Verdict}; use super::metal; -pub const CONFIG: &str = "tests/lancase"; -pub const BOOT: &str = "lancase"; - -/// The same boot with netd's `--provoke-message` armed: the arm that says -/// whether a message the card raises reaches a CPU at all, which no reading of -/// the shipping boot separates from a card that raised none. -pub const ICS_CONFIG: &str = "tests/lanicscase"; -pub const ICS_BOOT: &str = "lanicscase"; - -/// The same boot with netd's `--exit-with-lease` armed: netd brings the card up -/// and serves as that boot does, leaves [`LEASE_FILE`] on the log volume one -/// durable line at a time, and ends with the lease's verdict as its exit code, -/// which the kernel's `exit:` record carries off a machine whose console -/// reaches nobody. +/// The boot with netd's `--exit-with-lease` armed: netd brings the card up and +/// serves, leaves [`LEASE_FILE`] on the log volume one durable line at a time, +/// and ends with the lease's verdict as its exit code, which the kernel's +/// `exit:` record carries off a machine whose console reaches nobody. pub const LEASE_CONFIG: &str = "tests/lanleasecase"; pub const LEASE_BOOT: &str = "lanleasecase"; @@ -52,10 +42,9 @@ pub const TALK_BOOT: &str = "lantalkcase"; const TALK_HOLD: &str = "test_rs_lan_talk_hold"; pub const TALK_JOBS: &[&str] = &[TALK_HOLD]; -/// The armed boot's judge: the kernel's own records, tied to the I219's -/// hand-over, say whether a message it raised reached a CPU — whatever the PHY -/// did about a link. -pub fn provoked_on_metal(back: &metal::Readback) -> Result<(), String> { +/// The kernel's own records, tied to the I219's hand-over, say a message it +/// raised reached a CPU — whatever the PHY did about a link. +pub fn delivered_on_metal(back: &metal::Readback) -> Result<(), String> { let got = toyos_build::lan::delivered(back.kernel().text())?; eprintln!(" [lan] {}", got.handed.trim()); eprintln!(" [lan] {}", got.took.trim()); @@ -91,7 +80,7 @@ pub fn on_metal(back: &metal::Readback) -> Result<(), String> { Some(line) => format!("the kernel refused this function: {}", line.trim()), None => format!( "no `{handed}` record and no refusal either: nothing on this machine claimed \ - {ID}, so `tests/lancase` was flashed onto a machine that has no such card" + {ID}, so `tests/lantalkcase` was flashed onto a machine that has no such card" ), }), } @@ -154,10 +143,6 @@ pub fn on_metal(back: &metal::Readback) -> Result<(), String> { )), } - if let Err(why) = back.job_passed(JOBS[0]) { - bad.push(why); - } - if bad.is_empty() { return Ok(()); } diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 5ca51ef4a4b..aadefabdf8a 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -16,9 +16,10 @@ use std::cell::RefCell; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet}; +use std::io::{BufRead, BufReader}; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use toyos_build::bootlog; use toyos_build::metalimage; @@ -115,6 +116,13 @@ pub struct SharedBoot { pub links: Vec<(String, String)>, } +/// How many members allowed `allowance_ms` each one shared boot holds: the +/// runner's [`toyos_tco::JOB_BOUND_MS`] less a tenth of it, shared among them. +pub const fn members_fitting(allowance_ms: u64) -> NonZeroUsize { + let members = (toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10) / allowance_ms; + NonZeroUsize::new(members as usize).expect("a chunk holds a member") +} + /// The name of one chunk of a boot that had to be cut in two. fn chunk_name(boot: &str, index: usize) -> String { if index == 0 { @@ -739,7 +747,11 @@ fn build( field on an arm" )); } - let deadline = format!("{}{}", toyos_tco::DEADLINE_PARAM, toyos_tco::WEDGE_BOUND_MS); + let deadline = format!( + "{}{}", + toyos_tco::DEADLINE_PARAM, + toyos_build::metal::bound_for(&batch.params) + ); let mut params: Vec<&str> = batch.params.clone(); params.push(&deadline); // **A talking boot carries the key the loop will offer**, @@ -795,6 +807,33 @@ fn invocation(image: &Path, home: &Path, nic: Option<&str>, talk: bool) -> Vec Result<(), String> { + let mut child = Command::new("cargo") + .args(words) + .current_dir(root) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("toyos-metal could not be started: {e}"))?; + let mut stderr = BufReader::new(child.stderr.take().expect("stderr was asked for piped")); + let (mut said, mut line) = (String::new(), Vec::new()); + while stderr.read_until(b'\n', &mut line).map_err(|e| format!("toyos-metal's stderr: {e}"))? > 0 { + let text = String::from_utf8_lossy(&line); + eprint!("{text}"); + said.push_str(&text); + line.clear(); + } + let status = child.wait().map_err(|e| format!("toyos-metal: {e}"))?; + if status.success() { + return Ok(()); + } + Err(match toyos_build::metal::said_refusal(&said) { + Some(refusal) => format!("toyos-metal exited {status}: {refusal}"), + None => format!("toyos-metal exited {status} and said no refusal"), + }) +} + pub fn read_readback(dir: &Path, label: &str) -> Result { let home = at(dir, label); let read = |name: &str| -> Result { @@ -954,15 +993,8 @@ pub fn run( for (label, image) in &images { let words = invocation(image, &at(dir, label), batches[*label].nic, batches[*label].talk); eprintln!("[metal] {label}: cargo {}", words.join(" ")); - let booted = Command::new("cargo").args(&words).current_dir(&root).status(); - match booted { - Ok(status) if status.success() => {} - Ok(status) => { - refused.insert(label, format!("toyos-metal exited {status}")); - } - Err(e) => { - refused.insert(label, format!("toyos-metal could not be started: {e}")); - } + if let Err(why) = drive(&root, &words) { + refused.insert(label, why); } } } diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 4abf78d6323..0b7917e87d4 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -161,43 +161,6 @@ pub fn boot_census() -> (u32, u32, Vec) { pub const DECLARED_KERNEL_BUILDS: [&str; 3] = toyos_build::build::TEST_SUITE_KERNEL_BUILDS; -/// How many guests the run may have up at once. -/// -/// The harness's own wall-clock margins are margins on the *host*, and they were -/// all derived when one guest had it to itself. Four guests is a different -/// machine, so such a margin has to be stated against the regime it runs in -/// rather than widened outright — which is what this multiplies. -static WIDTH: AtomicU32 = AtomicU32::new(1); - -pub fn set_width(width: u32) { - assert!(width >= 1, "a run boots at least one guest"); - WIDTH.store(width, Ordering::SeqCst); -} - -/// A liveness ceiling, stated for one guest and paid out for the run's width. -/// -/// Every timeout a test hands [`QemuInstance::run_test`] and its relatives is a -/// guard against a wedge, never a verdict: the assertion is what the guest -/// *said*, and a test whose pass depended on a deadline expiring would be -/// asserting on the host's clock. So the number in the source stays the number -/// its author reasoned about — one guest, this host — and the width multiplies -/// it, as `wait_for_ready` multiplies the boot timeout. -/// -/// The cost of getting this wrong in the generous direction is that a wedge -/// takes longer to report. The cost in the other direction is a red run that -/// says a guest hung when it was only sharing a machine. -/// -/// This corrects for width and for how fast the host is, both host-wide facts. -/// It does not correct for a guest being wider than the host — an `smp:8` guest -/// on a four-core runner is oversubscribed and a mostly-serial boot never -/// showed it — which is [`budget_smp`]'s job and [`QemuInstance::budget`]'s -/// default. Callers that hold a guest want that one, so its ceiling reflects -/// the vCPUs it actually asked for. -pub fn budget(one_guest: Duration) -> Duration { - let (num, den) = host_scale(); - one_guest * WIDTH.load(Ordering::SeqCst) * num / den -} - /// The fastest boot-to-ready this run has seen, in milliseconds. /// /// A boot is the one piece of guest work every test does and no test asserts on @@ -208,35 +171,22 @@ pub fn budget(one_guest: Duration) -> Duration { /// is the closest this can get to the machine with nothing else on it. static FASTEST_BOOT_MS: AtomicU32 = AtomicU32::new(u32::MAX); -/// The same measurement on the host every ceiling in this tree was written for. -/// -/// Dev host, M4 Pro, cross-arch TCG, measured 2026-08-08: the fastest of ten -/// boots at `--jobs 1` was 1433 ms against a 1433–2063 ms spread, and the -/// fastest of a whole 291-test suite at width 12 was this. The smaller of the -/// two is the one to hold, because the factor only widens and a reference set -/// too high is a correction that does not happen. -const REFERENCE_BOOT_MS: u32 = 1320; +/// The fastest boot of the host the ceilings were measured on: at or above the +/// fastest boot of every run a ceiling in this tree was measured in, so that +/// host pays them at 1×. +const REFERENCE_BOOT_MS: u32 = 1424; fn record_boot(took: Duration) { let ms = took.as_millis().min(u32::MAX as u128) as u32; FASTEST_BOOT_MS.fetch_min(ms, Ordering::SeqCst); } -/// How much slower than the host these ceilings were written on this one is, as +/// How much slower than the host these ceilings were measured on this one is, as /// a fraction so that a 1.4× host is not rounded to 1. /// -/// [`budget`] corrects a ceiling for how many guests share the machine. It never -/// corrected for how fast the machine *is*, and that is the other half of the -/// same mistake: a number reasoned about on an M4 Pro is not a liveness ceiling -/// on a four-core Azure vCPU, it is a verdict about which of the two is running -/// the test. 307 bare timeouts were counted in one CI run and every one of -/// them was that. -/// -/// **Only ever upward.** On a faster host the number in the source stands, -/// because it is the number its author reasoned about, and a ceiling that shrank -/// would start reporting wedges that are not there. The ceiling of 8 is because -/// one anomalous boot must not be able to disable every liveness guard in the -/// suite at once. +/// **Only ever upward**, since a ceiling that shrank would report wedges that +/// are not there; and at most 8×, so one anomalous boot cannot disable every +/// guard in the suite at once. fn host_scale() -> (u32, u32) { let fastest = FASTEST_BOOT_MS.load(Ordering::SeqCst); // Before the first boot there is no measurement, and the sentinel must not @@ -317,28 +267,27 @@ fn oversubscription(smp: u32) -> (u32, u32) { oversub_ratio(smp, host_cores()) } -/// [`budget`] widened by a guest's own vCPU oversubscription. +/// A ceiling, paid out for the host and the guest it bounds. +/// +/// **Every ceiling in this suite is at most three times the slowest the test it +/// bounds was measured to take**, in whole-suite runs on the host [`REFERENCE_BOOT_MS`] +/// describes, at the suite's default width — so a test's time already carries +/// the guests it shares that host with. A wait is bounded by that multiple of +/// its whole test, the one number measured. The source states that number, and +/// this pays it out on a slower host ([`host_scale`]) and for a guest wider than +/// this one ([`oversubscription`]), and for nothing else. /// -/// The guest-agnostic [`budget`] scales by the run's width and boot-derived host -/// speed; this multiplies in `smp/cores` on top, so a wide-SMP guest that a -/// mostly-serial boot said little about is given the extra room the derivation -/// above says it needs. `smp <= cores` leaves it exactly [`budget`], which is -/// every guest on the dev host. -pub fn budget_smp(one_guest: Duration, smp: u32) -> Duration { +/// A ceiling is a guard against a wedge and never a verdict: the assertion is +/// what the guest *said*. +pub fn budget_smp(ceiling: Duration, smp: u32) -> Duration { + let (num, den) = host_scale(); let (onum, oden) = oversubscription(smp); - budget(one_guest) * onum / oden + ceiling * num / den * onum / oden } /// A liveness guard that watches the guest instead of the host's clock. /// -/// [`budget`] corrects a ceiling for how many guests share the machine, which -/// is the part of "how fast is the host today" the harness knows. It does not -/// know the rest, and a retry loop bounded by elapsed time has that ceiling for -/// a *verdict* the moment the rest moves: a guest that is merely late reports -/// exactly what a wedged one reports. -/// -/// The two are distinguishable and the console is what distinguishes them: a -/// guest still printing is a guest still working. So the ceiling here is time in +/// A guest still printing is a guest still working. So the ceiling here is time in /// which **nothing arrived**, and a guest that keeps talking is given as long as /// it needs. That is the whole idea — no number in this type is a statement /// about the host. @@ -400,8 +349,8 @@ impl Liveness { /// second producer: a test's own ceiling is a guard of exactly this kind. pub const STALLED: &str = "STALLED:"; -/// The backstop's red: a guest still talking past [`GUEST_WEDGED`] that never -/// finished. The ceiling too, and counted with [`STALLED`]'s. +/// The backstop's red: a guest still talking that never finished. The ceiling +/// too, and counted with [`STALLED`]'s. pub const TIMED_OUT: &str = "timed out after"; /// How long a guest may say nothing before a wait on it is a stall. @@ -422,12 +371,9 @@ 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. -pub const GUEST_WEDGED: Duration = Duration::from_secs(300); - -pub fn guest_liveness() -> Liveness { - Liveness::new(GUEST_QUIET, GUEST_WEDGED) -} +/// worse than one that reds. [`budget_smp`]'s rule over the tests that wait +/// through [`await_guest`]. +pub const GUEST_WEDGED: Duration = Duration::from_secs(141); /// A kernel line without its `[kernel cpu] ` stamp. fn without_stamp(line: &str) -> &str { @@ -523,6 +469,10 @@ impl std::fmt::Display for WaitVerdict { } } +/// How often a test's read loop asks [`ceiling_verdict`] about a guest that +/// has said nothing since it last asked. +pub const VERDICT_POLL: Duration = Duration::from_millis(100); + /// What a test's ceiling caught — the panic, the stall, or the slow test. /// /// `dying` is the line on which the kernel said it was dying, if it ever did, @@ -568,14 +518,14 @@ pub fn ceiling_verdict( ceiling.as_secs() )); } - // The absolute backstop, for a guest that is stuck *and* chatty and so never - // trips the silence guard — a suite that never ends is worse than one that - // reds. Never below the per-test ceiling, so a long test whose own budget - // already exceeds it is not cut short; never below [`GUEST_WEDGED`], the - // vetted stuck-and-chatty number a talking guest is judged by everywhere - // else. Not itself oversubscription-scaled — `ceiling` already carries that. - let backstop = ceiling.max(GUEST_WEDGED); + // For a guest that is stuck *and* chatty and so never trips the silence + // guard; never before a guest silent since `ceiling` has been silent for + // [`GUEST_QUIET`]. + let backstop = (ceiling * 2).max(ceiling + GUEST_QUIET); if elapsed > backstop { + if let Some(line) = dying { + return Some(kernel_died_here(line)); + } return Some(format!( "{TIMED_OUT} {}s, with the guest still talking {quiet:.0?} ago ({lines} \ console line(s) while it ran) — it was working and did not finish", @@ -606,7 +556,7 @@ pub fn await_guest( ) -> Result<(), String> { // Where this wait's own evidence starts. let from = log.len(); - let mut live = guest_liveness(); + let mut live = Liveness::new(GUEST_QUIET, qemu.budget(GUEST_WEDGED)); while !done(log) && live.working(log) { let more = qemu.drain_serial(Duration::from_millis(200)); log.push_str(&more); @@ -1471,15 +1421,14 @@ impl QemuInstance { } /// [`budget_smp`] for a host-side wait on this guest's own vCPU count. - pub fn budget(&self, one_guest: Duration) -> Duration { - budget_smp(one_guest, self.smp) + pub fn budget(&self, ceiling: Duration) -> Duration { + budget_smp(ceiling, self.smp) } /// Keep collecting serial output for `dur` after a test has returned. - /// **Not scaled by the width**, and it is the one duration in this file that - /// is not. Callers use it to *pace* — "let the guest run for 400 ms and tell - /// me what it said" — so multiplying it does not buy a slow guest more room, - /// it buys the test a longer sleep. + /// **Not a ceiling, and not paid out as one**: callers use it to *pace* — + /// "let the guest run for 400 ms and tell me what it said" — so scaling it + /// would buy a test a longer sleep and a slow guest nothing. pub fn drain_serial(&mut self, dur: Duration) -> String { self.drain_for(dur, |_| false) } @@ -1623,7 +1572,7 @@ impl QemuInstance { }; } - match self.rx.recv_timeout(Duration::from_millis(100)) { + match self.rx.recv_timeout(VERDICT_POLL) { Ok(line) => { last_line = Instant::now(); lines += 1; @@ -2363,6 +2312,10 @@ fn publish_line( tx.send(line).is_ok() } +/// A boot's ceiling: [`budget_smp`]'s rule over the tests that are one boot and +/// nothing after it. +pub const BOOT_CEILING: Duration = Duration::from_secs(63); + /// Returns every line seen on the way to the marker — see [`QemuInstance::boot_log`]. fn wait_for_ready( child: &mut Child, @@ -2373,30 +2326,7 @@ fn wait_for_ready( let no_timeout = options.debug_wait; let ready = options.ready_marker; let panic_aborts = ready == DEFAULT_READY; - // Ten seconds per guest this run may have up, and never fewer than two - // guests' worth — the tree runs 15-25 suites a day across several agents, - // so one guest on a quiet host stopped being - // the regime some time before this did. Measured on 2026-08-03 with other - // agents building: two boots exceeded the flat ten seconds, one of them in a - // run of a single guest. - // - // A wedge costs that much longer to report and nothing else. - // - // Scaled by the host too, and the first boot of a run is the one that - // cannot be: nothing has been measured yet, so it gets the flat number and - // every boot after it gets the corrected one. Two boot timeouts in CI run - // `31233476555` were this — `console: ready` and `compositor: ready`, on a - // runner where the same boots take twice what they take here. - // - // And by this guest's own oversubscription: an `smp:8` guest brings up all - // eight vCPUs during boot, so on the four-core runner even the boot is - // `8/4` oversubscribed, which the boot-derived `host_scale` cannot fold in - // because it *is* what boot measured. `oversubscription` says why in terms - // of `vcpus/cores`; on a host with a core per vCPU it multiplies by one. - let (num, den) = host_scale(); - let (onum, oden) = oversubscription(options.smp); - let boot_timeout = - Duration::from_secs(10) * WIDTH.load(Ordering::SeqCst).max(2) * num / den * onum / oden; + let boot_timeout = budget_smp(BOOT_CEILING, options.smp); let start = Instant::now(); let mut seen = String::new(); loop { diff --git a/tests/lancase/system.toml b/tests/lancase/system.toml deleted file mode 100644 index b469a5032a4..00000000000 --- a/tests/lancase/system.toml +++ /dev/null @@ -1,40 +0,0 @@ -# The one boot that runs netd in front of the ThinkPad T14's own NIC. -# -# A directory of its own for the reason `tests/e1000case` is one: a program that -# names a card the machine does not have costs an `init:` refusal line on every -# boot of the config that does, and no machine has both. -# -# Nothing here reaches the internal NVMe. There is no `[disks]` row, and the -# only device any program on this boot claims is the PCI function named below. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:15fc"] - -[programs.test-runner] -syscap = ["logread"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/lanicscase/system.toml b/tests/lanicscase/system.toml deleted file mode 100644 index 6e17dd2b1a6..00000000000 --- a/tests/lanicscase/system.toml +++ /dev/null @@ -1,42 +0,0 @@ -# `tests/lancase` with netd's delivery actuator armed, and nothing else. -# -# A boot whose only reading of the interrupt path is a count of messages cannot -# tell a part nothing made speak from a message that reached no CPU. The -# shipping config next door arms none of it, so its own record still means the -# card spoke on its own. -# -# Nothing here reaches the internal NVMe. There is no `[disks]` row, and the -# only device any program on this boot claims is the PCI function named below. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:15fc"] -args = ["--provoke-message"] - -[programs.test-runner] -syscap = ["logread"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/lanleasecase/system.toml b/tests/lanleasecase/system.toml index 436ae50ffa3..8222b3175ba 100644 --- a/tests/lanleasecase/system.toml +++ b/tests/lanleasecase/system.toml @@ -1,10 +1,9 @@ -# `tests/lancase` with netd's lease probe armed, and nothing else: netd brings -# the card up and serves exactly as that boot does, leaves what happened on the -# log volume one durable line at a time, and ends with the lease's verdict as -# its exit code — which the kernel's `exit:` record carries off a machine whose -# console reaches nobody. Like `tests/lancase`, nothing here reaches the -# internal NVMe: no `[disks]` row, and the only device any program claims is -# the PCI function named below. +# netd in front of the ThinkPad T14's own NIC with its lease probe armed: netd +# brings the card up and serves, leaves what happened on the log volume one +# durable line at a time, and ends with the lease's verdict as its exit code — +# which the kernel's `exit:` record carries off a machine whose console reaches +# nobody. Nothing here reaches the internal NVMe: no `[disks]` row, and the +# only device any program claims is the PCI function named below. [boot] start = ["logd", "blockd", "fsd", "netd", "test-runner"] diff --git a/tests/lantalkcase/system.toml b/tests/lantalkcase/system.toml index ef574afd005..b9363d92aa6 100644 --- a/tests/lantalkcase/system.toml +++ b/tests/lantalkcase/system.toml @@ -3,10 +3,8 @@ # listener the parameter line names, and sshd taking a command — `reboot` # among them, which is how the host hands the machine back. # -# `tests/e1000talkcase` is the same boot in front of QEMU's 82574; the two -# differ by the one `devices` row, for the reason `tests/lancase` and -# `tests/e1000case` do. Nothing here reaches the internal NVMe: no `[disks]` -# row, and the only device any program claims is the PCI function below. +# Nothing here reaches the internal NVMe: no `[disks]` row, and the only +# device any program claims is the PCI function below. [boot] start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] diff --git a/tests/metal/lenovo-20w0003amz.toml b/tests/metal/lenovo-20w0003amz.toml index 38f0210185c..b456029b221 100644 --- a/tests/metal/lenovo-20w0003amz.toml +++ b/tests/metal/lenovo-20w0003amz.toml @@ -3,9 +3,6 @@ product = "20W0003AMZ" bios = "N34ET71W (1.71 )" [measured] -"boot.ccorpus-2.complete_ms" = 1170 -"boot.ccorpus-2.panel_max_us" = 3878 -"boot.ccorpus-2.panel_us" = 21089 "boot.ccorpus.complete_ms" = 1165 "boot.ccorpus.panel_max_us" = 3759 "boot.ccorpus.panel_us" = 22178 @@ -19,9 +16,6 @@ bios = "N34ET71W (1.71 )" "boot.jobcase.complete_ms" = 1164 "boot.jobcase.panel_max_us" = 3873 "boot.jobcase.panel_us" = 21418 -"boot.lanicscase.complete_ms" = 1171 -"boot.lanicscase.panel_max_us" = 4000 -"boot.lanicscase.panel_us" = 21420 "boot.lanleasecase.complete_ms" = 1165 "boot.lanleasecase.panel_max_us" = 3841 "boot.lanleasecase.panel_us" = 22280 @@ -49,6 +43,9 @@ bios = "N34ET71W (1.71 )" "boot.shared-debug.complete_ms" = 1165 "boot.shared-debug.panel_max_us" = 3791 "boot.shared-debug.panel_us" = 21483 +"boot.shared.complete_ms" = 1154 +"boot.shared.panel_max_us" = 3608 +"boot.shared.panel_us" = 21429 "boot.testcases-deaf.complete_ms" = 1165 "boot.testcases-deaf.panel_max_us" = 5247 "boot.testcases-deaf.panel_us" = 26659 diff --git a/tests/toyos-rust-tests/src/bin/readdir_bound.rs b/tests/toyos-rust-tests/src/bin/readdir_bound.rs index c2ab6952483..3543c047428 100644 --- a/tests/toyos-rust-tests/src/bin/readdir_bound.rs +++ b/tests/toyos-rust-tests/src/bin/readdir_bound.rs @@ -7,11 +7,7 @@ //! buffer, stopped, and reported the bytes it had written, which is //! indistinguishable from a complete listing: 4125 entries of 34,816, as //! success. A bound plus a silent truncation is a quieter version of the same -//! defect, so both are asserted here. -//! -//! `/home` gets the same class third: its btree walk crossed the kernel's -//! allocation ceiling past 32,768 entries (a panic from ordinary `create`s) -//! and now refuses at the bound first. All below is an ordinary workload. +//! defect, so both are asserted here. All below is an ordinary workload. use std::fs; use std::process::Command; @@ -26,11 +22,7 @@ const MAX_LIST_ENTRIES: usize = 16_384; /// truncating kernel returns about 4,000 of them. const PLAIN_ENTRIES: usize = 6_000; -/// One past where the unbounded `/home` walk's `Vec` doubled over the kernel's allocation ceiling. -const HOME_ENTRIES: usize = 32_769; - fn main() { - home_tree_past_the_doubling_is_refused(); plain_entries_are_all_returned(); subdirectories_at_the_limit(); one_past_the_limit_is_refused(); @@ -39,19 +31,6 @@ fn main() { println!("all readdir bound tests passed"); } -/// First, while nothing else holds file-cache entries: the listing bound on -/// `/home` at the count that was a kernel death. The files stay — this boot is -/// the test's own. -fn home_tree_past_the_doubling_is_refused() { - for i in 0..HOME_ENTRIES { - fs::write(format!("/home/rb{i}"), b"").expect("create on /home failed"); - } - match fs::read_dir("/home") { - Ok(it) => panic!("listing /home past the limit returned {} entries", it.count()), - Err(e) => println!(" PASS: /home refused at {HOME_ENTRIES} entries ({e})"), - } -} - /// A listing larger than the caller's buffer comes back whole. /// /// Plain file entries, in their own subdirectory, so this exercises the @@ -96,11 +75,10 @@ fn one_past_the_limit_is_refused() { /// The bound belongs to the directory, not to the mount holding it. /// -/// Both mounts are over it by now and neither can list its own root, which is -/// the shape asserted at the end: every directory *in* them still lists, -/// including one holding nothing — the answer a mount-wide bound cannot give, -/// an empty listing and a refusal being the same byte to a caller checking -/// existence. A hash map and an on-disk B+tree, so this is the contract. +/// `/tmp` is over it by now and cannot list its own root, which is the shape +/// asserted at the end: every directory *in* it still lists, including one +/// holding nothing — the answer a mount-wide bound cannot give, an empty +/// listing and a refusal being the same byte to a caller checking existence. fn a_directory_still_lists_in_a_mount_past_the_bound() { let full = fs::read_dir("/tmp/d0").expect("read_dir of a directory in a full tmpfs"); let names: Vec = @@ -112,20 +90,11 @@ fn a_directory_still_lists_in_a_mount_past_the_bound() { assert_eq!(n, 0, "an empty directory listed {n} entries"); println!(" PASS: /tmp serves a directory and an empty one while its own root is refused"); - fs::write("/home/sub/one", b"x").expect("create on /home failed"); - let n = fs::read_dir("/home/sub").expect("read_dir of a directory in a full bcachefs").count(); - assert_eq!(n, 1, "/home/sub listed {n} entries"); - println!(" PASS: /home serves a directory while its own root is refused"); - - // The mount roots are directories too, and theirs is still the same bound. + // The mount root is a directory too, and its bound is still the same one. match fs::read_dir("/tmp") { Ok(it) => panic!("/tmp listed {} entries after the per-directory bound", it.count()), Err(e) => println!(" PASS: /tmp's own root is still refused ({e})"), } - match fs::read_dir("/home") { - Ok(it) => panic!("/home listed {} entries after the per-directory bound", it.count()), - Err(e) => println!(" PASS: /home's own root is still refused ({e})"), - } } /// The refusal cost the caller an error and nothing else. A panic inside diff --git a/tests/toyos.rs b/tests/toyos.rs index f1e5ee8f958..097e00fe3f9 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -93,7 +93,7 @@ const RUST_SKIP: &[&str] = &[ // Needs a launcher and a declared `cat` and shell, which `tests/testcases` // does not give: the `process_tree` metal row runs it on tests/proctreecase. "process_tree", - // It asserts nothing at all: it holds a `tests/lancase` boot open for + // It asserts nothing at all: it holds a `tests/lanleasecase` boot open for // twenty seconds so the host can reach this machine over the cable. On a // shared boot it would be twenty seconds of nothing. "lan_hold", @@ -202,13 +202,11 @@ const MACHINE_TESTS: &[&str] = &[ const METAL: &[(&str, metal::Metal)] = &[ ( "lan_dhcp_lease", - metal::Metal { arms: LANCASE, judge: |b| lan::on_metal(b[0]) }, + metal::Metal { arms: LANTALKCASE, judge: |b| lan::on_metal(b[0]) }, ), ( - // Folded into `lan_dhcp_lease`'s judge once the PHY is brought up - // (#453): lancase's own first-message record then carries this fact. "lan_message_delivery", - metal::Metal { arms: LANICSCASE, judge: |b| lan::provoked_on_metal(b[0]) }, + metal::Metal { arms: LANTALKCASE, judge: |b| lan::delivered_on_metal(b[0]) }, ), ( // The first byte: a lease from the bench's own router, read off the @@ -709,33 +707,22 @@ const METALCASE: &[metal::Arm] = &[metal::once("metalcase", "tests/metalcase", & const PROCTREECASE: &[metal::Arm] = &[metal::once("proctreecase", "tests/proctreecase", &[], &["test_rs_process_tree"])]; -/// The cable's own boot: netd in front of the T14's I219, and one job that -/// holds the machine up long enough for the host to reach it. The one arm in -/// this suite that names a PCI function for the loop to reach the boot over. -const LANCASE: &[metal::Arm] = - &[metal::Arm { nic: Some(lan::NIC), ..metal::once(lan::BOOT, lan::CONFIG, &[], lan::JOBS) }]; - -/// The cable's boot with netd's delivery actuator armed. **A count of no -/// messages is two facts** — a part nothing made speak and a message that -/// reached no CPU — so this boot asks the part for a message and `LANCASE` does -/// not. It names no PCI function: its judge reads the kernel's own records and -/// asks the cable nothing. -const LANICSCASE: &[metal::Arm] = &[metal::once(lan::ICS_BOOT, lan::ICS_CONFIG, &[], lan::JOBS)]; - -/// The cable's boot with netd's lease probe armed: netd's exit code is the -/// lease's verdict, read out of the kernel's own `exit:` record, and its report -/// is on the log volume. It names the I219 for the loop to ping over the cable, -/// as [`LANCASE`] does. +/// netd in front of the T14's I219 with its lease probe armed: netd's exit code +/// is the lease's verdict, read out of the kernel's own `exit:` record, and its +/// report is on the log volume. It names the I219 for the loop to ping over the +/// cable, as [`LANTALKCASE`] does. const LANLEASECASE: &[metal::Arm] = &[metal::Arm { nic: Some(lan::NIC), ..metal::once(lan::LEASE_BOOT, lan::LEASE_CONFIG, &[], lan::JOBS) }]; -/// The boot the host talks to over its own cable: the loop reads the log it -/// serves under its name, pings it, runs a command on it and tells it to -/// reboot. +/// The cable's boot, netd in front of the T14's I219, which the host talks to +/// over that cable: the loop reads the log it serves under its name, pings it, +/// runs a command on it and tells it to reboot. It names the PCI function, so +/// the loop also pings the address it held under the operating system before. const LANTALKCASE: &[metal::Arm] = &[metal::Arm { talk: true, + nic: Some(lan::NIC), ..metal::once(lan::TALK_BOOT, lan::TALK_CONFIG, &[], lan::TALK_JOBS) }]; @@ -776,7 +763,7 @@ fn shared_metal(keep: impl Fn(&str) -> bool) -> Vec { config: "tests/testcases", params: &[], features: &[], - members: const { std::num::NonZeroUsize::new(38).expect("a chunk holds a member") }, + members: const { metal::members_fitting(toyos_tco::RUST_MEMBER_MS) }, jobs: shipping .iter() .filter(|n| keep(n)) @@ -866,7 +853,7 @@ fn c_corpus_metal( config: "tests/testcases", params: &[], features: &[], - members: const { std::num::NonZeroUsize::new(90).expect("a chunk holds a member") }, + members: const { metal::members_fitting(toyos_tco::C_MEMBER_MS) }, jobs, files, links, @@ -1781,7 +1768,7 @@ fn virt_selftest( }, ); let said = format!("{param}: "); - let rest = qemu.drain_until(Duration::from_secs(180), |l| l.contains(&said)); + let rest = qemu.drain_until(Duration::from_secs(36), |l| l.contains(&said)); let serial = format!("{}\n{rest}", qemu.boot_log()); let Some(verdict) = serial.lines().find(|l| l.contains(&said)) else { return Err(format!("{param} never reported\nserial:\n{serial}")); @@ -1871,8 +1858,8 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re ..Default::default() }, ); - let dump = qemu.screendump_until("EARLY PANIC:", Duration::from_secs(30)); - let rest = qemu.drain_until(Duration::from_secs(10), |l| l.contains(EARLY_PANIC_MESSAGE)); + let dump = qemu.screendump_until("EARLY PANIC:", Duration::from_secs(6)); + let rest = qemu.drain_until(Duration::from_secs(6), |l| l.contains(EARLY_PANIC_MESSAGE)); let serial = format!("{}\n{rest}", qemu.boot_log()); // What stage 3 prints before it panics: every item is a record // only the AArch64 side of the loader or the kernel writes. @@ -1963,9 +1950,9 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re ..Default::default() }, ); - let dump = qemu.screendump_until("EARLY PANIC:", Duration::from_secs(30)); + let dump = qemu.screendump_until("EARLY PANIC:", Duration::from_secs(6)); const FAULT_MESSAGE: &str = "synchronous from EL1 on SP_EL1: unknown reason (an undefined instruction) at 0x"; - let rest = qemu.drain_until(Duration::from_secs(10), |l| l.contains(FAULT_MESSAGE)); + let rest = qemu.drain_until(Duration::from_secs(6), |l| l.contains(FAULT_MESSAGE)); let serial = format!("{}\n{rest}", qemu.boot_log()); for want in [ "KERNEL PANIC: synchronous from EL1 on SP_EL1: unknown reason (an undefined instruction)", @@ -2001,7 +1988,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re }, ); const SPAWNED: &str = "spawn: /system/bin/logd pid="; - let rest = qemu.drain_until(Duration::from_secs(180), |l| l.contains(SPAWNED)); + let rest = qemu.drain_until(Duration::from_secs(30), |l| l.contains(SPAWNED)); let serial = format!("{}\n{rest}", qemu.boot_log()); for want in [ "paging: the direct map holds memory below", @@ -2090,7 +2077,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re } // The pager runs only on the CPU that claimed the panel, and it is // the loop that watches the reset bound: a second page is its proof. - let paged = qemu.screendump_while(Duration::from_secs(20), Duration::from_millis(200), |d| { + let paged = qemu.screendump_while(Duration::from_secs(87), Duration::from_millis(200), |d| { d.rows().iter().any(|r| r.contains("[page ")) && d.text() != text }); if paged.text() == text { @@ -2132,7 +2119,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re // anything else is userland holding the panel. Without this the // test would prove that a fatal panic paints a screen nobody had // taken. - let up = qemu.screendump_while(Duration::from_secs(30), Duration::from_millis(200), |d| { + let up = qemu.screendump_while(Duration::from_secs(96), Duration::from_millis(200), |d| { d.fill() != FILL_BOOT }); if up.fill() == FILL_BOOT { @@ -3673,7 +3660,7 @@ impl Tally { if let Some(fastest) = fastest { say(format!( "host: fastest boot {fastest} ms against the reference {reference} ms — liveness \ - ceilings paid at {:.2}x width", + ceilings paid at {:.2}x", f64::from(num) / f64::from(den) )); } @@ -3812,7 +3799,6 @@ fn run_tasks(tasks: Vec, width: usize, test_config: &Path) -> Vec return Vec::new(); } let width = width.clamp(1, tasks.len()); - qemu::set_width(width as u32); let queue = std::sync::Mutex::new(std::collections::VecDeque::from(tasks)); let mut all = Vec::new(); thread::scope(|scope| { diff --git a/toyos-i219/src/lib.rs b/toyos-i219/src/lib.rs index f2facff1031..a7bd47444b5 100644 --- a/toyos-i219/src/lib.rs +++ b/toyos-i219/src/lib.rs @@ -894,17 +894,6 @@ impl I219 { Ok(nic) } - /// Raise one enabled cause on purpose (§10.2.4.4). - /// - /// **Nothing on a shipping path calls this**: the caller arms it and - /// [`Self::open`] does not, because a driver that raised a message every - /// boot would make the kernel's first-message record read the same on a - /// working card and a dead one. `LSC` is the cause, because acting on it is - /// re-reading `STATUS`, which the next pass does anyway. - pub fn provoke_message(&self) { - self.regs.write(regs::ICS, cause::LSC); - } - /// Pass frames sent to the multicast address `group`: the one bit of the /// Multicast Table Array its hash names on this part ([`regs::mta_bit_82574`], /// [`regs::mta_bit_pch`]) is set, and diff --git a/toyos-i219/src/regs.rs b/toyos-i219/src/regs.rs index caa2567ef7e..92cecd1bc59 100644 --- a/toyos-i219/src/regs.rs +++ b/toyos-i219/src/regs.rs @@ -31,8 +31,6 @@ pub const MDIC: usize = 0x00020; pub const ICR: usize = 0x000C0; /// Interrupt Throttling (§10.2.4.2, `0x000C4`). pub const ITR: usize = 0x000C4; -/// Interrupt Cause Set (§10.2.4.4, `0x000C8`), write-only. -pub const ICS: usize = 0x000C8; /// Interrupt Mask Set/Read (§10.2.4.5, `0x000D0`). pub const IMS: usize = 0x000D0; /// Interrupt Mask Clear (§10.2.4.6, `0x000D8`), write-only. diff --git a/toyos-i219/src/stub.rs b/toyos-i219/src/stub.rs index 97aec8f9614..5b2abf517c8 100644 --- a/toyos-i219/src/stub.rs +++ b/toyos-i219/src/stub.rs @@ -1039,9 +1039,8 @@ impl Model { } value } - // §10.2.4.6 and §10.2.4.4: write-only, and a read of one answers - // nothing. - regs::IMC | regs::ICS => 0, + // §10.2.4.6: write-only, and a read answers nothing. + regs::IMC => 0, // The statistics the driver reads: each read takes the count and // clears it. regs::GPTC | regs::GPRC | regs::TPR | regs::MPC | regs::CRCERRS => { @@ -1161,8 +1160,6 @@ impl Model { let held = self.get(regs::IMS); self.set(regs::IMS, held & !value); } - // §10.2.4.4: set a cause, as if the event had happened. - regs::ICS => self.raise(value), // §10.2.4.1: "Writing a 1b to any bit in the register also clears // that bit. Writing a 0b to any bit has no effect on that bit." // `INT_ASSERTED` is not writable and clears with its causes. diff --git a/toyos-i219/src/tests.rs b/toyos-i219/src/tests.rs index 1d8aea9088f..b3ef8522e76 100644 --- a/toyos-i219/src/tests.rs +++ b/toyos-i219/src/tests.rs @@ -678,58 +678,6 @@ fn transmit_descriptors_are_reclaimed_under_batched_write_back() { // --- interrupts and link --- -/// §10.2.4.4: writing a cause to `ICS` sets it in `ICR` as if the event had -/// happened, so a driver can make the part raise a message nothing on the wire -/// caused. That is the whole of the delivery experiment: a claim that takes -/// this message has an interrupt path, whatever the link is doing. -#[test] -fn a_cause_written_to_ics_raises_a_message_the_claim_takes() { - for part in [Part::E82574, Part::I219] { - let nic = - Nic::with(23, part, Permits { spurious_interrupts: false, ..Permits::default() }); - let mut driver = open(&nic); - // The bring-up's own causes first, so what the pass below reads is the - // provoked one and not what `open` left standing. - one_pass(&mut driver); - - driver.provoke_message(); - let pass = one_pass(&mut driver); - assert_eq!( - pass.messages, 1, - "{}", - nic.because("one cause written to ICS once is one message") - ); - // `OTHER` is §10.2.4.1's summary of `LSC` and is set with it; nothing - // else was written, so nothing else may be read. - assert_eq!( - pass.causes & !cause::INT_ASSERTED, - cause::LSC | cause::OTHER, - "{}", - nic.because("the causes read are not exactly the one written") - ); - } -} - -/// The negative control for the test above: the same boot with nothing written -/// to `ICS` takes no message, so a green arm there is the write's doing and not -/// a part that speaks on its own. -#[test] -fn a_pass_with_no_provoked_cause_takes_no_message() { - for part in [Part::E82574, Part::I219] { - let nic = - Nic::with(23, part, Permits { spurious_interrupts: false, ..Permits::default() }); - let mut driver = open(&nic); - one_pass(&mut driver); - - let pass = one_pass(&mut driver); - assert_eq!( - pass.messages, 0, - "{}", - nic.because("a message arrived on a pass nothing asked the part for one") - ); - } -} - /// §10.2.4.1's case 3 says a read of `ICR` with no interrupt asserted "has no /// side affect". A driver that treated the read as the acknowledgement would /// see the same causes for ever, so the causes it acted on are written back. diff --git a/toyos-tco/src/lib.rs b/toyos-tco/src/lib.rs index bb4c4efccea..eb7ceb1938e 100644 --- a/toyos-tco/src/lib.rs +++ b/toyos-tco/src/lib.rs @@ -114,6 +114,11 @@ pub const FIRMWARE_BOUND_MS: u64 = 60_000; /// while a job never finishes is no wedge to it and nothing else ends the boot. pub const JOB_BOUND_MS: u64 = 60_000; +/// What one member of a metal shared boot is allowed of [`JOB_BOUND_MS`], in +/// milliseconds: a Rust test on the shipping kernel, and a C corpus case. +pub const RUST_MEMBER_MS: u64 = 860; +pub const C_MEMBER_MS: u64 = 260; + /// The bound netd gives this machine's first DHCP lease before it says it has /// none and serves anyway, in milliseconds. The harness waits it out on a wire /// with no server, so the two read one declaration. @@ -141,6 +146,13 @@ pub const WEDGE_BOUND_MS: u64 = JOB_BOUND_MS * 2; /// test is a reset where it should have been a verdict. const _: () = assert!(WEDGE_BOUND_MS > JOB_BOUND_MS); +/// The bound a boot that stages its own wedge carries instead of +/// [`WEDGE_BOUND_MS`], in milliseconds: it ends the machine seconds after the +/// wedge rather than at the bound every other boot keeps for a wedge nobody +/// staged. Its half is the lockup detector's bound, which still outlasts the +/// lockup probe's own reach to its lock. +pub const STAGED_BOUND_MS: u64 = 10_000; + /// The bound one *CPU* gets to take no interrupt at all while it is burning /// cycles, in milliseconds, before that CPU ends the machine from its own NMI. /// diff --git a/userland/netd/src/i219.rs b/userland/netd/src/i219.rs index 3fdf8bf444d..d57df480eac 100644 --- a/userland/netd/src/i219.rs +++ b/userland/netd/src/i219.rs @@ -277,11 +277,6 @@ impl Nic { &self.claim } - /// `crate::PROVOKE_MESSAGE`: raise one enabled cause on purpose. - pub fn provoke_message(&self) { - self.driver.borrow().provoke_message(); - } - /// Pass frames sent to the multicast address `group`. pub fn accept_multicast(&self, group: [u8; 6]) { self.driver.borrow().accept_multicast(group); diff --git a/userland/netd/src/main.rs b/userland/netd/src/main.rs index 25b7689578b..16856f60b5b 100644 --- a/userland/netd/src/main.rs +++ b/userland/netd/src/main.rs @@ -27,23 +27,12 @@ mod virtio_net; /// at `00:1f.6`; `8086:10d3` is the 82574L, which QEMU's `e1000e` models. One /// driver takes both, and each row names which part it is because below the /// register file they are not one. -const CARDS: [(PciId, fn(toyos::PciDev, bool) -> Card); 3] = [ - (PciId { vendor: 0x8086, device: 0x15fc }, |c, p| Card::intel(c, Part::I219, p)), - (PciId { vendor: 0x8086, device: 0x10d3 }, |c, p| Card::intel(c, Part::E82574, p)), +const CARDS: [(PciId, fn(toyos::PciDev) -> Card); 3] = [ + (PciId { vendor: 0x8086, device: 0x15fc }, |c| Card::intel(c, Part::I219)), + (PciId { vendor: 0x8086, device: 0x10d3 }, |c| Card::intel(c, Part::E82574)), (PciId { vendor: 0x1af4, device: 0x1041 }, Card::virtio), ]; -/// The actuator that makes the card raise one interrupt on purpose, so a boot -/// whose only reading of the interrupt path is a count of messages can tell a -/// part nothing made speak from a message that reached no CPU. -/// -/// **Nothing a shipped machine runs arms it**: the argument comes from the -/// `[programs.netd] args` row of a boot config, and the one config that carries -/// it is `tests/lanicscase`. A boot that always raised a message would make the -/// kernel's first-message record read the same on a working card and a dead -/// one. -const PROVOKE_MESSAGE: &str = "--provoke-message"; - /// The probe under which this process brings the card up and serves exactly as /// it always does for [`LEASE_WINDOW`], leaves what happened on the log volume /// one durable line at a time (`report::PATH`), and then ends with @@ -51,10 +40,10 @@ const PROVOKE_MESSAGE: &str = "--provoke-message"; /// the window ends, and where none is, what the bring-up and the link said. /// /// **A lease is a frame out and a frame in, answered by a server this machine -/// does not control**, which is the claim the probe is flashed for; the lines -/// beside it say what the driver and the MAC counted each way, and so which -/// half went missing on a boot that got none. Armed the same way as -/// [`PROVOKE_MESSAGE`] and never beside it. +/// does not control**; the lines beside it say what the driver and the MAC +/// counted each way, and so which half went missing on a boot that got none. +/// The argument comes from the `[programs.netd] args` row of a boot config, +/// and nothing a shipped machine runs carries it. const EXIT_WITH_LEASE: &str = "--exit-with-lease"; /// How long [`EXIT_WITH_LEASE`] serves before it ends, counted from this @@ -67,9 +56,6 @@ const EXIT_WITH_LEASE: &str = "--exit-with-lease"; /// the machine answers the host's ping at the leased address. const LEASE_WINDOW: Duration = Duration::from_millis(toyos_tco::LEASE_BOUND_MS - 2_000); -/// The two, which cannot share a boot. -const ACTUATORS: [&str; 2] = [PROVOKE_MESSAGE, EXIT_WITH_LEASE]; - fn armed(actuator: &str) -> bool { std::env::args().any(|arg| arg == actuator) } @@ -111,25 +97,14 @@ impl Card { panic!("netd: the NIC this program was given is not one it can drive — {why}") } - /// `provoke` is [`PROVOKE_MESSAGE`], carried out once the card is up. - fn intel(claim: toyos::PciDev, part: Part, provoke: bool) -> Self { + fn intel(claim: toyos::PciDev, part: Part) -> Self { match i219::Nic::open(claim, part) { - Ok(nic) => { - if provoke { - nic.provoke_message(); - } - Self::Intel(nic) - } + Ok(nic) => Self::Intel(nic), Err(why) => Self::undrivable(why), } } - /// [`PROVOKE_MESSAGE`] is the Intel driver's: armed here it is refused, not - /// skipped, before `open` touches the card. - fn virtio(claim: toyos::PciDev, provoke: bool) -> Self { - if provoke { - panic!("netd: {PROVOKE_MESSAGE} is the Intel driver's and this card is virtio"); - } + fn virtio(claim: toyos::PciDev) -> Self { match VirtioNet::open(claim) { Ok(nic) => Self::Virtio(nic), Err(why) => Self::undrivable(why), @@ -1610,16 +1585,7 @@ fn main() { }; let acceptor = endow::acceptor("netd") .expect("the manifest declares this program serves `netd`"); - // Before the card is opened, so a boot config that arms both is refused - // before either acts. - let asked_for: Vec<&str> = ACTUATORS.into_iter().filter(|actuator| armed(actuator)).collect(); - if asked_for.len() > 1 { - panic!( - "netd: {asked_for:?} cannot share a boot: a probe ends this process before the \ - point another of them acts at" - ); - } - let nic = open(claim, armed(PROVOKE_MESSAGE)); + let nic = open(claim); let report = armed(EXIT_WITH_LEASE).then(|| { let report = report::Report::open(started); let intel = nic.intel_driver();