diff --git a/Cargo.lock b/Cargo.lock index f6abd5dccfc..64cca460091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1322,6 +1322,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "i219" +version = "0.0.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-device-memory", + "toyos-i219", + "toyos-link", + "toyos-manifest", + "toyos-pci-claim", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1804,12 +1817,10 @@ version = "0.0.0" dependencies = [ "toyos", "toyos-abi", - "toyos-device-memory", "toyos-dhcp", "toyos-dns", - "toyos-i219", "toyos-inspect", - "toyos-manifest", + "toyos-link", "toyos-mdns", "toyos-net-ip", "toyos-net-node", @@ -1817,9 +1828,7 @@ dependencies = [ "toyos-net-tcp", "toyos-net-udp", "toyos-net-wire", - "toyos-pci-claim", "toyos-tco", - "toyos-virtio", ] [[package]] @@ -3791,6 +3800,15 @@ dependencies = [ "toyos-elf", ] +[[package]] +name = "toyos-link" +version = "0.1.0" +dependencies = [ + "loom", + "toyos", + "toyos-abi", +] + [[package]] name = "toyos-logstream" version = "0.1.0" @@ -4362,6 +4380,19 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtio-net" +version = "0.0.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-device-memory", + "toyos-link", + "toyos-manifest", + "toyos-pci-claim", + "toyos-virtio", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 7026709d242..b384070be01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "lib/ieee80211", "lib/input", "lib/inspect", + "lib/link", "lib/logkeeper-api", "lib/logstream", "lib/manifest", @@ -101,13 +102,14 @@ members = [ "system/fileserver", "system/grants", "system/host", + "system/i219", + "system/i219/driver", "system/inspect", "system/logkeeper", "system/logout", "system/netstack", "system/netstack/dhcp", "system/netstack/dns", - "system/netstack/i219", "system/netstack/ip", "system/netstack/mdns", "system/netstack/node", @@ -144,6 +146,7 @@ members = [ "system/usbd/xhci", "system/vdagent", "system/vdagent/bridge", + "system/virtio-net", "tests/guest/hangupholder", "tests/guest/inputsink", "tests/guest/kernelprobe", diff --git a/build/build.rs b/build/build.rs index 5b25b5052cd..1ce2b51f27c 100644 --- a/build/build.rs +++ b/build/build.rs @@ -3257,6 +3257,7 @@ mod tests { "tests/images/launchcase.toml", "tests/images/floorcase.toml", "tests/images/libccase.toml", + "tests/images/linkcase.toml", "tests/images/sessioncase.toml", "tests/images/metalcase.toml", "tests/images/metaldevicecase.toml", diff --git a/build/ci.rs b/build/ci.rs index dee7247cb2b..d28728a8b26 100644 --- a/build/ci.rs +++ b/build/ci.rs @@ -482,6 +482,7 @@ const KERNEL_SIM: &str = "kernel-sim"; /// The kernel's models, which [`Part::Models`] tests beside their controls. const MODELS: [&str; 2] = [KERNEL_LOOM, KERNEL_SIM]; const BLOCKRING: &str = "toyos-blockring"; +const LINK: &str = "toyos-link"; const PROCLIFE: &str = "toyos-proclife"; /// The SDK package, whose `transport` module carries its own controls. const SDK: &str = "toyos"; @@ -752,6 +753,8 @@ pub(crate) const CONTROLS: &[Control] = &[ red(BLOCKRING, "mutate-no-reissue-after-loss", None, &[ Fails("model::tests::what_a_flush_calls_durable_is_on_the_medium"), ]), + // The link's one: a full ring's want swapped `Relaxed`, a wake lost. + red(LINK, "mutate-want-relaxed", Some("ends_loom"), &[Fails("a_full_ring_is_never_left_asleep_on_room_it_has")]), red(SDK, "transport-publish-relaxed", Some("transport_loom"), &[ Fails("a_published_entry_is_read_whole"), Fails("a_published_byte_is_read_as_written"), diff --git a/images/system.toml b/images/system.toml index 30b0a4fb44a..9e2ef83dc4b 100644 --- a/images/system.toml +++ b/images/system.toml @@ -132,14 +132,29 @@ drives = ["pci:04:03:*"] receives = ["sound-output"] syscap = ["rt"] -# netstack holds the NIC's PCI function and drives it: the virtqueues, the register -# window and the interrupt are its own, and the kernel keeps only the claim. -# Named by class, vendor and device rather than by slot, so one row finds the card -# wherever firmware put it. +# The network stack, over the link a NIC driver brings on `link`; it claims no +# device. It serves once the first driver says hello, and leaves a machine +# none says hello on within 30 s. [programs.netstack] service = true -serves = ["netstack"] -devices = ["pci:02:00:00/1af4:1041"] +serves = ["netstack", "link"] + +# The NIC drivers: one process per function a row's parts match, by class, +# vendor and device rather than by slot, holding that function alone and +# bringing its link to netstack, which keeps every connection across a +# link's end. virtio's is started again when it ends other than by exit 0. +[programs.virtio-net] +drives = ["pci:02:00:00/1af4:1041"] +receives = ["link"] +restart = true + +# The I219, the T14's onboard NIC, and the 82574, which QEMU's `e1000e` models. +# Not started again: its start resets the part, and over a ring a link change +# left it that reset is not known to be safe +# (`issues/no-machine-has-read-an-intel-nic-across-a-link-change.md`). +[programs.i219] +drives = ["pci:02:00:00/8086:15fc", "pci:02:00:00/8086:10d3"] +receives = ["link"] # The shared clipboard with QEMU's host: one process per virtio-serial # function the kernel does not drive (the kernel's console is `1af4:1043` too), diff --git a/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md b/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md index 21914a5a463..6ef646dfbc1 100644 --- a/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md +++ b/issues/a-link-that-goes-and-returns-inside-one-pass-of-the-intel-driver-is-reported-as-no-change.md @@ -6,14 +6,16 @@ opened: 2026-10-09 # A link that goes and returns inside one pass of the Intel driver is reported as no change -`toyos-i219`'s `begin_pass` (`system/netstack/i219/src/lib.rs`) reads the link only on +`toyos-i219`'s `begin_pass` (`system/i219/driver/src/lib.rs`) reads the link only on a pass that acknowledged `LSC`, and answers `link_changed: self.link != before`: the state after the pass compared with the state before it. `LSC` is one bit, so a link that went down and came back at the same speed and duplex before one pass acknowledged it is read as the link it was, and the pass -answers no change. `Nic::begin_pass` (`system/netstack/src/i219.rs`) hands -netstack a link only where that is true, so `main`'s `link_up` stays true and -no pass tells the name's responder it had no link. +answers no change. The driver's pass writes the link's word only when the +link differs from the one it last said (`lib/link/src/driver.rs`), and +netstack tells the node only of a change in whether it is up +(`system/netstack/src/link.rs`), so no pass tells the name's responder it had +no link. RFC 6762 §8 asks for probing and announcing whenever a responder "receives an indication of a network interface "Link Change" event". The part gave that indication, the `LSC` cause, and the driver held diff --git a/issues/every-driver-is-still-in-the-kernel.md b/issues/every-driver-is-still-in-the-kernel.md index c2bc63a6a7e..5d5028dbf52 100644 --- a/issues/every-driver-is-still-in-the-kernel.md +++ b/issues/every-driver-is-still-in-the-kernel.md @@ -10,7 +10,7 @@ The owner's target is that `virtio` does not appear anywhere in the kernel. The strongest check on it is not the driver files but the ABI: no syscall name may carry a NIC, GPU or audio device operation. `rg '^pub const SYS_(NIC|GPU|AUDIO|HDA)_' sdk/abi/src/syscall.rs` is the check, and it finds none: the three `SYS_NIC_*` -names went with the virtio NIC driver, which is `system/netstack/src/virtio_net.rs` +names went with the virtio NIC driver, which is `system/virtio-net/src/nic.rs` now, the four `SYS_GPU_*` names with virtio-gpu's, which is the compositor's (`system/compositor/virtio-gpu`), and `SYS_HDA_BIND` with the kernel's HDA stub. The kernel's virtio code left is its console's, and it diff --git a/issues/extcnf-ctrl-bit-7-is-read-as-manageability-on-a-mac-where-it-gates-phy-configuration.md b/issues/extcnf-ctrl-bit-7-is-read-as-manageability-on-a-mac-where-it-gates-phy-configuration.md index e5e76969bb0..e59e8cbad42 100644 --- a/issues/extcnf-ctrl-bit-7-is-read-as-manageability-on-a-mac-where-it-gates-phy-configuration.md +++ b/issues/extcnf-ctrl-bit-7-is-read-as-manageability-on-a-mac-where-it-gates-phy-configuration.md @@ -6,7 +6,7 @@ opened: 2026-09-23 # `EXTCNF_CTRL` bit 7 is read as manageability's MDIO ownership on a MAC where it gates the PHY's automatic configuration -`system/netstack/i219/src/regs.rs`'s `extcnf::MDIO_MNG_OWNERSHIP` names bit 7 of +`system/i219/driver/src/regs.rs`'s `extcnf::MDIO_MNG_OWNERSHIP` names bit 7 of `EXTCNF_CTRL` after the 82574's §4.5.2, and `phy::Others`, the `SoftwareFlagStoodBeside*` refusals and exit codes 73 to 75 read it that way on the T14 too. The PCH's own datasheet (631120, rev 002, §8.2.4) calls bit 7 diff --git a/issues/linux-loses-the-t14s-i219-after-a-toyos-warm-reset.md b/issues/linux-loses-the-t14s-i219-after-a-toyos-warm-reset.md index 4e0d1a9be2b..8f585b7aaec 100644 --- a/issues/linux-loses-the-t14s-i219-after-a-toyos-warm-reset.md +++ b/issues/linux-loses-the-t14s-i219-after-a-toyos-warm-reset.md @@ -110,7 +110,7 @@ result (`ok`, `MISSING` or `unreachable`). At evidence commit success or failure: it never reads out the ULP configuration it met, so a clean probe cannot tell an armed hand-off from an unarmed one. -**Owner**: the I219 driver (`system/netstack/i219/`). +**Owner**: the I219 driver (`system/i219/driver/`). **Exit**, in order: diff --git a/issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md b/issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md index 2070d45b302..290f6b5f46c 100644 --- a/issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md +++ b/issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md @@ -32,4 +32,5 @@ its control `transport-want-relaxed` of `toyos`. **Exit**: a test on the fork's `toyos` branch in which a model that deadlocks with a `loom::sync::Arc` live prints loom's `deadlock` message and its test -fails without an abort, and that stream model holds its state in loom's `Arc`. +fails without an abort, and that stream model and `lib/link/tests/ends_loom.rs` +hold their state in loom's `Arc`. diff --git a/issues/netstack-drives-one-card.md b/issues/netstack-drives-one-card.md index 19821a1f369..7843af9cb34 100644 --- a/issues/netstack-drives-one-card.md +++ b/issues/netstack-drives-one-card.md @@ -6,11 +6,17 @@ opened: 2026-10-12 # netstack drives one card -netstack opens the first of `CARDS` the supervisor endowed, and no other -(`system/netstack/src/main.rs`, `main`). It opens it behind `Card`, an enum -of its in-process drivers (`system/netstack/src/card.rs`). Its node is one -interface with one DHCP client (`system/netstack/node/src/lib.rs`). So a -machine with a wired and a wireless link uses only one of them. [ip] already +netstack opens the first link a NIC driver brings, and no other: a hello with +another address, or with that address while its link is alive, is refused +`OneLink` (`system/netstack/src/main.rs`'s loop). +Its node is one interface with one DHCP client, made with the first link's +address (`system/netstack/node/src/lib.rs`). So a machine with a wired and a +wireless link uses only one of them, whichever driver says hello first. + +Because the node needs that address, netstack serves nothing before a +driver's hello: every client waits for it, and on a machine with no NIC +driver for 30 s (`LINK_BOUND`, `system/netstack/src/main.rs`), after which +netstack leaves and a client hears it gone. [ip] already routes over several interfaces, the lower index winning a tie (`system/netstack/ip/src/lib.rs`). @@ -23,12 +29,13 @@ That reverses part of `issues/first-match-selection-that-remains.md` dropped" and that "Neither is a defect today". Under ruling 3 a second NIC dropped is this defect. -Owner: P5 of `issues/there-is-no-wifi.md`, after the LAN track's stage 3 -moves the drivers out (`issues/the-lan-is-not-yet-production-grade.md`). +Owner: P5 of `issues/there-is-no-wifi.md`. The drivers are out: each is a +process of its own over a link (`toyos-link`). **Exit**: - one interface per link, each with its own DHCP client, lease and mDNS - responder, under one host name; + responder, under one host name, and a node that serves before any link, + so `LINK_BOUND` goes; - links ranked wired before wireless, then by arrival; - a guest with two virtio NICs in which the first's link is set off over QMP, and traffic leaves on the second; diff --git a/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md b/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md deleted file mode 100644 index 93dbcfc5311..00000000000 --- a/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-09 ---- - -# netstack ends for the boot on a NIC it cannot drive on - -`system.toml` gives `restart = true` to diskserver and fileserver and not to -netstack. netstack ends by name, "this NIC cannot be driven on", wherever a -card stops being one it can drive: a claim that refuses its interrupt read, an -Intel part that kept stranded descriptors past the driver's deadline -(`Card::begin_pass`, `system/netstack/src/card.rs`), and a virtio device -whose used ring is not believed (`finished`, -`system/netstack/src/virtio_net.rs`), which is any of a head past the table, -a head with no chain in flight, a length past a chain's writable bytes and a -used index past the available one. After any of them the machine has no -network until it boots again, and every program holding the `netstack` port -holds a port nobody serves. - -Ending is the right answer to the device: none of the four is something a -conforming device writes, and a ring read on past one loses frames silently. -What is missing is what comes after the end. - -A restart is not one line in `system.toml`. Every claim a second netstack -mints spends device addresses -(`issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md`), and -section 3 of `issues/the-lan-is-not-yet-production-grade.md` splits the driver -from the stack so that a driver's end does not take the stack's connections. - -Owned by whoever takes that section. Exit: a guest test ends netstack's driver -and reads the network served again without a boot, or the owner rules that a -NIC whose device broke its ring stays down for the boot and this file becomes -that ruling. diff --git a/issues/netstack-is-never-told-the-machine-stops.md b/issues/netstack-is-never-told-the-machine-stops.md deleted file mode 100644 index 2e0e57ddcb7..00000000000 --- a/issues/netstack-is-never-told-the-machine-stops.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-28 ---- - -# netstack is never told the machine stops - -The supervisor tells only disk drivers that the machine stops -(`toyos_quiesce::DRIVER_STOP`, `stop_drivers` in -`system/supervisor/src/main.rs`). netstack is a `[boot] start` service holding -its card through `devices`, so a reboot or power-off parks it in the kernel's -quiesce and its I219 destructor never runs: `CTRL_EXT` bit 28, the host's word -to the firmware that a driver holds the function (`toyos_i219`'s `pch`), stays -set across the reset, and nothing the driver could say about the PHY at the end -reaches the log. - -The next operating system's start does not depend on it: the driver disarms -Ultra Low Power at its own start (`system/netstack/i219/src/ulp.rs`), and a -machine cut off mid-boot gets no stop either. - -**Owner**: stage 3 of `issues/the-supervisor-is-host-tested-and-owns-the-stop.md`, -where the supervisor asks each service it started to finish before it calls -the kernel. - -**Exit**: a T14 reboot whose log shows the I219's driver told of the stop, its -destructor run and `CTRL_EXT` bit 28 clear before the kernel's reset. diff --git a/issues/netstacks-latency-under-a-receive-batch-over-64-frames-is-unmeasured.md b/issues/netstacks-latency-under-a-receive-batch-over-64-frames-is-unmeasured.md index 1997ed22945..69598c775fb 100644 --- a/issues/netstacks-latency-under-a-receive-batch-over-64-frames-is-unmeasured.md +++ b/issues/netstacks-latency-under-a-receive-batch-over-64-frames-is-unmeasured.md @@ -6,9 +6,10 @@ opened: 2026-10-12 # netstack's latency under a receive batch of more than 64 frames is unmeasured -`Node::receive` takes at most the card's ring in one batch (`Card::receive`, -`system/netstack/src/card.rs`): `toyos_i219::RX_RING - 1`, 999 frames, on the -I219, and the queue's 256 on virtio. Before pull request #861 it took 64. Every +`Node::receive` takes at most the link's receive ring in one batch, 256 +frames whatever the driver (`toyos_link::layout::RX_DEPTH`, +`system/netstack/src/link.rs`). Before the drivers left netstack it took the +card's ring, 999 frames on the I219, and before pull request #861, 64. Every frame of a batch reaches the stack under one `now`, read before the batch (`system/netstack/src/main.rs`'s loop), and the deadlines that are due, the transmit opportunity and the clients' answers run only after the batch's last diff --git a/issues/no-machine-has-read-an-intel-nic-across-a-link-change.md b/issues/no-machine-has-read-an-intel-nic-across-a-link-change.md index d83a843c966..19bde2808d1 100644 --- a/issues/no-machine-has-read-an-intel-nic-across-a-link-change.md +++ b/issues/no-machine-has-read-an-intel-nic-across-a-link-change.md @@ -8,12 +8,12 @@ opened: 2026-10-09 A link that changes while the transmit ring holds unsent descriptors leaves them the part's, on the 82574 and on the I219 alike -(`system/netstack/i219/src/lib.rs`, "A frame is offered only to a ring with room for +(`system/i219/driver/src/lib.rs`, "A frame is offered only to a ring with room for it"): they are counted `descriptors.stranded`, the part is owed their write-back within `STRANDED_DEADLINE_NANOS` (12.98 s) of the link being read up, and a part that has not written them back by then is refused by name, so -netstack ends with "cannot be driven on". Every reading of that is the -driver's own model. +the `i219` driver ends with "cannot be driven on" and netstack's link with +it. Every reading of that is the driver's own model. **Both parts' documents say they keep the frame for the link that returns.** The 82574 datasheet's Defer Count (317694 §10.2.7) and the I219 datasheet's @@ -41,11 +41,12 @@ PCH. The one piece of evidence the tree has is froze at reboot with that function left holding live rings, cause not established. -**`open`'s own reset has the same exposure.** Every start of netstack resets -the function before it programs it. On a netstack started again over a -function that still holds published descriptors, `toyos_i219::quiesce` stops -the receiver and the transmitter and the reset then goes out over that ring. -Nothing guards it, because nothing found says what the guard is. +**`open`'s own reset has the same exposure.** Every start of the `i219` +driver resets the function before it programs it. On a driver started again +over a function that still holds published descriptors, +`toyos_i219::quiesce` stops the receiver and the transmitter and the reset +then goes out over that ring. Nothing guards it, because nothing found says +what the guard is. ## What the rule costs @@ -67,12 +68,12 @@ read by the row the exit names: at 10 Mb/s. Nothing bounds how long a station defers to others on a busy half-duplex segment, and no document read says how long after `STATUS.LU` a part resumes. A part still deferring at the deadline is refused, and that - ends the machine's network. -- **The refusal is safe only while netstack's row has no `restart`.** - `[programs.netstack]` in `system.toml` carries none, so a refused part is - next reset by a `swap netstack` or a reboot. With a `restart` the refusal - becomes an automatic `open` over the ring the part kept, which is the reset - this issue does not know to be safe. + ends the machine's wired network. +- **The refusal is safe only while the `i219` row has no `restart`.** + `[programs.i219]` carries none in any image, so a refused part is next + reset by a reboot. With a `restart` the refusal becomes an automatic + `open` over the ring the part kept, which is the reset this issue does not + know to be safe. The virtio driver's row restarts; the Intel parts' do not. ## Owner @@ -87,16 +88,16 @@ can do either. The instrument is a link partner a row commands, owed by stage survives unplug and replug") needs the same one. That row fills the ring, drops the link and raises it, and reads: -- `descriptors.stranded` above 0 with netstack still serving, and on the +- `descriptors.stranded` above 0 with the link still carrying, and on the partner whether the stranded frames arrived (stale frames) or `wire.sent` stood still while the descriptors came back (lost by the `TNCRS` reading); - or netstack's "cannot be driven on" line naming the ring, where the part + or the driver's "cannot be driven on" line naming the ring, where the part wrote nothing back; - the same at 10 Mb/s half duplex against a partner that keeps the segment busy, for the deadline's false refusal; -- with netstack started again under traffic and the machine then rebooted, - whether it comes back: the reset over a live ring. Until that is read, - netstack's row takes no `restart`. +- with the `i219` driver started again under traffic and the machine then + rebooted, whether it comes back: the reset over a live ring. Until that is + read, the `i219` row takes no `restart`. An Intel document that states the I219's sequence for a reset over published descriptors closes the second unknown without a machine: `open` then does diff --git a/issues/the-i219-driver-is-never-told-the-machine-stops.md b/issues/the-i219-driver-is-never-told-the-machine-stops.md new file mode 100644 index 00000000000..c6f20b12e01 --- /dev/null +++ b/issues/the-i219-driver-is-never-told-the-machine-stops.md @@ -0,0 +1,28 @@ +--- +status: open +kind: defect +opened: 2026-10-28 +--- + +# The I219 driver is never told the machine stops + +The supervisor tells only disk drivers that the machine stops +(`toyos_quiesce::DRIVER_STOP`, `stop_drivers` in +`system/supervisor/src/main.rs`). The `i219` driver is a `drives` row the +supervisor gives no stop pipe, so a reboot or power-off parks it in the +kernel's quiesce and its I219 destructor never runs: `CTRL_EXT` bit 28, the +host's word to the firmware that a driver holds the function (`toyos_i219`'s +`pch`), stays set across the reset, and nothing the driver could say about +the PHY at the end reaches the log. The disk drivers' stop pipe is the +mechanism a `drives` row already has for it. + +The next operating system's start does not depend on it: the driver disarms +Ultra Low Power at its own start (`system/i219/driver/src/ulp.rs`), and a +machine cut off mid-boot gets no stop either. + +**Owner**: stage 3 of `issues/the-supervisor-is-host-tested-and-owns-the-stop.md`, +where the supervisor asks each service it started to finish before it calls +the kernel. + +**Exit**: a T14 reboot whose log shows the I219's driver told of the stop, its +destructor run and `CTRL_EXT` bit 28 clear before the kernel's reset. diff --git a/issues/the-i219-leaves-ulp-to-a-valid-firmware-without-asking-it.md b/issues/the-i219-leaves-ulp-to-a-valid-firmware-without-asking-it.md index 6f7802fcc2c..6597d2bbe74 100644 --- a/issues/the-i219-leaves-ulp-to-a-valid-firmware-without-asking-it.md +++ b/issues/the-i219-leaves-ulp-to-a-valid-firmware-without-asking-it.md @@ -8,7 +8,7 @@ opened: 2026-10-28 Where `FWSM` bit 15 reports the firmware valid, the Management Engine configures the PHY's Ultra Low Power, and `toyos_i219` writes none of it -(`system/netstack/i219/src/ulp.rs`). It also asks the engine for nothing: a +(`system/i219/driver/src/ulp.rs`). It also asks the engine for nothing: a host driver on such a machine asks it to leave ULP through the MAC's host-to-ME register at `0x5B50` and waits on `FWSM` for the engine's answer, and this driver has neither. A PHY the engine left in ULP there stays out of diff --git a/issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md b/issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md index b2069c7a95f..86660fb6fc1 100644 --- a/issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md +++ b/issues/the-intel-nics-room-and-wake-are-unread-on-hardware.md @@ -6,12 +6,13 @@ opened: 2026-10-08 # The Intel NIC's transmit room and its wake have been read on no hardware -netstack offers its card a frame only when the driver counts room for it, and -where there is none it sleeps on the claim until `toyos_i219::I219::wake_on_room`'s -cause arrives. On the I219 and the 82574 every reading of that path is the -driver's own model (`system/netstack/i219/src/stub.rs`), which is Intel's documents -written down: no image gives netstack `pci:8086:15fc`, and the harness has no -Intel card, so neither the T14 nor QEMU's `e1000e` has run it. +The `i219` driver takes a frame off its link only when it counts room for it +in the part's ring, and where there is none it sleeps on the claim until +`toyos_i219::I219::wake_on_room`'s cause arrives (`lib/link/src/driver.rs`). +On the I219 and the 82574 every reading of that path is the driver's own +model (`system/i219/driver/src/stub.rs`), which is Intel's documents written +down: the T14's part and QEMU's `e1000e` now carry links, but no run has +filled either one's transmit ring, so neither has run it. What the documents do not say of the T14's part, and the model therefore takes on trust: that the PCH's MAC raises a message for `TXDW` once it is unmasked. @@ -37,15 +38,15 @@ exit: `issues/no-machine-has-read-an-intel-nic-across-a-link-change.md`. ## Owner The T14's outbound rows of `issues/toyos-has-its-own-network-stack.md`: the -first boot that gives netstack the wired card. +first that fills the wired card's transmit ring. ## Exit -A T14 boot whose netstack drives `8086:15fc` queues more than +A T14 boot over the `i219` driver's link queues more than `toyos_i219::TX_RING - 1` frames in one pass, from more sockets than one and to a destination that answers none of them, and `inspect` then answers `transmit.full`, `transmit.wake_armed` and `transmit.wake_taken` each at least 1, `descriptors.stranded` 0, and `descriptors.sent` equal to `wire.sent`; the boot's log carries no "cannot be driven on" line -from netstack. A boot whose `transmit.full` reads 0 never filled the ring and +from the driver. A boot whose `transmit.full` reads 0 never filled the ring and has read nothing. diff --git a/issues/the-lan-is-not-yet-production-grade.md b/issues/the-lan-is-not-yet-production-grade.md index 45688ee20a4..443f32698d7 100644 --- a/issues/the-lan-is-not-yet-production-grade.md +++ b/issues/the-lan-is-not-yet-production-grade.md @@ -74,21 +74,17 @@ survives unplug and replug, the handback is tested, the fuzzers run in CI. ## 3. One driver per device, one stack -netd holds every NIC driver and the stack in one process, and `system.toml` -names the card per machine (`devices = ["pci:1af4:1041"]`). Neither scales to -all modern hardware: a bug in one driver takes every connection with it, a -driver cannot restart without the stack's state, and a machine nobody has seen -has no row. - -- One driver process per network function, claiming exactly that function. -- netd is the stack alone, and meets every driver through one narrow interface: - frames over shared-memory rings, link state, speed, MAC address, MTU, offloads. -- init binds drivers to hardware from a declared match table by vendor, device - and class, instead of a row naming the card. - -The drivers already live in their own crates (`toyos-i219`, the virtio driver), -so this is a move of hosts, not a rewrite. It lands before a second real NIC -family is added, which is when the monolith starts to cost. +Built as P4 of `issues/there-is-no-wifi.md`, which holds what of its exit the +benches still owe. + +- One driver process per network function, claiming exactly that function: + each driver a `drives` row (`system/i219`, `system/virtio-net`). +- netstack is the stack alone, and meets every driver through one narrow + interface (`toyos-link`): frames over a shared region's rings, and the link + state, speed and MAC address. No MTU or offload is in it: the stack sends + frames of 1,514 bytes at most, and no driver offers an offload. +- The supervisor binds drivers to hardware from the `drives` rows' match + table by vendor, device and class, instead of a row naming the card. **Exit**: killing a driver process restarts it with the stack's connections intact, and a machine with an unlisted but matching NIC comes up without a diff --git a/issues/the-mac-resets-ordering-against-the-mdio-arbitration-is-unmeasured-on-this-part.md b/issues/the-mac-resets-ordering-against-the-mdio-arbitration-is-unmeasured-on-this-part.md index 641904f2519..4ba3a795e3e 100644 --- a/issues/the-mac-resets-ordering-against-the-mdio-arbitration-is-unmeasured-on-this-part.md +++ b/issues/the-mac-resets-ordering-against-the-mdio-arbitration-is-unmeasured-on-this-part.md @@ -6,8 +6,8 @@ opened: 2026-09-16 # The MAC reset's ordering against the MDIO arbitration is unmeasured on this part -`system/netstack/i219/src/lib.rs`'s `open` now resets the I219 on the properties -`system/netstack/i219/src/wake.rs`'s header states, which Intel's host driver for this +`system/i219/driver/src/lib.rs`'s `open` now resets the I219 on the properties +`system/i219/driver/src/wake.rs`'s header states, which Intel's host driver for this family acts on: `CTRL.RST` with `CTRL.PHY_RST` beside it where `FWSM` bit 6 allows, written under the software flag, then 20 ms with no register access, then a bounded wait on `STATUS` bit 9, the PHY configured. The 82574 path is diff --git a/issues/the-tree-says-who-uses-each-thing.md b/issues/the-tree-says-who-uses-each-thing.md index 74fce80b3d1..d35f4354c6a 100644 --- a/issues/the-tree-says-who-uses-each-thing.md +++ b/issues/the-tree-says-who-uses-each-thing.md @@ -165,9 +165,9 @@ Where this differs from the tree the owner adopted: - `sdk/std/` is std's ToyOS backend, which left the `rust` fork after the design (the owner: "MOVE IT"); the fork's `library/std`, outside this repository, compiles it by `#[path]` (rule step 3). -- `toyos-i219` is `system/netstack/i219`: the harness stopped using it in - 93719bc03, after the design, which leaves netstack its one user (rule - step 4). +- `toyos-i219` is `system/i219/driver`: the harness stopped using it in + 93719bc03, after the design, which leaves the `i219` driver program its + one user (rule step 4). - `toyos-pci-claim`, `acpiserver-api` and `logkeeper-api` are in `lib/`: each has two users (rule step 5). - `toyos-ssh` is `lib/ssh`: `build/signing.rs` and `system/sshserver` use it diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 3d2e80104dc..7cb12fbe14b 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -109,8 +109,8 @@ and the ACPI read-out in `s1-r2/` without `ECAP.EIM`: `kernel/src/arch/x86_64/vtd/mod.rs`) rests on `CFIS=0` alone. -**S2, the link hop's cost on the T14.** A scout, never landing: two -processes over `toyos::transport`, one entry per 1,514-byte frame, batches of +**S2, the link hop's cost on the T14: done, reported under P4.** A scout, +never landing: two processes over `toyos::transport`, one entry per 1,514-byte frame, batches of 1, 8 and 32, both directions, each side copying each frame once. Owner: the orchestrator. Depends on: nothing. Exit: cycles per frame and frames a second on one CPU, reported in P4's design round. Beside them stands the @@ -192,6 +192,29 @@ stage 3 of `issues/the-lan-is-not-yet-production-grade.md`, whole. - the network suite unchanged under QEMU; - a T14 row reading the I219 working, with nothing regressed; - S2 rerun and reported. +- **Built:** `toyos-link` (`lib/link`), and the I219 and virtio drivers as + `drives` rows of their own (`system/i219`, `system/virtio-net`), which the + supervisor's match table already binds; netstack claims no device and + refuses a link of another address by name. A link whose driver wrote what + no driver writes ends, and that address's next hello is refused + `Faulted`, on which the driver ends: the supervisor's restart budget (3 in + 10 s) bounds a driver that keeps doing it. virtio's row restarts; the I219's + does not, until + `issues/no-machine-has-read-an-intel-nic-across-a-link-change.md` reads its + reset over a live ring. In a guest: + `a_link_ends_under_a_held_stream_and_comes_back` (a driver killed and + started again by its job, the stream intact; the supervisor's restart of a + `drives` row is the lifecycle crate's host tests'), and + `netstack_holders_exported_to_children` on virtio and on QEMU's `e1000e`. +- **Read on the T14:** `i219_carries_netstacks_link` and + `i219_leaves_ulp_unarmed` passed on one boot at 8bdf49156 + (https://github.com/ToyOSOrg/evidence/blob/4d818727fb372e181c402149aeb2779808fa2ef2/p4-r1/metal-t14-judge.log). +- **S2, reported:** on the T14, the hop costs one CPU about 3,300 TSC + cycles a frame with a doorbell per frame, 920 per batch of 8 and 620 per + batch of 32, both directions alike: about 740,000, 2.6 million and 3.9 + million frames a second of one CPU at 2.42 GHz, against the 200,163 a + second each way full rate needs + (https://github.com/ToyOSOrg/evidence/blob/4a8d02243d626df929fe05365e991873574e0dc2/s2/README.md). **P5, netstack over several links.** Owner: the orchestrator; its defect is `issues/netstack-drives-one-card.md`. Depends on: P4. diff --git a/issues/toyos-has-its-own-network-stack.md b/issues/toyos-has-its-own-network-stack.md index 58f9af86a01..ac762d7a1be 100644 --- a/issues/toyos-has-its-own-network-stack.md +++ b/issues/toyos-has-its-own-network-stack.md @@ -24,7 +24,7 @@ What the node does not yet meet: - Of what a third party wrote, it has met QEMU's user network in a guest, on virtio and on QEMU's e1000e: its DHCP server, its ARP and the host kernel's TCP behind it (`netstack_streams`, `netstack_socket_churn`, `libc_sockets`). It has met no router and no switch. Exit: the T14's outbound rows are green on it. - It counts a DHCP message [udp] refused as `node.dhcp-unsent`, whatever the rule; the `dhcp.renew-unroutable` scenario owed above is not written. Exit: that scenario names the counter, or the node counts the renewal apart. - With the link down the DHCP client keeps its timers: every 4 to 64 s the node has a deadline, builds a DISCOVER that [udp] refuses, and counts it in `dhcp.tx.discover` and `node.dhcp-unsent`, from `Node::new` on. Exit: `toyos-dhcp`'s client is told the link went down and waits for it, and the node's first DISCOVER is the one that leaves. -- The node's link-down handling (`Node::link`, `system/netstack/node/src/lib.rs`) and its INIT-REBOOT, a held lease asked for again by REQUEST when the link returns (`Client::link_up`, `system/netstack/dhcp/src/lib.rs`), have never run on a real card or against a real DHCP server: host tests alone hold them, and no T14 boot has had its link go. Exit: one attended T14 boot with the cable pulled and returned, whose log shows `netstack: I219: link down`, then on the link's return the held lease verified by a REQUEST and its ACK, or taken again, with neither DHCP line that says this machine has no address, then the name's claim line, `netstack: mDNS: no host answered for toyos-t14.local; this machine answers as it`, again. +- The node's link-down handling (`Node::link`, `system/netstack/node/src/lib.rs`) and its INIT-REBOOT, a held lease asked for again by REQUEST when the link returns (`Client::link_up`, `system/netstack/dhcp/src/lib.rs`), have never run on a real card or against a real DHCP server: host tests alone hold them, and no T14 boot has had its link go. Exit: one attended T14 boot with the cable pulled and returned, whose log shows `i219: link down`, then on the link's return the held lease verified by a REQUEST and its ACK, or taken again, with neither DHCP line that says this machine has no address, then the name's claim line, `netstack: mDNS: no host answered for toyos-t14.local; this machine answers as it`, again. - The node's places are one number for every client: a program that connects, listens or binds in a loop takes them all, and so does a peer that keeps the connections [tcp] is finishing alive, each of which holds its stream's place (the line below on a client gone with nothing left in its pipe). The node has no word for the program behind a request, as `issues/netstack-lookup-slots-have-no-per-client-share.md` records of netd. Exit: a test of the node in which one program at its share of the places leaves another's connect, listen, accept and bind answered. - A port is one listener's, whatever address either listen named: a listen on a port a listener holds is refused `ListenRefused::InUse` (`system/netstack/node/src/listeners.rs`), so [tcp]'s rule that a SYN goes to the listener that named its address before one at every address (LS-09) is not reachable from the node, and one program's two listeners on one port at two addresses, which a host allows one user, are refused too. The node has no word for the program behind a listen: with LS-09 reachable, any program that reaches netstack would take every later connection of another program's listener at every address by naming the machine's address on its port. Exit: a listen carries its program, a second listener on a held port is allowed only to the program that holds every listener on it, and a test of the node finds one program's pair standing, a SYN handed to the one that named its address, and another program's listen on that port refused. - A listener's handshakes in progress are [tcp]'s `limits::LISTEN_PENDING`, each held until `limits::SYNACK_GIVE_UP`, and a SYN past them is dropped: 256 SYNs a minute from addresses that never answer shut a port to every other peer for as long as they keep coming. [tcp] has no SYN cookie and lets no older handshake go for a newer one. Exit: a test in `system/netstack/shard/tcp/tests/` in which a peer's handshake completes while LS-12's flood runs. diff --git a/lib/link/Cargo.toml b/lib/link/Cargo.toml new file mode 100644 index 00000000000..315dfb818b1 --- /dev/null +++ b/lib/link/Cargo.toml @@ -0,0 +1,30 @@ +# A member of the host workspace (root `Cargo.toml`). The link protocol +# between netstack and each NIC driver: the region's layout, both ends of its +# rings, the state words and the opening frames, all pure and host-tested; the +# region's mapping; and the pass every NIC driver runs over a device. + +[package] +name = "toyos-link" +description = "The link protocol between the network stack and each NIC driver: frames over one shared region's rings, the link's state, and the driver's side of it." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[features] +# Declared, never enabled by any build that ships: `build/ci.rs`'s `CONTROLS` +# runs it and demands `tests/ends_loom.rs`'s own FAILED line. The want's two +# swaps are `Relaxed`, so a full ring's producer can miss the room a release +# gave back while the consumer misses its want: a wake lost for good. +mutate-want-relaxed = [] + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } +# The rings on the region's header page: `toyos::transport`. +toyos = { path = "../../sdk/toyos" } + +[lints.rust] +warnings = "deny" + +[dev-dependencies] +# `tests/ends_loom.rs`: the full ring's want on two CPUs. +loom = "0.7" diff --git a/lib/link/src/driver.rs b/lib/link/src/driver.rs new file mode 100644 index 00000000000..4216b88ff89 --- /dev/null +++ b/lib/link/src/driver.rs @@ -0,0 +1,265 @@ +//! A NIC driver's side of a link: the session with the stack, carried over the +//! device one [`Nic`] drives. +//! +//! **One pass per wake**: the device's interrupt and link, then its received +//! frames into the receive ring while it has room, then the transmit ring's +//! frames into the device while it has room, then the stack's groups and its +//! ask for counters, then one doorbell if anything changed. A pass takes at +//! most a ring of frames each way, so a peer that keeps one full cannot keep +//! the other waiting. +//! +//! **A stack that hangs up is connected to again at once**, with a fresh +//! region, and the device stays up meanwhile. The stack hangs up on a link +//! whose driver wrote what no driver writes, and refuses that address's next +//! hello [`Refusal::Faulted`], on which this driver ends: its supervisor's +//! restart budget is what bounds a driver that keeps doing it. Any other +//! refusal, or a port closed for good, ends [`drive`]: nothing this driver +//! does changes either. + +use toyos::endow::{self, EndowError}; +use toyos::ipc::{Connection, FrameRx, RxStep}; +use toyos::poller::{Poller, READABLE}; +use toyos::shm::ReadBytes; +use toyos::{say, PciDev}; +use toyos_abi::syscall::SyscallError; + +use crate::ends::{Driver, Fault}; +use crate::layout::{RX_DEPTH, SLOT_BYTES, TX_DEPTH}; +use crate::region::Region; +use crate::state::{Counters, Group, Link}; +use crate::wire::{self, Address, Hello, Refusal}; +use crate::PORT; + +/// The device under a link, as its driver drives it. +pub trait Nic { + fn mac(&self) -> [u8; 6]; + + /// The claim, for the poller: readable means an interrupt has landed. + fn claim(&self) -> &PciDev; + + /// Take the interrupt and what the device owes since the last pass, and + /// answer the link as it is now. + fn begin_pass(&mut self) -> Link; + + /// Hand the next received frame to `take` and give its buffer back; + /// `false` when none waits. + fn receive(&mut self, take: &mut dyn FnMut(&[u8])) -> bool; + + /// Every buffer a pass's receives gave back reaches the device. + fn received(&mut self) {} + + /// How many frames the device takes now. + fn tx_room(&mut self) -> usize; + + /// Have the claim read ready when room returns, and answer the room there + /// is once that is so. + fn wake_on_room(&mut self) -> usize { + self.tx_room() + } + + /// Copy `frame` into the device and send it, for a caller + /// [`Self::tx_room`] answered room. + fn send(&mut self, frame: ReadBytes<'_>); + + /// How long until a pass is owed with no interrupt, in nanoseconds. + fn pass_due_in(&self) -> Option { + None + } + + /// Pass frames sent to `group`. + fn join(&mut self, group: Group); + + /// What the driver and the device counted so far. + fn counters(&mut self) -> Counters { + Counters::default() + } + + /// Say what changed of what the driver counts, once a pass. + fn report(&mut self) {} +} + +const TOKEN_CLAIM: u64 = 0; +const TOKEN_LINK: u64 = 1; + +/// Drive `nic` for the stack under `label`, its driver's name, until the +/// stack refuses it or its port closes for good. +/// +/// # Panics +/// On anything the stack writes that no stack of the protocol writes, on the +/// stack's refusal for what this driver wrote, on a device whose address is a +/// group's, and on a manifest that gives this driver no link: nothing drives +/// on from any of them. +pub fn drive(label: &str, nic: &mut impl Nic) { + loop { + let conn = match endow::service(PORT) { + Ok(conn) => conn, + Err(EndowError::ServerGone) => { + say!("{label}: the stack's port is closed for good; this device carries nothing"); + return; + } + Err(why) => panic!("{label}: the manifest gives this driver `{PORT}`, and it answered {why:?}"), + }; + let mut session = Session::open(label, nic, conn); + match session.serve(nic) { + Ended::Refused(Refusal::Faulted) => panic!("{label}: the stack ended this driver's link for what it wrote"), + Ended::Refused(why) => { + say!("{label}: the stack refused this link: {why:?}"); + return; + } + Ended::HungUp(why) => say!("{label}: the stack's connection ended ({why}); connecting again"), + } + } +} + +enum Ended { + Refused(Refusal), + HungUp(&'static str), +} + +struct Session<'a> { + label: &'a str, + conn: Connection, + region: Region, + end: Driver, + rx: FrameRx<4>, + opened: bool, + said: Option, + /// Received frames longer than a slot, dropped. + oversize: u64, + oversize_said: u64, +} + +impl<'a> Session<'a> { + /// A fresh region and its end, said hello with over `conn`. + fn open(label: &'a str, nic: &impl Nic, conn: Connection) -> Self { + let region = Region::create().unwrap_or_else(|why| panic!("{label}: no region for a link: {why:?}")); + let end = Driver::new(region.words()); + let mac = Address::new(nic.mac()) + .unwrap_or_else(|| panic!("{label}: the device's address is a group's, which no link carries")); + let hello = Hello { mac, label: label.into() }; + let shared = region.share().unwrap_or_else(|why| panic!("{label}: the link's region would not share: {why:?}")); + // A connection that will not take the hello is one whose stack has + // gone: its hang-up is read in the first pass. + let _ = conn.try_send_bytes_with(wire::MSG_HELLO, &hello.encode(), [shared]); + Self { label, conn, region, end, rx: FrameRx::new(), opened: false, said: None, oversize: 0, oversize_said: 0 } + } + + fn serve(&mut self, nic: &mut impl Nic) -> Ended { + let poller = Poller::new(2); + loop { + let ring = self.pass(nic).unwrap_or_else(|why| panic!("{}: the stack wrote {why}", self.label)); + if ring && self.opened { + match self.conn.write_nonblock(&[1]) { + Ok(_) | Err(SyscallError::WouldBlock) => {} + Err(_) => return Ended::HungUp("it would not take a doorbell"), + } + } + poller.watch(nic.claim(), READABLE, TOKEN_CLAIM); + poller.watch(&self.conn, READABLE, TOKEN_LINK); + poller.wait(1, nic.pass_due_in().unwrap_or(u64::MAX), |_| {}); + if let Some(ended) = self.hear() { + return ended; + } + } + } + + /// One pass; whether the stack is to be rung. + fn pass(&mut self, nic: &mut impl Nic) -> Result { + let page = self.region.words(); + let bytes = self.region.bytes(); + let mut ring = false; + + let link = nic.begin_pass(); + if self.said != Some(link) { + self.end.set_link(page, link); + self.said = Some(link); + ring = true; + } + + let mut room = self.end.room(page)?; + for _ in 0..RX_DEPTH { + if room == 0 { + room = self.end.want_room(page)?; + if room == 0 { + break; + } + } + let slot = self.end.slot(); + let mut len = 0; + let mut oversize = false; + let took = nic.receive(&mut |frame| match frame.len() { + 0 => {} + n if n > SLOT_BYTES => oversize = true, + n => { + bytes.write(slot.offset, frame); + len = n; + } + }); + if !took { + break; + } + self.oversize += u64::from(oversize); + if len > 0 { + self.end.receive(page, len)?; + room -= 1; + } + } + nic.received(); + ring |= self.end.publish(page); + + for _ in 0..TX_DEPTH { + if nic.tx_room() == 0 && nic.wake_on_room() == 0 { + break; + } + let Some(frame) = self.end.to_send(page)? else { break }; + nic.send(*bytes.sub(frame.offset, frame.len)); + } + ring |= self.end.release(page); + + while let Some(group) = self.end.joined(page)? { + nic.join(group); + } + if let Some(asked) = self.end.counters_asked(page) { + self.end.answer(page, asked, &nic.counters()); + ring = true; + } + nic.report(); + if self.oversize != self.oversize_said { + say!("{}: dropped {} received frame(s) longer than a link's {SLOT_BYTES}-byte slot", self.label, self.oversize); + self.oversize_said = self.oversize; + } + Ok(ring) + } + + /// Read the stack's answer, then its doorbells; the end of the session + /// if that is what came. + fn hear(&mut self) -> Option { + if !self.opened { + return match self.rx.pump(&self.conn) { + RxStep::Idle => None, + RxStep::Eof => Some(Ended::HungUp("its process ended before it answered")), + RxStep::Frame { msg_type: wire::MSG_OPENED, payload_len: 0, handles: 0 } => { + self.opened = true; + say!("{}: the link is open", self.label); + self.hear() + } + RxStep::Frame { msg_type: wire::MSG_REFUSED, payload_len, handles: 0 } => { + match Refusal::decode(self.rx.payload(payload_len)) { + Some(why) => Some(Ended::Refused(why)), + None => panic!("{}: the stack refused this link with a word no stack writes", self.label), + } + } + RxStep::Frame { .. } | RxStep::Malformed => panic!("{}: the stack answered a hello with a frame no stack writes", self.label), + }; + } + let mut sink = [0u8; 64]; + loop { + match self.conn.read_nonblock(&mut sink) { + Ok(0) => return Some(Ended::HungUp("its process ended")), + Ok(_) => {} + Err(SyscallError::WouldBlock) => return None, + Err(_) => return Some(Ended::HungUp("it refused a read")), + } + } + } +} diff --git a/lib/link/src/ends.rs b/lib/link/src/ends.rs new file mode 100644 index 00000000000..ab41a0a2bfd --- /dev/null +++ b/lib/link/src/ends.rs @@ -0,0 +1,310 @@ +//! The stack's end and the driver's end of a link's region. +//! +//! **Each end holds cursors, never the region**: every call is handed the +//! region's words, and an end answers the [`Span`] of a slot its adapter +//! copies a frame into or out of, once. +//! +//! **Everything the peer writes is bounded before it is believed**: a cursor +//! by the transport, a frame's length by the slot, the link word by +//! [`Link::decode`], and the groups by their count and their encoding. A +//! [`Fault`] names what the peer wrote, and ends the session. Counter values +//! are the one exception: they are displayed, never acted on. +//! +//! **A full ring wants its room back.** The producer that finds no room sets +//! its ring's want and looks again; the consumer that releases takes the want +//! and is told to ring. Both are one `AcqRel` swap of the want's word, so +//! neither misses the other: in that word's order, the consumer's swap after +//! the producer's takes the want, and the producer's after the consumer's +//! acquires the release before it, so its look finds the room. + +use core::fmt; +use core::sync::atomic::Ordering; + +use toyos::transport::{Consumer, Producer, Span, Violation, Word}; + +use crate::layout::{self, RX, RX_DEPTH, SLOT_BYTES, TX, TX_DEPTH, WORDS}; +use crate::state::{Counter, Counters, Group, Link}; + +/// The peer wrote what no peer of the protocol writes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Fault { + Ring(Violation), + /// A frame of no bytes, or longer than its slot. + Length(u32), + /// A link word no driver writes. + Link(u32), + /// More groups than the region holds, or fewer than the driver applied. + Groups(u32), + /// A group's words that name no group address. + Group, +} + +impl From for Fault { + fn from(violation: Violation) -> Self { + Self::Ring(violation) + } +} + +impl fmt::Display for Fault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Ring(violation) => write!(f, "a ring's cursor out of bounds ({violation:?})"), + Self::Length(len) => write!(f, "a {len}-byte frame, where a frame is 1 to {SLOT_BYTES} bytes"), + Self::Link(word) => write!(f, "link word {word:#010x}, which names no link"), + Self::Groups(count) => write!(f, "{count} multicast groups joined"), + Self::Group => write!(f, "a multicast group that is no group address"), + } + } +} + +/// A frame's length as its entry says it, bounded by its slot. +fn length(word: u32) -> Result { + match usize::try_from(word) { + Ok(len) if (1..=SLOT_BYTES).contains(&len) => Ok(len), + _ => Err(Fault::Length(word)), + } +} + +/// The first `len` bytes of `slot`. +fn frame(slot: Span, len: usize) -> Span { + Span { offset: slot.offset, len } +} + +/// How the want's word is swapped. +#[cfg(not(feature = "mutate-want-relaxed"))] +const WANT: Ordering = Ordering::AcqRel; +#[cfg(feature = "mutate-want-relaxed")] +const WANT: Ordering = Ordering::Relaxed; + +/// Set `want`, and answer the room the ring has after it. +fn want(page: &[W; WORDS], want: usize, room: impl FnOnce() -> Result) -> Result { + page[want].swap(1, WANT); + Ok(room()?) +} + +/// Whether the peer wanted the room just released, taking its want. +fn wanted(page: &[W; WORDS], want: usize) -> bool { + page[want].swap(0, WANT) != 0 +} + +/// The stack's end: received frames in, frames to send out. +pub struct Stack { + rx: Consumer<1, RX_DEPTH, WORDS>, + tx: Producer<1, TX_DEPTH, WORDS>, + received: u32, + sent: u32, + joined: u32, + asked: u32, +} + +impl Stack { + /// This end of a region the driver sent, every word it owns set to 0. + pub fn new(page: &[W; WORDS]) -> Self { + for at in [layout::TX_WANT, layout::GROUPS_JOINED, layout::COUNTERS_ASKED] { + page[at].store(0, Ordering::Relaxed); + } + Self { rx: Consumer::new(page, RX), tx: Producer::new(page, TX), received: 0, sent: 0, joined: 0, asked: 0 } + } + + /// The link as the driver last said it. + pub fn link(&self, page: &[W; WORDS]) -> Result { + let word = page[layout::LINK].load(Ordering::Acquire); + Link::decode(word).ok_or(Fault::Link(word)) + } + + /// The bytes of the next received frame, or `None` for none. They are + /// the stack's until [`Self::release`]. + pub fn received(&mut self, page: &[W; WORDS]) -> Result, Fault> { + let Some([len]) = self.rx.pop(page)? else { return Ok(None) }; + let slot = layout::rx_slot(self.received).span(); + self.received = self.received.wrapping_add(1); + Ok(Some(frame(slot, length(len)?))) + } + + /// Give every frame taken back to the driver; `true` when it waits for + /// the room and is to be rung. + pub fn release(&mut self, page: &[W; WORDS]) -> bool { + self.rx.release(page); + wanted(page, layout::RX_WANT) + } + + /// How many frames may be sent before the driver takes more. + pub fn room(&mut self, page: &[W; WORDS]) -> Result { + Ok(self.tx.space(page)?) + } + + /// [`Self::room`], having asked to be rung when the driver frees some. + pub fn want_room(&mut self, page: &[W; WORDS]) -> Result { + want(page, layout::TX_WANT, || self.tx.space(page)) + } + + /// The slot the next frame sent is copied into. + pub fn slot(&self) -> Span { + layout::tx_slot(self.sent).span() + } + + /// Hand the driver the `len` bytes just copied into [`Self::slot`]. + /// + /// # Panics + /// On a frame no slot holds, or a ring [`Self::room`] did not answer room + /// in: the stack's own bug. + pub fn send(&mut self, page: &[W; WORDS], len: usize) -> Result<(), Fault> { + assert!((1..=SLOT_BYTES).contains(&len), "link: a {len}-byte frame offered to a {SLOT_BYTES}-byte slot"); + let word = u32::try_from(len).unwrap_or_else(|_| unreachable!("a slot's length is a u32")); + assert!(self.tx.push(page, [word])?, "link: a frame offered to a ring with no room"); + self.sent = self.sent.wrapping_add(1); + Ok(()) + } + + /// Publish every frame sent; `true` when there was one, and the driver + /// is to be rung. + pub fn publish(&mut self, page: &[W; WORDS]) -> bool { + self.tx.publish(page) + } + + /// Ask the driver to pass frames sent to `group`; `false` when the + /// region holds no more groups. The driver is to be rung. + pub fn join(&mut self, page: &[W; WORDS], group: Group) -> bool { + let Ok(at) = usize::try_from(self.joined) else { return false }; + if at >= layout::MAX_GROUPS { + return false; + } + let [lo, hi] = group.encode(); + page[layout::GROUPS + 2 * at].store(lo, Ordering::Relaxed); + page[layout::GROUPS + 2 * at + 1].store(hi, Ordering::Relaxed); + self.joined = self.joined.wrapping_add(1); + page[layout::GROUPS_JOINED].store(self.joined, Ordering::Release); + true + } + + /// Ask the driver for its counters. The driver is to be rung, and + /// [`Self::counters`] reads them once it answered. + pub fn ask_counters(&mut self, page: &[W; WORDS]) { + self.asked = self.asked.wrapping_add(1); + page[layout::COUNTERS_ASKED].store(self.asked, Ordering::Release); + } + + /// The counters the driver answered the last ask with, or `None` while it + /// has not answered it. + pub fn counters(&self, page: &[W; WORDS]) -> Option { + if page[layout::COUNTERS_ANSWERED].load(Ordering::Acquire) != self.asked { + return None; + } + let present = page[layout::COUNTERS_PRESENT].load(Ordering::Relaxed); + let mut counters = Counters::default(); + for counter in Counter::ALL.into_iter().filter(|c| present & 1 << c.index() != 0) { + let at = layout::COUNTERS + 2 * counter.index(); + let lo = page[at].load(Ordering::Relaxed); + let hi = page[at + 1].load(Ordering::Relaxed); + counters.set(counter, u64::from(hi) << 32 | u64::from(lo)); + } + Some(counters) + } +} + +/// The driver's end: received frames out, frames to send in. +pub struct Driver { + rx: Producer<1, RX_DEPTH, WORDS>, + tx: Consumer<1, TX_DEPTH, WORDS>, + received: u32, + sent: u32, + applied: u32, +} + +impl Driver { + /// This end of a region the driver made, every word it owns set: its + /// link down and no counters answered. + pub fn new(page: &[W; WORDS]) -> Self { + for at in [layout::LINK, layout::RX_WANT, layout::COUNTERS_ANSWERED, layout::COUNTERS_PRESENT] { + page[at].store(0, Ordering::Relaxed); + } + Self { rx: Producer::new(page, RX), tx: Consumer::new(page, TX), received: 0, sent: 0, applied: 0 } + } + + /// Say the link. The stack is to be rung. + pub fn set_link(&self, page: &[W; WORDS], link: Link) { + page[layout::LINK].store(link.encode(), Ordering::Release); + } + + /// How many frames may be received before the stack takes more. + pub fn room(&mut self, page: &[W; WORDS]) -> Result { + Ok(self.rx.space(page)?) + } + + /// [`Self::room`], having asked to be rung when the stack frees some. + pub fn want_room(&mut self, page: &[W; WORDS]) -> Result { + want(page, layout::RX_WANT, || self.rx.space(page)) + } + + /// The slot the next received frame is copied into. + pub fn slot(&self) -> Span { + layout::rx_slot(self.received).span() + } + + /// Hand the stack the `len` bytes just copied into [`Self::slot`]. + /// + /// # Panics + /// As [`Stack::send`]: the driver's own bug. + pub fn receive(&mut self, page: &[W; WORDS], len: usize) -> Result<(), Fault> { + assert!((1..=SLOT_BYTES).contains(&len), "link: a {len}-byte frame offered to a {SLOT_BYTES}-byte slot"); + let word = u32::try_from(len).unwrap_or_else(|_| unreachable!("a slot's length is a u32")); + assert!(self.rx.push(page, [word])?, "link: a frame offered to a ring with no room"); + self.received = self.received.wrapping_add(1); + Ok(()) + } + + /// Publish every frame received; `true` when there was one, and the + /// stack is to be rung. + pub fn publish(&mut self, page: &[W; WORDS]) -> bool { + self.rx.publish(page) + } + + /// The bytes of the next frame to send, or `None` for none. They are the + /// driver's until [`Self::release`]. + pub fn to_send(&mut self, page: &[W; WORDS]) -> Result, Fault> { + let Some([len]) = self.tx.pop(page)? else { return Ok(None) }; + let slot = layout::tx_slot(self.sent).span(); + self.sent = self.sent.wrapping_add(1); + Ok(Some(frame(slot, length(len)?))) + } + + /// Give every frame taken back to the stack; `true` when it waits for the + /// room and is to be rung. + pub fn release(&mut self, page: &[W; WORDS]) -> bool { + self.tx.release(page); + wanted(page, layout::TX_WANT) + } + + /// The next group the stack joined since the last call, or `None`. + pub fn joined(&mut self, page: &[W; WORDS]) -> Result, Fault> { + let count = page[layout::GROUPS_JOINED].load(Ordering::Acquire); + let held = usize::try_from(count).is_ok_and(|n| n <= layout::MAX_GROUPS); + if !held || count < self.applied { + return Err(Fault::Groups(count)); + } + if count == self.applied { + return Ok(None); + } + let at = layout::GROUPS + 2 * usize::try_from(self.applied).unwrap_or_else(|_| unreachable!("below MAX_GROUPS")); + let words = [page[at].load(Ordering::Relaxed), page[at + 1].load(Ordering::Relaxed)]; + self.applied = self.applied.wrapping_add(1); + Group::decode(words).map(Some).ok_or(Fault::Group) + } + + /// The stack's ask for counters, while it is unanswered. + pub fn counters_asked(&self, page: &[W; WORDS]) -> Option { + let asked = page[layout::COUNTERS_ASKED].load(Ordering::Acquire); + (asked != page[layout::COUNTERS_ANSWERED].load(Ordering::Relaxed)).then_some(asked) + } + + /// Answer the ask `asked` with `counters`. The stack is to be rung. + pub fn answer(&self, page: &[W; WORDS], asked: u32, counters: &Counters) { + for (counter, value) in counters.iter() { + let at = layout::COUNTERS + 2 * counter.index(); + page[at].store(value as u32, Ordering::Relaxed); + page[at + 1].store((value >> 32) as u32, Ordering::Relaxed); + } + page[layout::COUNTERS_PRESENT].store(counters.present(), Ordering::Relaxed); + page[layout::COUNTERS_ANSWERED].store(asked, Ordering::Release); + } +} diff --git a/lib/link/src/layout.rs b/lib/link/src/layout.rs new file mode 100644 index 00000000000..d8b1aab19dc --- /dev/null +++ b/lib/link/src/layout.rs @@ -0,0 +1,107 @@ +//! Where everything is on a link's region, in 32-bit words from its start. +//! +//! Each cache line of the header is written by one side: the stack's cursors +//! and requests, and the driver's cursors, state and answers, never share one. + +use toyos::transport::{Geometry, Place, Run}; + +/// A link's whole region: `toyos::transport`'s region length. +pub const REGION_BYTES: usize = Geometry::BYTES as usize; + +/// One frame's slot in the arena, and so the longest frame either way. +pub const SLOT_BYTES: usize = 2048; + +/// The arena, cut into frame slots after the header page. +pub const ARENA: Geometry = match Geometry::new(SLOT_BYTES as u32) { + Some(arena) => arena, + None => panic!("a slot is no longer than the arena"), +}; + +/// Frames each ring holds. Received frame `n` is in receive slot +/// `n % RX_DEPTH`, and sent frame `n` in transmit slot `n % TX_DEPTH`: a slot +/// is its producer's again exactly when the consumer releases its entry. +pub const RX_DEPTH: u32 = 256; +pub const TX_DEPTH: u32 = 256; + +/// The stack's line: the receive ring's head and the transmit ring's tail. +pub const RX_HEAD: usize = 0; +pub const TX_TAIL: usize = 16; +/// The driver's line: the receive ring's tail and the transmit ring's head. +pub const RX_TAIL: usize = 32; +pub const TX_HEAD: usize = 48; + +/// The driver's state: its link ([`crate::state::Link`]), the receive ring's +/// want, which the driver sets when the ring is full and the stack takes when +/// it releases, and the counters it answered. +pub const LINK: usize = 64; +pub const RX_WANT: usize = 65; +pub const COUNTERS_ANSWERED: usize = 66; +pub const COUNTERS_PRESENT: usize = 67; + +/// The stack's requests: the transmit ring's want, which the stack sets when +/// the ring is full and the driver takes when it releases; the multicast +/// groups it joined; and the counters it asked for. +pub const TX_WANT: usize = 80; +pub const GROUPS_JOINED: usize = 81; +pub const COUNTERS_ASKED: usize = 82; + +/// The groups the stack joined, two words each, in the order it joined them. +pub const GROUPS: usize = 96; +pub const MAX_GROUPS: usize = 8; + +/// The driver's counters, two words each, low word first +/// ([`crate::state::Counter`]). +pub const COUNTERS: usize = 128; + +/// The rings' entries: one word each, the frame's length. +pub const RX_ENTRIES: usize = 256; +pub const TX_ENTRIES: usize = RX_ENTRIES + RX_DEPTH as usize; + +/// Every word the header uses. +pub const WORDS: usize = TX_ENTRIES + TX_DEPTH as usize; + +pub const RX: Place<1, RX_DEPTH, WORDS> = Place::new::(); +pub const TX: Place<1, TX_DEPTH, WORDS> = Place::new::(); + +/// The slot of received frame `n`. +pub fn rx_slot(n: u32) -> Run { + slot(n % RX_DEPTH) +} + +/// The slot of sent frame `n`, after every receive slot. +pub fn tx_slot(n: u32) -> Run { + slot(RX_DEPTH + n % TX_DEPTH) +} + +fn slot(index: u32) -> Run { + match ARENA.run(index, 1) { + Some(run) => run, + None => unreachable!("both rings' slots fit the arena"), + } +} + +const _: () = { + assert!(WORDS * 4 <= Geometry::HEADER_BYTES as usize); + assert!(GROUPS + 2 * MAX_GROUPS <= COUNTERS); + assert!(COUNTERS + 2 * crate::state::Counter::ALL.len() <= RX_ENTRIES); + assert!(RX_DEPTH + TX_DEPTH <= ARENA.slots()); +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// No two slots of either ring overlap, and every one is inside the region. + #[test] + fn every_slot_is_its_own() { + let mut spans: Vec<_> = (0..RX_DEPTH).map(rx_slot).chain((0..TX_DEPTH).map(tx_slot)).map(|r| r.span()).collect(); + spans.sort_by_key(|s| s.offset); + for pair in spans.windows(2) { + assert!(pair[0].offset + pair[0].len <= pair[1].offset); + } + let last = spans.last().unwrap(); + assert!(last.offset + last.len <= REGION_BYTES); + assert_eq!(rx_slot(RX_DEPTH), rx_slot(0), "frame n's slot repeats a ring later"); + assert_eq!(tx_slot(TX_DEPTH + 3), tx_slot(3)); + } +} diff --git a/lib/link/src/lib.rs b/lib/link/src/lib.rs new file mode 100644 index 00000000000..fb6a452d8dd --- /dev/null +++ b/lib/link/src/lib.rs @@ -0,0 +1,33 @@ +//! The link protocol between the network stack and each NIC driver. +//! +//! **A link is one connection and one region.** A driver connects to the +//! stack's [`PORT`] and sends a region it made with its hello +//! ([`wire`]). The region's header page holds two `toyos::transport` rings of +//! frame lengths, received frames from the driver and frames to send from the +//! stack, and the words each side states and asks with ([`layout`], +//! [`state`]); its arena holds a slot per ring entry, which a frame is copied +//! into and out of once on each side ([`ends`]). +//! +//! **A doorbell is a byte on the connection**, written after what it +//! announces is in the region. The connection's hang-up is the link's end: +//! the stack keeps every connection it carries across it, and the driver +//! started again opens a new link. +//! +//! The rings, the frames' lengths and every state word are pure and +//! host-tested; [`region`] maps the region and [`driver`] carries a device's +//! frames over it. + +#![cfg_attr(not(test), no_std)] +#![deny(unsafe_code)] + +extern crate alloc; + +pub mod driver; +pub mod ends; +pub mod layout; +pub mod region; +pub mod state; +pub mod wire; + +/// The name the stack is served to NIC drivers under. +pub const PORT: &str = "link"; diff --git a/lib/link/src/region.rs b/lib/link/src/region.rs new file mode 100644 index 00000000000..6aa5c818e0e --- /dev/null +++ b/lib/link/src/region.rs @@ -0,0 +1,55 @@ +//! A link's region, mapped into this process. +//! +//! **Nothing on it is reached through a Rust reference but the header's +//! words**, and those are `&AtomicU32`: an atomic's `UnsafeCell` is what +//! withdraws `noalias` and `readonly` from memory a peer writes. Frames are +//! reached through [`Bytes`], one copy each. + +#![allow(unsafe_code)] + +use core::sync::atomic::AtomicU32; + +use toyos::shm::{Bytes, Lent, SharedMemory}; +use toyos_abi::syscall::SyscallError; + +use crate::layout::{REGION_BYTES, WORDS}; + +pub struct Region { + memory: SharedMemory, +} + +impl Region { + /// A fresh region, for a driver opening a link. + pub fn create() -> Result { + Ok(Self { memory: SharedMemory::create(REGION_BYTES)? }) + } + + /// A region a driver sent, refused unless it is memory the kernel + /// allocated and exactly a region long. + pub fn lent(handle: toyos::OwnedHandle) -> Result { + Ok(Self { memory: SharedMemory::lent(handle, REGION_BYTES)? }) + } + + /// The header's words, as the ends take them. + pub fn words(&self) -> &[AtomicU32; WORDS] { + let base = self.memory.as_ptr(); + assert!((base as usize).is_multiple_of(align_of::())); + // SAFETY: the mapping is `REGION_BYTES` long and lives as long as + // `self.memory`, which the returned borrow cannot outlive; `WORDS` + // words fit its header (`layout`'s assertion); the base is aligned; + // and an atomic is the one type that may alias memory another process + // writes. + unsafe { &*(base as *const [AtomicU32; WORDS]) } + } + + /// The whole region's bytes, which every [`toyos::transport::Span`] an + /// end answers is inside. + pub fn bytes(&self) -> Bytes<'_> { + self.memory.bytes() + } + + /// A second handle to the region, for the hello. + pub fn share(&self) -> Result { + self.memory.share() + } +} diff --git a/lib/link/src/state.rs b/lib/link/src/state.rs new file mode 100644 index 00000000000..fb33c6c7b1f --- /dev/null +++ b/lib/link/src/state.rs @@ -0,0 +1,218 @@ +//! What the driver's state words say, and their encodings. + +/// The link as its driver reads it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Link { + Down, + Up { mbps: u32, full_duplex: bool }, + /// The device reports no link at all, as virtio's network device without + /// `VIRTIO_NET_F_STATUS` does; the stack takes it as up. + Unreported, +} + +const DOWN: u32 = 0; +const UNREPORTED: u32 = 1; +const UP: u32 = 2; +const FULL_DUPLEX: u32 = 1 << 2; +const MBPS_SHIFT: u32 = 8; + +impl Link { + pub fn is_up(self) -> bool { + !matches!(self, Self::Down) + } + + /// Its word. A speed past what eight bits short of a word holds is + /// written as the most that does. + pub fn encode(self) -> u32 { + match self { + Self::Down => DOWN, + Self::Unreported => UNREPORTED, + Self::Up { mbps, full_duplex } => { + let mbps = mbps.clamp(1, u32::MAX >> MBPS_SHIFT); + UP | if full_duplex { FULL_DUPLEX } else { 0 } | mbps << MBPS_SHIFT + } + } + } + + /// The link a driver's word names, or `None` for a word no driver writes. + pub fn decode(word: u32) -> Option { + let reserved = word & 0xf8; + let mbps = word >> MBPS_SHIFT; + match word & 0b11 { + DOWN if word == DOWN => Some(Self::Down), + UNREPORTED if word == UNREPORTED => Some(Self::Unreported), + UP if reserved == 0 && mbps > 0 => Some(Self::Up { mbps, full_duplex: word & FULL_DUPLEX != 0 }), + _ => None, + } + } +} + +/// A count a driver may report, under the name `inspect` reads it by. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Counter { + DescriptorsSent, + DescriptorsReceived, + DescriptorsStranded, + TransmitFull, + TransmitWakeArmed, + TransmitWakeTaken, + TransmitFullNanos, + TransmitUnreported, + TransmitUnreportedMax, + WireSent, + WireReceived, + WireSeen, + ErrorsMissed, + ErrorsCrc, +} + +impl Counter { + pub const ALL: [Self; 14] = [ + Self::DescriptorsSent, + Self::DescriptorsReceived, + Self::DescriptorsStranded, + Self::TransmitFull, + Self::TransmitWakeArmed, + Self::TransmitWakeTaken, + Self::TransmitFullNanos, + Self::TransmitUnreported, + Self::TransmitUnreportedMax, + Self::WireSent, + Self::WireReceived, + Self::WireSeen, + Self::ErrorsMissed, + Self::ErrorsCrc, + ]; + + pub fn name(self) -> &'static str { + match self { + Self::DescriptorsSent => "descriptors.sent", + Self::DescriptorsReceived => "descriptors.received", + Self::DescriptorsStranded => "descriptors.stranded", + Self::TransmitFull => "transmit.full", + Self::TransmitWakeArmed => "transmit.wake_armed", + Self::TransmitWakeTaken => "transmit.wake_taken", + Self::TransmitFullNanos => "transmit.full.ns", + Self::TransmitUnreported => "transmit.unreported", + Self::TransmitUnreportedMax => "transmit.unreported.max", + Self::WireSent => "wire.sent", + Self::WireReceived => "wire.received", + Self::WireSeen => "wire.seen", + Self::ErrorsMissed => "errors.missed", + Self::ErrorsCrc => "errors.crc", + } + } + + pub(crate) fn index(self) -> usize { + self as usize + } +} + +/// What a driver counted, each counter it reports with its value. The values +/// are the driver's word: read for display, never for a decision. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Counters { + values: [Option; Counter::ALL.len()], +} + +impl Counters { + pub fn set(&mut self, counter: Counter, value: u64) { + self.values[counter.index()] = Some(value); + } + + pub fn get(&self, counter: Counter) -> Option { + self.values[counter.index()] + } + + /// Every counter reported, in [`Counter::ALL`]'s order. + pub fn iter(&self) -> impl Iterator + '_ { + Counter::ALL.into_iter().filter_map(|c| self.get(c).map(|v| (c, v))) + } + + /// Which counters are reported, a bit each. + pub(crate) fn present(&self) -> u32 { + Counter::ALL.into_iter().filter(|c| self.get(*c).is_some()).fold(0, |mask, c| mask | 1 << c.index()) + } +} + +/// A multicast group's address, as the stack joins it and the driver passes +/// frames sent to it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Group([u8; 6]); + +impl Group { + /// `None` for an address that is no group's: its first byte's low bit, + /// IEEE 802's individual/group bit, clear. + pub fn new(mac: [u8; 6]) -> Option { + (mac[0] & 1 == 1).then_some(Self(mac)) + } + + pub fn mac(self) -> [u8; 6] { + self.0 + } + + pub(crate) fn encode(self) -> [u32; 2] { + let m = self.0; + [u32::from_le_bytes([m[0], m[1], m[2], m[3]]), u32::from_le_bytes([m[4], m[5], 0, 0])] + } + + pub(crate) fn decode(words: [u32; 2]) -> Option { + let [a, b, c, d] = words[0].to_le_bytes(); + let [e, f, rest @ ..] = words[1].to_le_bytes(); + if rest != [0, 0] { + return None; + } + Self::new([a, b, c, d, e, f]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_link_reads_back_as_written() { + for link in [ + Link::Down, + Link::Unreported, + Link::Up { mbps: 10, full_duplex: false }, + Link::Up { mbps: 1000, full_duplex: true }, + Link::Up { mbps: u32::MAX >> 8, full_duplex: true }, + ] { + assert_eq!(Link::decode(link.encode()), Some(link)); + } + assert_eq!(Link::decode(Link::Up { mbps: u32::MAX, full_duplex: false }.encode()), Some(Link::Up { mbps: u32::MAX >> 8, full_duplex: false })); + } + + /// Every word no driver writes is refused: an up link of no speed, a + /// reserved bit, a down or unreported link carrying fields, a fourth tag. + #[test] + fn a_word_no_driver_writes_names_no_link() { + for word in [UP, UP | FULL_DUPLEX, UP | 1 << 3 | 1 << 8, DOWN | 1 << 8, UNREPORTED | FULL_DUPLEX, 3, 3 | 1 << 8, u32::MAX] { + assert_eq!(Link::decode(word), None, "{word:#x}"); + } + } + + #[test] + fn a_group_is_a_group_address() { + let mdns = [0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb]; + let group = Group::new(mdns).unwrap(); + assert_eq!(Group::decode(group.encode()), Some(group)); + assert_eq!(Group::new([0x52, 0x54, 0, 0x12, 0x34, 0x56]), None, "an individual address"); + let [lo, hi] = group.encode(); + assert_eq!(Group::decode([lo, hi | 1 << 16]), None, "bytes past the address"); + } + + #[test] + fn counters_report_only_what_was_set() { + let mut counters = Counters::default(); + assert_eq!(counters.present(), 0); + counters.set(Counter::ErrorsCrc, 3); + counters.set(Counter::DescriptorsSent, u64::MAX); + assert_eq!(counters.iter().collect::>(), [(Counter::DescriptorsSent, u64::MAX), (Counter::ErrorsCrc, 3)]); + assert_eq!(counters.present(), 1 | 1 << 13); + let names: std::collections::BTreeSet<_> = Counter::ALL.iter().map(|c| c.name()).collect(); + assert_eq!(names.len(), Counter::ALL.len()); + assert!(Counter::ALL.iter().enumerate().all(|(i, c)| c.index() == i)); + } +} diff --git a/lib/link/src/wire.rs b/lib/link/src/wire.rs new file mode 100644 index 00000000000..7673a8a5591 --- /dev/null +++ b/lib/link/src/wire.rs @@ -0,0 +1,142 @@ +//! The frames a link is opened with, before the region carries anything. +//! +//! **One way in, one answer.** A driver says who it is ([`MSG_HELLO`]) and +//! sends the region it made with it; the stack answers [`MSG_OPENED`] or +//! [`MSG_REFUSED`] with a [`Refusal`]. After `MSG_OPENED` the connection +//! carries nothing but doorbell bytes, each way, and its hang-up ends the +//! link. + +use alloc::string::String; +use alloc::vec::Vec; + +/// A device's own address: an individual one, its first byte's low bit, +/// IEEE 802's individual/group bit, clear. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Address([u8; 6]); + +impl Address { + /// `None` for a group's address, the broadcast one among them. + pub fn new(bytes: [u8; 6]) -> Option { + (bytes[0] & 1 == 0).then_some(Self(bytes)) + } + + pub fn bytes(self) -> [u8; 6] { + self.0 + } +} + +/// Driver to stack: a [`Hello`]. Handles: the region. +pub const MSG_HELLO: u32 = 1; +/// Stack to driver: the link is open. No payload. +pub const MSG_OPENED: u32 = 2; +/// Stack to driver: refused, a [`Refusal`]'s word. +pub const MSG_REFUSED: u32 = 3; + +/// The longest driver name a hello carries. +pub const MAX_LABEL: usize = 32; + +/// Who a driver is: its device's address, which is an individual one, and its +/// name for the stack's lines: ASCII letters, digits and `-`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Hello { + pub mac: Address, + pub label: String, +} + +impl Hello { + pub const MAX_BYTES: usize = 6 + MAX_LABEL; + + pub fn encode(&self) -> Vec { + let mut out = Vec::with_capacity(6 + self.label.len()); + out.extend_from_slice(&self.mac.bytes()); + out.extend_from_slice(self.label.as_bytes()); + out + } + + /// `None` for bytes [`Self::encode`] does not make: too short, a group's + /// address, or a name that is empty, too long or not one. + pub fn decode(bytes: &[u8]) -> Option { + let (mac, label) = bytes.split_first_chunk::<6>()?; + let mac = Address::new(*mac)?; + let named = |b: &u8| b.is_ascii_alphanumeric() || *b == b'-'; + if label.is_empty() || label.len() > MAX_LABEL || !label.iter().all(named) { + return None; + } + Some(Self { mac, label: String::from(core::str::from_utf8(label).ok()?) }) + } +} + +/// Why the stack would not open a link. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// The stack drives one link, and its address is another's, or a link of + /// this address is open and alive (`issues/netstack-drives-one-card.md`). + OneLink, + /// The stack ended this address's last link for what its driver wrote. + /// The driver ends on it, so its supervisor's restart budget bounds a + /// driver that keeps writing what no driver writes. + Faulted, + /// A hello this protocol does not make, or a region that is not one. + Malformed, +} + +impl Refusal { + pub fn encode(self) -> [u8; 4] { + match self { + Self::OneLink => 1u32, + Self::Malformed => 2, + Self::Faulted => 3, + } + .to_le_bytes() + } + + pub fn decode(bytes: &[u8]) -> Option { + match u32::from_le_bytes(bytes.try_into().ok()?) { + 1 => Some(Self::OneLink), + 2 => Some(Self::Malformed), + 3 => Some(Self::Faulted), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mac(bytes: [u8; 6]) -> Address { + Address::new(bytes).unwrap() + } + + #[test] + fn a_hello_reads_back_as_written() { + let hello = Hello { mac: mac([0x52, 0x54, 0, 0x12, 0x34, 0x56]), label: "virtio-net".into() }; + assert_eq!(Hello::decode(&hello.encode()), Some(hello)); + let longest = Hello { mac: mac([2; 6]), label: "a".repeat(MAX_LABEL) }; + assert_eq!(Hello::decode(&longest.encode()), Some(longest)); + } + + /// Each way a hello's bytes are not one a driver makes is refused, a + /// group's address among them: the node is made with the address. + #[test] + fn a_hello_no_driver_writes_is_refused() { + let named = |mac: [u8; 6], label: &[u8]| [mac.as_slice(), label].concat(); + assert_eq!(Hello::decode(&[2, 0, 0, 0, 0]), None, "short of an address"); + assert_eq!(Hello::decode(&named([0x01, 0x00, 0x5e, 0, 0, 0xfb], b"i219")), None, "a group's address"); + assert_eq!(Hello::decode(&named([0xff; 6], b"i219")), None, "the broadcast address"); + assert_eq!(Hello::decode(&named([2; 6], &[b'a'; MAX_LABEL + 1])), None, "a name too long"); + assert_eq!(Hello::decode(&named([2; 6], b"")), None, "no name"); + for name in [b"i219\n".as_slice(), b"two words", b"\xff", b"e1000e."] { + assert_eq!(Hello::decode(&named([2; 6], name)), None, "the name {name:?}"); + } + } + + #[test] + fn every_refusal_reads_back_and_no_other_word_does() { + for why in [Refusal::OneLink, Refusal::Malformed, Refusal::Faulted] { + assert_eq!(Refusal::decode(&why.encode()), Some(why)); + } + assert_eq!(Refusal::decode(&0u32.to_le_bytes()), None); + assert_eq!(Refusal::decode(&[1, 0, 0]), None); + } +} diff --git a/lib/link/tests/ends.rs b/lib/link/tests/ends.rs new file mode 100644 index 00000000000..9d6040e6902 --- /dev/null +++ b/lib/link/tests/ends.rs @@ -0,0 +1,226 @@ +//! Both ends of a link over one header of words and an arena of bytes, as +//! the two processes share them, and every value a hostile peer can write. + +use core::sync::atomic::{AtomicU32, Ordering}; + +use toyos::transport::{Span, Violation}; +use toyos_link::ends::{Driver, Fault, Stack}; +use toyos_link::layout::{self, REGION_BYTES, RX_DEPTH, SLOT_BYTES, TX_DEPTH, WORDS}; +use toyos_link::state::{Counter, Counters, Group, Link}; + +struct Region { + words: [AtomicU32; WORDS], + bytes: Vec, +} + +impl Region { + fn new() -> Self { + Self { words: core::array::from_fn(|_| AtomicU32::new(0)), bytes: vec![0; REGION_BYTES] } + } + + fn write(&mut self, at: Span, frame: &[u8]) { + self.bytes[at.offset..at.offset + frame.len()].copy_from_slice(frame); + } + + fn read(&self, at: Span) -> Vec { + self.bytes[at.offset..at.offset + at.len].to_vec() + } + + fn poke(&self, word: usize, value: u32) { + self.words[word].store(value, Ordering::Release); + } +} + +fn frame(n: u32, len: usize) -> Vec { + (0..len).map(|i| (n as usize * 7 + i) as u8).collect() +} + +/// A link a driver made and the stack opened. +fn opened() -> (Region, Driver, Stack) { + let region = Region::new(); + let driver = Driver::new(®ion.words); + let stack = Stack::new(®ion.words); + (region, driver, stack) +} + +/// Frames received come out at the stack whole and in order, across many +/// wraps of the ring, and a slot is the driver's again only once the stack +/// released it. +#[test] +fn received_frames_reach_the_stack_whole_and_in_order() { + let (mut region, mut driver, mut stack) = opened(); + let (mut sent, mut taken) = (0u32, 0u32); + for round in 0..5 * RX_DEPTH { + for _ in 0..1 + round % 37 { + if driver.room(®ion.words).unwrap() == 0 { + assert_eq!(sent - taken, RX_DEPTH, "no room with less than a ring unreleased"); + break; + } + let len = 1 + (sent as usize * 31) % SLOT_BYTES; + let slot = driver.slot(); + region.write(slot, &frame(sent, len)); + driver.receive(®ion.words, len).unwrap(); + sent += 1; + } + driver.publish(®ion.words); + for _ in 0..1 + round % 23 { + let Some(at) = stack.received(®ion.words).unwrap() else { break }; + let len = 1 + (taken as usize * 31) % SLOT_BYTES; + assert_eq!(region.read(at), frame(taken, len), "frame {taken}"); + taken += 1; + } + stack.release(®ion.words); + } + assert!(sent > 2 * RX_DEPTH, "the ring wrapped"); +} + +/// The same the other way, frames the stack sends. +#[test] +fn frames_sent_reach_the_driver_whole_and_in_order() { + let (mut region, mut driver, mut stack) = opened(); + let (mut sent, mut taken) = (0u32, 0u32); + for round in 0..5 * TX_DEPTH { + for _ in 0..1 + round % 41 { + if stack.room(®ion.words).unwrap() == 0 { + break; + } + let len = 60 + sent as usize % 1455; + let slot = stack.slot(); + region.write(slot, &frame(sent, len)); + stack.send(®ion.words, len).unwrap(); + sent += 1; + } + stack.publish(®ion.words); + for _ in 0..1 + round % 19 { + let Some(at) = driver.to_send(®ion.words).unwrap() else { break }; + assert_eq!(region.read(at), frame(taken, 60 + taken as usize % 1455)); + taken += 1; + } + driver.release(®ion.words); + } + assert!(sent > 2 * TX_DEPTH); +} + +/// A full ring's producer that asks is rung once the consumer releases, and +/// a release nobody waited for rings nobody. +#[test] +fn a_full_ring_is_rung_when_its_room_returns() { + let (mut region, mut driver, mut stack) = opened(); + assert!(!driver.release(®ion.words), "nobody waits"); + while stack.room(®ion.words).unwrap() > 0 { + let slot = stack.slot(); + region.write(slot, &[1; 60]); + stack.send(®ion.words, 60).unwrap(); + } + assert!(stack.publish(®ion.words)); + assert_eq!(stack.want_room(®ion.words), Ok(0)); + assert!(driver.to_send(®ion.words).unwrap().is_some()); + assert!(driver.release(®ion.words), "the stack waits for this room"); + assert_eq!(stack.room(®ion.words), Ok(1)); + assert!(!driver.release(®ion.words), "the want was taken"); + + while driver.room(®ion.words).unwrap() > 0 { + driver.receive(®ion.words, 60).unwrap(); + } + driver.publish(®ion.words); + assert_eq!(driver.want_room(®ion.words), Ok(0)); + assert!(stack.received(®ion.words).unwrap().is_some()); + assert!(stack.release(®ion.words)); + assert!(!stack.release(®ion.words)); +} + +/// A frame the driver says is empty, or longer than its slot, ends the +/// session by name; so does a tail past the ring. +#[test] +fn a_frame_no_slot_holds_is_a_fault() { + for (len, fault) in [(0, Fault::Length(0)), (SLOT_BYTES as u32 + 1, Fault::Length(SLOT_BYTES as u32 + 1)), (u32::MAX, Fault::Length(u32::MAX))] { + let (region, _, mut stack) = opened(); + region.poke(layout::RX_ENTRIES, len); + region.poke(layout::RX_TAIL, 1); + assert_eq!(stack.received(®ion.words), Err(fault)); + } + let (region, mut driver, _) = opened(); + region.poke(layout::TX_ENTRIES, SLOT_BYTES as u32 + 1); + region.poke(layout::TX_TAIL, 1); + assert_eq!(driver.to_send(®ion.words), Err(Fault::Length(SLOT_BYTES as u32 + 1))); + + let (region, _, mut stack) = opened(); + region.poke(layout::RX_TAIL, RX_DEPTH + 1); + assert_eq!(stack.received(®ion.words), Err(Fault::Ring(Violation::TailPastDepth))); + let (region, _, mut stack) = opened(); + region.poke(layout::TX_HEAD, 1); + assert_eq!(stack.room(®ion.words), Err(Fault::Ring(Violation::HeadPastTail))); +} + +/// The link the driver says is the stack's; a word no driver writes is a +/// fault, not a guess. +#[test] +fn the_link_reads_as_the_driver_said_it() { + let (region, driver, stack) = opened(); + assert_eq!(stack.link(®ion.words), Ok(Link::Down), "a new link is down"); + let up = Link::Up { mbps: 1000, full_duplex: true }; + driver.set_link(®ion.words, up); + assert_eq!(stack.link(®ion.words), Ok(up)); + region.poke(layout::LINK, 0xdead_beef); + assert_eq!(stack.link(®ion.words), Err(Fault::Link(0xdead_beef))); +} + +#[test] +fn each_group_joined_reaches_the_driver_once() { + let (region, mut driver, mut stack) = opened(); + let groups: Vec = (0..layout::MAX_GROUPS as u8).map(|i| Group::new([0x01, 0x00, 0x5e, 0, 0, i]).unwrap()).collect(); + assert_eq!(driver.joined(®ion.words), Ok(None)); + for (i, group) in groups.iter().enumerate() { + assert!(stack.join(®ion.words, *group)); + if i % 2 == 1 { + assert_eq!(driver.joined(®ion.words), Ok(Some(groups[i - 1]))); + assert_eq!(driver.joined(®ion.words), Ok(Some(*group))); + assert_eq!(driver.joined(®ion.words), Ok(None)); + } + } + assert!(!stack.join(®ion.words, groups[0]), "the region holds no more groups"); +} + +/// A count past what the region holds, a count that went backwards, and a +/// group's words that are no group address are faults. +#[test] +fn groups_the_stack_never_joined_are_a_fault() { + let (region, mut driver, _) = opened(); + region.poke(layout::GROUPS_JOINED, layout::MAX_GROUPS as u32 + 1); + assert_eq!(driver.joined(®ion.words), Err(Fault::Groups(layout::MAX_GROUPS as u32 + 1))); + + let (region, mut driver, mut stack) = opened(); + assert!(stack.join(®ion.words, Group::new([0x33, 0x33, 0, 0, 0, 1]).unwrap())); + assert!(driver.joined(®ion.words).unwrap().is_some()); + region.poke(layout::GROUPS_JOINED, 0); + assert_eq!(driver.joined(®ion.words), Err(Fault::Groups(0))); + + let (region, mut driver, _) = opened(); + region.poke(layout::GROUPS, 0x0000_0052); + region.poke(layout::GROUPS_JOINED, 1); + assert_eq!(driver.joined(®ion.words), Err(Fault::Group), "an individual address"); +} + +/// The stack reads counters only once the driver answered its latest ask, +/// whatever the driver wrote in them. +#[test] +fn counters_are_read_once_answered() { + let (region, driver, mut stack) = opened(); + assert_eq!(driver.counters_asked(®ion.words), None, "nothing asked"); + stack.ask_counters(®ion.words); + assert_eq!(stack.counters(®ion.words), None, "not yet answered"); + let asked = driver.counters_asked(®ion.words).expect("an ask"); + let mut counters = Counters::default(); + counters.set(Counter::WireSent, u64::MAX); + counters.set(Counter::ErrorsCrc, 7); + driver.answer(®ion.words, asked, &counters); + assert_eq!(driver.counters_asked(®ion.words), None); + assert_eq!(stack.counters(®ion.words), Some(counters)); + + stack.ask_counters(®ion.words); + assert_eq!(stack.counters(®ion.words), None, "the last answer is the last ask's"); + region.poke(layout::COUNTERS_PRESENT, u32::MAX); + region.poke(layout::COUNTERS_ANSWERED, asked + 1); + let hostile = stack.counters(®ion.words).expect("answered"); + assert_eq!(hostile.iter().count(), Counter::ALL.len(), "bits past the last counter name nothing"); +} diff --git a/lib/link/tests/ends_loom.rs b/lib/link/tests/ends_loom.rs new file mode 100644 index 00000000000..6471a28feb7 --- /dev/null +++ b/lib/link/tests/ends_loom.rs @@ -0,0 +1,79 @@ +//! A full ring's want on two CPUs, under loom: the producer that finds no room +//! and the consumer that releases one never both miss each other, so the +//! producer is never left asleep on room it has. +//! +//! cargo test -p toyos-link --features mutate-want-relaxed --test ends_loom + +use core::sync::atomic::Ordering; +use std::rc::Rc; + +use loom::sync::Notify; +use loom::sync::atomic::AtomicU32; +use toyos::transport::Word; +use toyos_link::ends::{Driver, Stack}; +use toyos_link::layout::WORDS; + +/// A loom atomic as a region word: the trait is the SDK's and the type is +/// loom's, so the two meet through a wrapper. +struct Shared(AtomicU32); + +impl Word for Shared { + fn load(&self, order: Ordering) -> u32 { + self.0.load(order) + } + fn store(&self, value: u32, order: Ordering) { + self.0.store(value, order) + } + fn swap(&self, value: u32, order: Ordering) -> u32 { + self.0.swap(value, order) + } + fn compare_exchange(&self, current: u32, new: u32, success: Ordering, failure: Ordering) -> Result { + self.0.compare_exchange(current, new, success, failure) + } +} + +/// The region's page and the stack's doorbell. Held in an `Rc`, as +/// `toyos`'s `transport_loom` holds its streams: loom's `Arc` panics again in +/// its drop when a deadlock unwinds the model, which aborts the run before it +/// says which test failed +/// (`issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md`). The +/// page is built on the heap: its words on the model's 32 KiB coroutine stack +/// leave too little of it to unwind. +struct Link { + page: Box<[Shared; WORDS]>, + bell: Notify, +} + +fn link() -> Rc { + let words: Box<[Shared]> = (0..WORDS).map(|_| Shared(AtomicU32::new(0))).collect(); + let page = words.try_into().ok().expect("WORDS words"); + Rc::new(Link { page, bell: Notify::new() }) +} + +/// The stack fills the transmit ring and sleeps on its doorbell until it has +/// room; the driver takes one frame and rings only if the stack wanted it. +#[test] +fn a_full_ring_is_never_left_asleep_on_room_it_has() { + loom::model(|| { + let link = link(); + let mut driver = Driver::new(&link.page); + let mut stack = Stack::new(&link.page); + while stack.room(&link.page).unwrap() > 0 { + stack.send(&link.page, 60).unwrap(); + } + stack.publish(&link.page); + let consumer = { + let link = Rc::clone(&link); + loom::thread::spawn(move || { + assert!(driver.to_send(&link.page).unwrap().is_some(), "a frame published"); + if driver.release(&link.page) { + link.bell.notify(); + } + }) + }; + while stack.want_room(&link.page).unwrap() == 0 { + link.bell.wait(); + } + consumer.join().unwrap(); + }); +} diff --git a/system/i219/Cargo.toml b/system/i219/Cargo.toml new file mode 100644 index 00000000000..0d0005b4ef9 --- /dev/null +++ b/system/i219/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "i219" +description = "The Intel I219 and 82574 network driver: one process per such function the kernel does not drive, holding its claim and carrying its frames to netstack over a link." +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } +toyos = { path = "../../sdk/toyos" } +toyos-device-memory = { path = "../../lib/device-memory" } +toyos-i219 = { path = "driver" } +toyos-link = { path = "../../lib/link" } +toyos-manifest = { path = "../../lib/manifest" } +toyos-pci-claim = { path = "../../lib/pci-claim" } + +[package.metadata.toyos.host] +exempt.owns = "one Intel I219 or 82574 function, claimed from the kernel, and its link to netstack" diff --git a/system/netstack/i219/Cargo.toml b/system/i219/driver/Cargo.toml similarity index 97% rename from system/netstack/i219/Cargo.toml rename to system/i219/driver/Cargo.toml index 64959d7e21b..dbe66458e1d 100644 --- a/system/netstack/i219/Cargo.toml +++ b/system/i219/driver/Cargo.toml @@ -3,7 +3,7 @@ # QEMU. # # What lives here is every decision the I219 driver makes — the descriptor -# rings, the interrupt causes, the link — against four traits netstack implements +# rings, the interrupt causes, the link — against four traits the `i219` program implements # one instruction deep, two of them the boundary every userland driver shares. # The device is on the far side of a trust boundary, so the numbers it writes # into a descriptor have to be driven with values no working card would send diff --git a/system/netstack/i219/src/delivery.rs b/system/i219/driver/src/delivery.rs similarity index 100% rename from system/netstack/i219/src/delivery.rs rename to system/i219/driver/src/delivery.rs diff --git a/system/netstack/i219/src/lib.rs b/system/i219/driver/src/lib.rs similarity index 99% rename from system/netstack/i219/src/lib.rs rename to system/i219/driver/src/lib.rs index 90997f28163..b3144136a23 100644 --- a/system/netstack/i219/src/lib.rs +++ b/system/i219/driver/src/lib.rs @@ -40,7 +40,7 @@ //! taken after the load that found `DD` set, so a written-back length cannot //! be read from before the write-back. //! -//! In netstack those four are the substrate's own: the mapped BAR, the monotonic +//! In the `i219` program those four are the substrate's own: the mapped BAR, the monotonic //! clock, a `DmaRegion` in this function's IOMMU domain, and the claim's //! interrupt record. On the host they are `stub.rs`, which is the datasheet //! written down. @@ -72,7 +72,7 @@ //! Every number in a written-back descriptor is the device's, and this driver //! is on the far side of an IOMMU domain from the rest of the machine but on //! the *same* side as its own memory. A length longer than the buffer it was -//! given becomes the length of a slice netstack hands to its stack, so `parse_rx` +//! given becomes the length of a slice the program copies to the link, so `parse_rx` //! bounds it, and `RxRefusal` is every way a written-back descriptor is refused //! rather than believed. //! diff --git a/system/netstack/i219/src/pch.rs b/system/i219/driver/src/pch.rs similarity index 100% rename from system/netstack/i219/src/pch.rs rename to system/i219/driver/src/pch.rs diff --git a/system/netstack/i219/src/phy.rs b/system/i219/driver/src/phy.rs similarity index 100% rename from system/netstack/i219/src/phy.rs rename to system/i219/driver/src/phy.rs diff --git a/system/netstack/i219/src/power.rs b/system/i219/driver/src/power.rs similarity index 100% rename from system/netstack/i219/src/power.rs rename to system/i219/driver/src/power.rs diff --git a/system/netstack/i219/src/regs.rs b/system/i219/driver/src/regs.rs similarity index 100% rename from system/netstack/i219/src/regs.rs rename to system/i219/driver/src/regs.rs diff --git a/system/netstack/i219/src/stub.rs b/system/i219/driver/src/stub.rs similarity index 100% rename from system/netstack/i219/src/stub.rs rename to system/i219/driver/src/stub.rs diff --git a/system/netstack/i219/src/tests.rs b/system/i219/driver/src/tests.rs similarity index 99% rename from system/netstack/i219/src/tests.rs rename to system/i219/driver/src/tests.rs index 31e5259d59b..ad51ae377bf 100644 --- a/system/netstack/i219/src/tests.rs +++ b/system/i219/driver/src/tests.rs @@ -18,7 +18,7 @@ use crate::*; type Driver = I219; /// The mode the claim was armed in, read off the modelled configuration space -/// the way netstack reads the claim's. +/// the way the `i219` program reads the claim's. fn delivery(nic: &Nic) -> Delivery { Delivery::of(|at| Ok::<_, ()>(nic.config(at))) .unwrap_or_else(|why| panic!("{}", nic.because(&format!("no delivery: {why}")))) @@ -3471,7 +3471,7 @@ fn a_full_ring_counts_how_long_it_waited_for_room() { /// The bring-up leaves one `TCTL` whatever the last holder wrote: a reset that /// leaves the register as it was, as the T14's does, and one that resets it to /// §10.2.6.1's initial values, under a holder that left `MULR` set, one that -/// left it clear, and none. Each left its transmitter stopped, as netstack's +/// left it clear, and none. Each left its transmitter stopped, as the program's /// quiesce does before the bring-up. #[test] fn bring_up_sets_tctl_whatever_the_last_holder_left() { diff --git a/system/netstack/i219/src/ulp.rs b/system/i219/driver/src/ulp.rs similarity index 100% rename from system/netstack/i219/src/ulp.rs rename to system/i219/driver/src/ulp.rs diff --git a/system/netstack/i219/src/wake.rs b/system/i219/driver/src/wake.rs similarity index 100% rename from system/netstack/i219/src/wake.rs rename to system/i219/driver/src/wake.rs diff --git a/system/i219/src/main.rs b/system/i219/src/main.rs new file mode 100644 index 00000000000..43ba2b5076b --- /dev/null +++ b/system/i219/src/main.rs @@ -0,0 +1,43 @@ +//! The Intel I219 and 82574 driver: one process per such function the kernel +//! does not drive, holding its claim (a `drives` row) and carrying its frames +//! to the network stack over a link (`toyos-link`). +//! +//! **Every decision about the part is `toyos-i219`'s**; [`nic`] is the claim's +//! two mappings and the copies between the part's buffers and the link. + +use toyos::endow; +use toyos::{say, PciDev}; +use toyos_abi::syscall::PciId; +use toyos_i219::Part; +use toyos_link::driver::Nic as _; +use toyos_manifest::device::PciMatch; + +mod nic; + +/// The parts this driver brings up, matched by their class and identity +/// rather than by the slot firmware put them in, as the row spells them. +/// `8086:15fc` is the ThinkPad T14's onboard I219 at `00:1f.6`; `8086:10d3` +/// is the 82574L, which QEMU's `e1000e` models. Each names which part it is +/// because below the register file they are not one. +const PARTS: [(PciMatch, Part); 2] = [(ethernet(0x8086, 0x15fc), Part::I219), (ethernet(0x8086, 0x10d3), Part::E82574)]; + +/// An Ethernet controller, class `02:00:00`. +const fn ethernet(vendor: u16, device: u16) -> PciMatch { + PciMatch::new([0x02, 0x00, 0x00], Some(PciId { vendor, device })) +} + +fn main() { + let (claim, part) = PARTS + .iter() + .find_map(|(matcher, part)| endow::claim::(&matcher.to_string()).map(|claim| (claim, *part))) + .expect("i219: the drives row endows this process one of its parts' claims"); + // A part the claim was minted on that this driver cannot bring up is + // something the device said that this driver does not understand: loud, + // and the supervisor's to start again or give up on. + let mut nic = nic::Nic::open(claim, part) + .unwrap_or_else(|why| panic!("i219: the NIC this program was given is not one it can drive — {why}")); + let m = nic.mac(); + let label = nic.label(); + say!("{label}: MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5]); + toyos_link::driver::drive(label, &mut nic); +} diff --git a/system/netstack/src/i219.rs b/system/i219/src/nic.rs similarity index 62% rename from system/netstack/src/i219.rs rename to system/i219/src/nic.rs index 9c68dad47cd..1f724d64346 100644 --- a/system/netstack/src/i219.rs +++ b/system/i219/src/nic.rs @@ -1,6 +1,6 @@ -//! The I219, as netstack drives it: the clock and the claim the driver's logic -//! is written against beside `toyos-pci-claim`'s register window and grant, -//! and nothing else. +//! The I219, as this program drives it: the clock and the claim the driver's +//! logic is written against beside `toyos-pci-claim`'s register window and +//! grant, and nothing else. //! //! What the kernel keeps is the *claim*: config space, the interrupt vector it //! programmed into this function, and the address space the function @@ -9,22 +9,24 @@ //! at the unit and recorded against this process. //! //! **Two views of one grant, and the split is the boundary itself.** `Grant` -//! is the driver's, and it reaches descriptors — sixteen bytes each, read and -//! written a word at a time. [`Nic::frames`] is netstack's, and it reaches -//! payloads. The driver never touches a payload and netstack never touches a -//! descriptor, which is what lets the driver's logic live in a crate that -//! forbids `unsafe` at all. +//! is the driver's logic's, and it reaches descriptors — sixteen bytes each, +//! read and written a word at a time. [`Nic::frames`] is this file's, and it +//! reaches payloads, which it copies to and from the link. The logic never +//! touches a payload and this file never touches a descriptor, which is what +//! lets the logic live in a crate that forbids `unsafe` at all. -use std::cell::{Cell, RefCell}; +use std::cell::Cell; use std::rc::Rc; use toyos::poller::{Poller, READABLE}; use toyos::shm::SharedMemory; use toyos::volatile::Window; -use toyos::{DmaRegion, PciDev}; +use toyos::shm::ReadBytes; +use toyos::{say, DmaRegion, PciDev}; use toyos_abi::syscall::{RegWidth, SyscallError}; use toyos_device_memory::Registers; -use toyos_i219::{Clock, Delivery, Interrupts, NoDelivery}; +use toyos_i219::{Clock, Delivery, Interrupts, NoDelivery, Part}; +use toyos_link::state::{Counter, Counters, Group, Link}; use toyos_pci_claim::{Bar, Grant, KernelRefused}; /// The machine's monotonic clock: one syscall, which the kernel serves from an @@ -94,21 +96,21 @@ impl std::fmt::Display for Opening { pub struct Nic { /// Above the mappings it reaches: dropping the driver writes the part's /// registers, and fields drop in the order they are declared. - driver: RefCell, + driver: Driver, /// Held for their mappings' lives: the register window points into the /// first and the grant's into the second. _mapped: SharedMemory, region: DmaRegion, claim: Rc, - /// netstack's view of the same grant: the frame payloads, and nothing else. + /// This file's view of the same grant: the frame payloads, and nothing else. frames: Window, mac: [u8; 6], - part: toyos_i219::Part, + label: &'static str, reported: Latch<(toyos_i219::Counters, toyos_i219::Link)>, } /// The claim's register window, and the mapping it points into. -fn registers(dev: &PciDev) -> Result<(Bar, SharedMemory), Opening> { +fn registers(dev: &PciDev, label: &str) -> Result<(Bar, SharedMemory), Opening> { let info = toyos_pci_claim::describe(dev).map_err(Opening::Kernel)?; // The lowest BAR wide enough, rather than BAR 0 by name: the kernel // reports 0 bytes for a BAR it keeps back, and the MSI-X table's is one @@ -121,8 +123,9 @@ fn registers(dev: &PciDev) -> Result<(Bar, SharedMemory), Opening> { .map(|(index, bytes)| (index as u32, *bytes)) .ok_or(Opening::NoWindow)?; let (window, mapped) = Bar::map(dev, bar, bytes).map_err(Opening::Kernel)?; - crate::say!( - "netstack: I219: PCI {:02x}:{:02x}.{} registers in BAR {bar} ({bytes:#x} bytes)", + say!( + "{}: PCI {:02x}:{:02x}.{} registers in BAR {bar} ({bytes:#x} bytes)", + label, info.bus, info.dev, info.func, @@ -134,14 +137,14 @@ fn registers(dev: &PciDev) -> Result<(Bar, SharedMemory), Opening> { /// it is asked: the register window, the part stopped, then one grant — as the /// driver reaches its descriptors — with the two mappings they point into, and /// the function started. -fn granted(dev: &PciDev) -> Result<(Bar, Grant, SharedMemory, DmaRegion), Opening> { - let (bar, mapped) = registers(dev)?; +fn granted(dev: &PciDev, label: &str) -> Result<(Bar, Grant, SharedMemory, DmaRegion), Opening> { + let (bar, mapped) = registers(dev, label)?; // Before the start: a part a previous holder left receiving would write // into its descriptors. // Said first: whether the kernel's release reset the part is read here, in // the receive and transmit enables it handed over. - crate::say!( - "netstack: I219: inherited RCTL {:#010x} TCTL {:#010x}", + say!( + "{label}: inherited RCTL {:#010x} TCTL {:#010x}", bar.read32(toyos_i219::regs::RCTL), bar.read32(toyos_i219::regs::TCTL) ); @@ -195,19 +198,23 @@ fn brought_up_words(brought_up: toyos_i219::BringUp) -> Vec { words } -fn say_brought_up(brought_up: toyos_i219::BringUp) { +fn say_brought_up(label: &str, brought_up: toyos_i219::BringUp) { for words in brought_up_words(brought_up) { - crate::say!("netstack: I219: {words}"); + say!("{label}: {words}"); } } impl Nic { /// Take the claim's register window and one grant, and bring the part up. - pub fn open(dev: PciDev, part: toyos_i219::Part) -> Result { + pub fn open(dev: PciDev, part: Part) -> Result { + let label = match part { + Part::I219 => "i219", + Part::E82574 => "82574", + }; let dev = Rc::new(dev); // Bound before the driver, so one that is refused lets its part go // while both are still mapped. - let (bar, grant, mapped, region) = granted(&dev)?; + let (bar, grant, mapped, region) = granted(&dev, label)?; let delivery = Delivery::of(|at| dev.config_read(at.into(), RegWidth::U32)) .map_err(Opening::Delivery)?; let frames = grant.window(); @@ -215,114 +222,121 @@ impl Nic { toyos_i219::I219::open(part, delivery, bar, Monotonic, grant, Claim(Rc::clone(&dev))) .map_err(Opening::Driver)?; let mac = driver.mac(); - say_brought_up(driver.brought_up()); - Ok(Self { - driver: RefCell::new(driver), - _mapped: mapped, - region, - claim: dev, - frames, - mac, - part, - reported: Latch::default(), - }) - } - - pub fn mac(&self) -> [u8; 6] { - self.mac + say_brought_up(label, driver.brought_up()); + Ok(Self { driver, _mapped: mapped, region, claim: dev, frames, mac, label, reported: Latch::default() }) } - pub fn part(&self) -> toyos_i219::Part { - self.part + /// The driver's name for its part: its lines' and the link's. + pub fn label(&self) -> &'static str { + self.label } - /// The claim, for the poller: readable means an interrupt has landed. - pub fn claim(&self) -> &PciDev { - &self.claim - } - - /// Pass frames sent to the multicast address `group`. - pub fn accept_multicast(&self, group: [u8; 6]) { - self.driver.borrow().accept_multicast(group); - } - - pub fn link(&self) -> toyos_i219::Link { - self.driver.borrow().link() - } - - /// What the driver and the MAC have counted so far, the transmit ring - /// reclaimed first so a frame that has left is counted as sent. - pub fn counts(&self) -> (toyos_i219::Counters, toyos_i219::Wire) { - let mut driver = self.driver.borrow_mut(); - driver.reclaim(); - let wire = driver.wire(); - (driver.counters(), wire) + fn link(&self) -> Link { + match self.driver.link() { + toyos_i219::Link::Down => Link::Down, + toyos_i219::Link::Up { speed, full_duplex } => Link::Up { mbps: speed.mbps().into(), full_duplex }, + } } +} - /// Take the interrupt, acknowledge its causes and refresh the link — and - /// answer the link where this pass found it changed. - pub fn begin_pass(&self) -> Result, toyos_i219::PassRefused> { - let pass = self.driver.borrow_mut().begin_pass()?; - Ok(pass.link_changed.then(|| self.link())) +impl toyos_link::driver::Nic for Nic { + fn mac(&self) -> [u8; 6] { + self.mac } - /// How long until a pass has to begin with nothing arrived: the driver's - /// deadline for descriptors a link change left in the ring. - pub fn pass_due_in(&self) -> Option { - self.driver.borrow().pass_due_in() + fn claim(&self) -> &PciDev { + &self.claim } - pub fn poll_rx(&self) -> Option { - self.driver.borrow_mut().poll_rx() + /// **A claim that refuses the read for anything but `WouldBlock` is the + /// kernel saying this function is no longer this process's**: a fault at + /// the unit is the one that happens, and by the time it is answered the + /// function's bus mastering is gone. So is a part that kept the frames a + /// link change left in its transmit ring past the driver's deadline. + /// Nothing drives either from here, and this dies where it can be read. + fn begin_pass(&mut self) -> Link { + if let Err(why) = self.driver.begin_pass() { + panic!("{}: this NIC cannot be driven on — {why}", self.label); + } + self.link() } - /// The bytes of a received frame. - pub fn rx_frame(&self, frame: &toyos_i219::Frame) -> &[u8] { + fn receive(&mut self, take: &mut dyn FnMut(&[u8])) -> bool { + let Some(frame) = self.driver.poll_rx() else { return false }; let window = self.frames.sub(frame.at, frame.len); // SAFETY: the window is inside the grant, which lives as long as // `self`; the device has finished with this buffer — the `DD` its // descriptor carries is what said so — and it is not published again - // until `rx_done`, which the caller makes after this borrow ends. The - // I219 is the chipset vendor's integrated MAC, inside the trusted base, - // so its descriptor protocol is believed; an external or hot-plug - // device's driver must copy instead. - unsafe { std::slice::from_raw_parts(window.as_ptr() as *const u8, frame.len) } - } - - /// Give the buffer back once its frame has been read. - pub fn rx_done(&self, frame: toyos_i219::Frame) { - self.driver.borrow_mut().rx_done(frame); + // until `rx_done`, after this borrow ends. The I219 is the chipset + // vendor's integrated MAC, inside the trusted base, so its descriptor + // protocol is believed; an external or hot-plug device's driver must + // copy instead. + take(unsafe { std::slice::from_raw_parts(window.as_ptr() as *const u8, frame.len) }); + self.driver.rx_done(frame); + true } - /// Hand the part every buffer the batch gave back. - pub fn rx_flush(&self) { - self.driver.borrow_mut().rx_flush(); + fn received(&mut self) { + self.driver.rx_flush(); } - /// How many frames the transmit ring takes now. - pub fn tx_room(&self) -> usize { - self.driver.borrow_mut().tx_room() + fn tx_room(&mut self) -> usize { + self.driver.tx_room() } - /// Have the part interrupt when a transmit descriptor comes back, and - /// answer the room there is once it has been told to. - pub fn wake_on_room(&self) -> usize { - self.driver.borrow_mut().wake_on_room() + fn wake_on_room(&mut self) -> usize { + self.driver.wake_on_room() } - /// Copy `frame` into a transmit buffer and hand it to the device. - /// - /// Non-blocking, and for a caller [`Self::tx_room`] answered in this - /// pass: a frame the driver has no slot for is a caller that did not ask, - /// or one that offered more than a buffer holds. - pub fn tx(&self, frame: &[u8]) { + fn send(&mut self, frame: ReadBytes<'_>) { let len = frame.len(); - let slot = self.driver.borrow_mut().tx_reserve(len).unwrap_or_else(|| { - panic!("netstack: a {len}-byte frame was offered to a transmit ring that refuses it") + let slot = self.driver.tx_reserve(len).unwrap_or_else(|| { + panic!("{}: a {len}-byte frame was offered to a transmit ring that refuses it", self.label) }); - // `frames` is the grant's window, which starts at the region's first byte. - self.region.memory.bytes().write(slot.at, frame); - self.driver.borrow_mut().tx_commit(slot); + let memory = &self.region.memory; + assert!(slot.at.checked_add(len).is_some_and(|end| end <= memory.len()), "{}: a transmit slot past the grant", self.label); + // SAFETY: the assertion keeps `len` bytes from `slot.at` inside the + // grant's mapping, which lives as long as `self`; the part reads none + // of them until `tx_commit` publishes the slot; and the link's region + // the frame is copied from is another mapping. + unsafe { frame.read_to(memory.as_ptr().add(slot.at)) }; + self.driver.tx_commit(slot); + } + + fn pass_due_in(&self) -> Option { + self.driver.pass_due_in() + } + + fn join(&mut self, group: Group) { + self.driver.accept_multicast(group.mac()); + } + + /// What the driver and the MAC have counted so far, the transmit ring + /// reclaimed first so a frame that has left is counted as sent. + fn counters(&mut self) -> Counters { + self.driver.reclaim(); + let wire = self.driver.wire(); + let ours = self.driver.counters(); + let mut counters = Counters::default(); + for (counter, value) in [ + (Counter::DescriptorsSent, ours.sent.into()), + (Counter::DescriptorsReceived, ours.received.into()), + (Counter::DescriptorsStranded, ours.stranded.into()), + (Counter::TransmitFull, ours.tx_full.into()), + (Counter::TransmitWakeArmed, ours.tx_wake_armed.into()), + (Counter::TransmitWakeTaken, ours.tx_wake_taken.into()), + (Counter::TransmitFullNanos, ours.tx_full_nanos), + (Counter::TransmitUnreported, ours.tx_unreported), + (Counter::TransmitUnreportedMax, ours.tx_unreported_max.into()), + (Counter::WireSent, wire.sent), + (Counter::WireReceived, wire.received), + (Counter::WireSeen, wire.seen), + (Counter::ErrorsMissed, wire.missed), + (Counter::ErrorsCrc, wire.crc_errors), + ] { + counters.set(counter, value); + } + counters } /// Say what this driver has refused or been told about, and what the link @@ -332,18 +346,19 @@ impl Nic { /// /// Keyed on the anomalies and never on the spurious count: that one moves /// on its own. - pub fn report(&self) { - let driver = self.driver.borrow(); + fn report(&mut self) { + let driver = &self.driver; let counters = driver.counters(); let link = driver.link(); let Some((was_counters, was_link)) = self.reported.moved((counters.anomalies(), link)) else { return; }; + let label = self.label; if link != was_link { match link { - toyos_i219::Link::Up { speed, full_duplex } => crate::say!( - "netstack: I219: link up at {} Mb/s {}{}", + toyos_i219::Link::Up { speed, full_duplex } => say!( + "{label}: link up at {} Mb/s {}{}", speed.mbps(), if full_duplex { "full duplex" } else { "half duplex" }, match driver.link_up_after_nanos() { @@ -351,12 +366,12 @@ impl Nic { None => String::new(), }, ), - toyos_i219::Link::Down => crate::say!("netstack: I219: link down"), + toyos_i219::Link::Down => say!("{label}: link down"), } } if counters.anomalies() != was_counters { - crate::say!( - "netstack: I219: refused {} over-length, {} errored, {} split and {} empty \ + say!( + "{label}: refused {} over-length, {} errored, {} split and {} empty \ descriptor(s); {} overrun(s), {} descriptor-starvation report(s), \ {} frame(s) left in the ring at a link change, {} spurious interrupt(s)", counters.over_length, diff --git a/system/netstack/Cargo.toml b/system/netstack/Cargo.toml index 6fb3a3a3b0a..f514aee0a26 100644 --- a/system/netstack/Cargo.toml +++ b/system/netstack/Cargo.toml @@ -1,18 +1,13 @@ [package] name = "netstack" -description = "The network server: one Ethernet device claimed from the kernel, ToyOS's own stack over it (toyos-net-node), its DHCP lease, resolver and mDNS name, served to programs over IPC." +description = "The network server: ToyOS's own stack (toyos-net-node) over the link a NIC driver brings, its DHCP lease, resolver and mDNS name, served to programs over IPC." edition = "2024" license = "MIT OR Apache-2.0" [dependencies] toyos-abi = { path = "../../sdk/abi" } -# The matcher its claim is endowed under, spelled as the manifest spells it. -toyos-manifest = { path = "../../lib/manifest" } toyos = { path = "../../sdk/toyos" } -toyos-device-memory = { path = "../../lib/device-memory" } -toyos-i219 = { path = "i219" } -toyos-virtio = { path = "../../lib/virtio" } -toyos-pci-claim = { path = "../../lib/pci-claim" } +toyos-link = { path = "../../lib/link" } toyos-tco = { path = "../../lib/tco" } toyos-mdns = { path = "mdns" } toyos-dns = { path = "dns" } @@ -25,8 +20,5 @@ toyos-net-tcp = { path = "shard/tcp" } toyos-net-udp = { path = "udp" } toyos-net-wire = { path = "wire" } -[dev-dependencies] -toyos-pci-claim = { path = "../../lib/pci-claim", features = ["host-grant"] } - [package.metadata.toyos.host] -exempt.owns = "the NIC ToyOS claims for it" +exempt.manages = "the machine's network stack, over the links ToyOS's NIC drivers bring" diff --git a/system/netstack/src/card.rs b/system/netstack/src/card.rs deleted file mode 100644 index 915d290b96d..00000000000 --- a/system/netstack/src/card.rs +++ /dev/null @@ -1,217 +0,0 @@ -use toyos_i219::Part; -use toyos_inspect::Snapshot; - -use crate::i219; -use crate::virtio_net::VirtioNet; - -/// The NIC this program drives, whichever one the manifest gave it. -/// -/// **One enum and not a trait object**: there are two of them, both known at -/// build time, and what a `dyn` would buy is a vtable on the frame path. -pub enum Card { - Virtio(VirtioNet), - Intel(i219::Nic), -} - -impl Card { - /// A device this driver cannot bring up is not a machine without a NIC: the - /// claim was minted, so something the device said is not what this driver - /// understands, and that is loud. - fn undrivable(why: impl std::fmt::Display) -> ! { - panic!("netstack: the NIC this program was given is not one it can drive — {why}") - } - - pub fn intel(claim: toyos::PciDev, part: Part) -> Self { - match i219::Nic::open(claim, part) { - Ok(nic) => Self::Intel(nic), - Err(why) => Self::undrivable(why), - } - } - - pub fn virtio(claim: toyos::PciDev) -> Self { - match VirtioNet::open(claim) { - Ok(nic) => Self::Virtio(nic), - Err(why) => Self::undrivable(why), - } - } - - pub fn mac(&self) -> [u8; 6] { - match self { - Self::Virtio(nic) => nic.mac(), - Self::Intel(nic) => nic.mac(), - } - } - - /// The claim, for the poller: readable means an interrupt has landed. - pub fn claim(&self) -> &toyos::PciDev { - match self { - Self::Virtio(nic) => nic.claim(), - Self::Intel(nic) => nic.claim(), - } - } - - /// **The record has to be taken, not merely noticed.** A claim reads ready - /// while it holds an undrained interrupt, so a pass that saw the token and - /// left it would find the same one on the next `wait` and every one after - /// it. - /// - /// A claim that refuses the read for anything but `WouldBlock` is the - /// kernel saying this function is no longer this process's: a fault at the - /// unit is the one that happens, and by the time it is answered the - /// function's bus mastering is gone. Every frame from here on is one that - /// silently never arrives, so this dies where it can be read — once, for - /// whichever driver is running. - /// - /// So does an Intel part that kept the frames a link change left in its - /// transmit ring past the driver's deadline: nothing drives it from there. - /// - /// Answers the link where the pass found it changed; virtio reports none. - pub fn begin_pass(&self) -> Option { - let answered = match self { - Self::Virtio(nic) => nic.take_interrupt().map(|_| None).map_err(toyos_i219::PassRefused::Claim), - Self::Intel(nic) => nic.begin_pass(), - }; - answered.unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}")) - } - - /// What `inspect` reads about the card: which driver, its address, its - /// link, and on the Intel parts what the driver and the MAC counted. - /// - /// **virtio's link is `unreported`, not `up`**: the device tells netstack - /// nothing about one, and netstack serving as though it were up is netstack's - /// assumption rather than something it measured. - pub fn inspect(&self, snap: &mut Snapshot) { - let m = self.mac(); - snap.put( - "mac", - format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5]), - ); - let nic = match self { - Self::Virtio(_) => { - snap.put("driver", "virtio-net"); - snap.put("link.state", "unreported"); - return; - } - Self::Intel(nic) => nic, - }; - snap.put( - "driver", - match nic.part() { - Part::I219 => "i219", - Part::E82574 => "82574", - }, - ); - match nic.link() { - toyos_i219::Link::Down => snap.put("link.state", "down"), - toyos_i219::Link::Up { speed, full_duplex } => { - snap.put("link.state", "up"); - snap.put( - "link.speed_mbps", - match speed { - toyos_i219::Speed::Mbps10 => 10u32, - toyos_i219::Speed::Mbps100 => 100, - toyos_i219::Speed::Mbps1000 => 1000, - }, - ); - snap.put("link.duplex", if full_duplex { "full" } else { "half" }); - } - } - let (counters, wire) = nic.counts(); - snap.put("descriptors.sent", counters.sent); - snap.put("descriptors.received", counters.received); - snap.put("descriptors.stranded", counters.stranded); - snap.put("transmit.full", counters.tx_full); - snap.put("transmit.wake_armed", counters.tx_wake_armed); - snap.put("transmit.wake_taken", counters.tx_wake_taken); - snap.put("transmit.full.ns", counters.tx_full_nanos); - snap.put("transmit.unreported", counters.tx_unreported); - snap.put("transmit.unreported.max", counters.tx_unreported_max); - snap.put("wire.sent", wire.sent); - snap.put("wire.received", wire.received); - snap.put("wire.seen", wire.seen); - snap.put("errors.missed", wire.missed); - snap.put("errors.crc", wire.crc_errors); - } - - /// One batch of received frames: `batch` is given the most it may take — the card's ring, - /// all of what one wait can bring — and the card's next frame on each call, and every buffer - /// it gave back reaches the card once it returns. - pub fn receive(&self, batch: impl FnOnce(usize, &mut dyn FnMut(&mut dyn FnMut(&[u8])) -> bool) -> R) -> R { - let budget = match self { - Self::Virtio(_) => crate::virtio_net::RX_BUF_COUNT, - Self::Intel(_) => toyos_i219::RX_RING - 1, - }; - let took = batch(budget, &mut |sink| self.rx(sink)); - if let Self::Intel(nic) = self { - nic.rx_flush(); - } - took - } - - /// Hands the next received frame to `take` and gives its buffer back to - /// the card once `take` returns; `false` when none waits. - fn rx(&self, take: &mut dyn FnMut(&[u8])) -> bool { - match self { - Self::Virtio(nic) => { - let Some((index, len)) = nic.poll_rx() else { return false }; - take(nic.rx_frame(index, len)); - nic.rx_done(index); - } - Self::Intel(nic) => { - let Some(frame) = nic.poll_rx() else { return false }; - take(nic.rx_frame(&frame)); - nic.rx_done(frame); - } - } - true - } - - /// How many frames the card takes now. [`Self::tx`] is for a caller this - /// answered: a card with no room is not offered a frame. - pub fn tx_room(&self) -> usize { - match self { - Self::Virtio(nic) => nic.tx_room(), - Self::Intel(nic) => nic.tx_room(), - } - } - - /// Have the claim read ready when room returns, and answer the room there - /// is once that is so: a caller answered 0 has nothing to do before the - /// claim wakes it. - /// - /// virtio is asked nothing: its every finished transmit interrupts - /// (`VirtioNet::tx_room`). - pub fn wake_on_room(&self) -> usize { - match self { - Self::Virtio(nic) => nic.tx_room(), - Self::Intel(nic) => nic.wake_on_room(), - } - } - - pub fn tx(&self, frame: &[u8]) { - match self { - Self::Virtio(nic) => nic.tx(frame), - Self::Intel(nic) => nic.tx(frame), - } - } - - /// How long until a pass has to begin whether or not the claim reads - /// ready, in nanoseconds: [`Self::begin_pass`] is where a card that owes - /// something by then is refused. virtio owes nothing. - pub fn pass_due_in(&self) -> Option { - match self { - Self::Virtio(_) => None, - Self::Intel(nic) => nic.pass_due_in(), - } - } - - /// Say what the driver counted, once a pass and after every frame the pass - /// sent: a line per refused descriptor is itself more frames to send. - /// virtio counts nothing: what its device is not believed on ends it. - pub fn report(&self) { - match self { - Self::Virtio(_) => {} - Self::Intel(nic) => nic.report(), - } - } -} diff --git a/system/netstack/src/link.rs b/system/netstack/src/link.rs new file mode 100644 index 00000000000..0ab6bd16d40 --- /dev/null +++ b/system/netstack/src/link.rs @@ -0,0 +1,281 @@ +//! The stack's side of a link: the driver's connection, the region it sent, +//! and the stack's end of the region's rings (`toyos-link`). +//! +//! **What the driver writes is believed only as `toyos-link`'s ends bound +//! it**: a frame's length by its slot, the link by its word, and the counters +//! not at all, displayed as the driver wrote them. A [`Fault`] ends the link, +//! as a hang-up does, and the stack keeps every connection it carries. + +use toyos::ipc::{Connection, FrameRx, RxStep}; +use toyos::poller::READABLE; +use toyos::say; +use toyos_abi::syscall::SyscallError; +use toyos_inspect::Snapshot; +use toyos_link::ends::{Fault, Stack}; +use toyos_link::layout::{RX_DEPTH, SLOT_BYTES}; +use toyos_link::region::Region; +use toyos_link::state::{Counters, Group, Link}; +use toyos_link::wire::{self, Hello, Refusal}; +use toyos_net_node::Node; +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +use toyos_net_wire::Instant; + +use crate::watches::Watches; + +const _: () = assert!(toyos_net_ip::FRAME <= SLOT_BYTES, "every frame the stack sends fits a link's slot"); + +/// A driver's connection that has not said hello yet. +pub struct Pending { + conn: Connection, + rx: FrameRx<{ Hello::MAX_BYTES }>, + pub since: std::time::Instant, +} + +/// What a pending connection's hello came to. +pub enum Heard { + Nothing, + /// The driver went, or said what no driver says: it was refused, or let go. + Gone, + /// The driver said who it is, its address as the node takes one: its + /// connection is [`Pending::into_conn`]. + Hello(Hello, IndividualMac, Region), +} + +impl Pending { + pub fn new(conn: Connection) -> Self { + Self { conn, rx: FrameRx::new(), since: std::time::Instant::now() } + } + + pub fn watch(&self, poller: &mut Watches, token: u64) { + poller.watch(&self.conn, READABLE, token); + } + + pub fn into_conn(self) -> Connection { + self.conn + } + + pub fn hear(&mut self) -> Heard { + match self.rx.pump(&self.conn) { + RxStep::Idle => Heard::Nothing, + RxStep::Eof => Heard::Gone, + RxStep::Frame { msg_type: wire::MSG_HELLO, payload_len, handles: 1 } => { + let hello = Hello::decode(self.rx.payload(payload_len)); + let mac = hello.as_ref().and_then(|hello| IndividualMac::new(MacAddr(hello.mac.bytes()))); + let region = self.rx.take_handles::<1>().and_then(|[region]| Region::lent(region).ok()); + match (hello, mac, region) { + (Some(hello), Some(mac), Some(region)) => Heard::Hello(hello, mac, region), + _ => { + say!("netstack: a NIC driver's hello is not one this protocol makes, or its region is none"); + refuse(&self.conn, Refusal::Malformed); + Heard::Gone + } + } + } + RxStep::Frame { .. } | RxStep::Malformed => { + say!("netstack: a NIC driver's connection sent a frame no driver sends first"); + Heard::Gone + } + } + } +} + +/// Answer a driver's hello with `why`; one `try_send`, as every answer. +pub fn refuse(conn: &Connection, why: Refusal) { + let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); +} + +/// Why a link ended. +pub enum Ended { + HungUp, + Fault(Fault), +} + +/// A link the stack opened. +pub struct Session { + conn: Connection, + region: Region, + end: Stack, + hello: Hello, + mac: IndividualMac, + /// The link as the node was last told it. + said: Link, + frame: Box<[u8; SLOT_BYTES]>, + /// The driver owes an answer to the counters asked. + asked: bool, + ring: bool, +} + +impl Session { + /// Open the link `hello` asked for over `region`, and say so to the + /// driver; `None` for a driver gone before it heard. + pub fn open(hello: Hello, mac: IndividualMac, conn: Connection, region: Region, groups: &[Group]) -> Option { + let mut end = Stack::new(region.words()); + for group in groups { + assert!(end.join(region.words(), *group), "netstack: more groups than a link holds"); + } + conn.try_send_bytes(wire::MSG_OPENED, &[]).ok()?; + Some(Self { conn, region, end, hello, mac, said: Link::Down, frame: Box::new([0; SLOT_BYTES]), asked: false, ring: true }) + } + + pub fn hello(&self) -> &Hello { + &self.hello + } + + /// The driver's address, as the node takes one. + pub fn mac(&self) -> IndividualMac { + self.mac + } + + /// The link's end if its driver has hung up, read now. + pub fn alive(&self) -> Result<(), Ended> { + self.hear() + } + + /// Whether the node was last told this link is up. + pub fn is_up(&self) -> bool { + self.said.is_up() + } + + pub fn watch(&self, poller: &mut Watches, token: u64) { + poller.watch(&self.conn, READABLE, token); + } + + /// The link's state, the frames the driver received, and what is due + /// to the node; `true` when the receive stopped at its bound, and another + /// pass is owed at once. + pub fn receive(&mut self, node: &mut Node, now: Instant, draw: fn() -> u32) -> Result { + self.hear()?; + let link = self.end.link(self.region.words()).map_err(Ended::Fault)?; + if link.is_up() != self.said.is_up() { + node.link(now, link.is_up(), draw); + } + self.said = link; + let (words, bytes, end, frame) = (self.region.words(), self.region.bytes(), &mut self.end, &mut self.frame); + let mut fault = None; + let owed = node.receive( + now, + RX_DEPTH as usize, + |sink| match end.received(words) { + Ok(Some(span)) => { + let frame = &mut frame[..span.len]; + bytes.read(span.offset, frame); + sink(frame); + true + } + Ok(None) => false, + Err(why) => { + fault = Some(why); + false + } + }, + draw, + ); + self.ring |= self.end.release(words); + match fault { + Some(why) => Err(Ended::Fault(why)), + None => Ok(owed), + } + } + + /// Offer the node the transmit ring's room until it has no frame left or + /// the ring no room, and ring the driver for what was sent. + pub fn transmit(&mut self, node: &mut Node, now: Instant, draw: fn() -> u32) -> Result<(), Ended> { + let words = self.region.words(); + loop { + let room = match self.end.room(words).map_err(Ended::Fault)? { + 0 => self.end.want_room(words).map_err(Ended::Fault)?, + room => room, + } as usize; + let (bytes, end) = (self.region.bytes(), &mut self.end); + let mut fault = None; + let sent = node.transmit( + now, + room, + |frame| { + bytes.write(end.slot().offset, frame); + if let Err(why) = end.send(words, frame.len()) { + fault.get_or_insert(why); + } + }, + draw, + ); + if let Some(why) = fault { + return Err(Ended::Fault(why)); + } + self.ring |= self.end.publish(words); + if room == 0 || sent < room { + return Ok(()); + } + } + } + + /// Ask the driver for its counters, which [`Self::counters`] reads once + /// it answered. + pub fn ask_counters(&mut self) { + if !self.asked { + self.end.ask_counters(self.region.words()); + self.asked = true; + self.ring = true; + } + } + + /// The counters asked for, once the driver answered; `None` while it owes + /// them. + pub fn counters(&mut self) -> Option { + if !self.asked { + return None; + } + let counters = self.end.counters(self.region.words())?; + self.asked = false; + Some(counters) + } + + /// Ring the driver if this pass gave it anything to do; a refusal is a + /// full pipe, a doorbell already rung. + pub fn ring(&mut self) -> Result<(), Ended> { + if std::mem::take(&mut self.ring) { + match self.conn.write_nonblock(&[1]) { + Ok(_) | Err(SyscallError::WouldBlock) => {} + Err(_) => return Err(Ended::HungUp), + } + } + Ok(()) + } + + /// Consume the driver's doorbell bytes; its hang-up is the link's end. + fn hear(&self) -> Result<(), Ended> { + let mut sink = [0u8; 64]; + loop { + match self.conn.read_nonblock(&mut sink) { + Ok(0) => return Err(Ended::HungUp), + Ok(_) => {} + Err(SyscallError::WouldBlock) => return Ok(()), + Err(_) => return Err(Ended::HungUp), + } + } + } + + /// What `inspect` reads about the link: its driver, its address, its + /// state, and what the driver counted. + /// + /// **An unreported link is `unreported`, not `up`**: the device tells the + /// stack nothing about one, and the stack serving as though it were up is + /// its assumption rather than something it measured. + pub fn inspect(&self, counters: Option<&Counters>, snap: &mut Snapshot) { + let m = self.hello.mac.bytes(); + snap.put("mac", format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5])); + snap.put("driver", self.hello.label.as_str()); + match self.said { + Link::Down => snap.put("link.state", "down"), + Link::Unreported => snap.put("link.state", "unreported"), + Link::Up { mbps, full_duplex } => { + snap.put("link.state", "up"); + snap.put("link.speed_mbps", mbps); + snap.put("link.duplex", if full_duplex { "full" } else { "half" }); + } + } + for (counter, value) in counters.into_iter().flat_map(Counters::iter) { + snap.put(counter.name(), value); + } + } +} diff --git a/system/netstack/src/main.rs b/system/netstack/src/main.rs index 813a4dfc8fb..f334489fada 100644 --- a/system/netstack/src/main.rs +++ b/system/netstack/src/main.rs @@ -1,26 +1,32 @@ -//! The network server: one Ethernet card claimed from the kernel, ToyOS's own -//! stack over it (`toyos-net-node`), served to programs over `toyos::net`'s +//! The network server: ToyOS's own stack (`toyos-net-node`) over the link a +//! NIC driver brings (`toyos-link`), served to programs over `toyos::net`'s //! protocol. //! -//! **This program holds no protocol and reads no frame.** Every byte off the -//! wire goes to the node as the card handed it over, and every decision about +//! **This program holds no protocol and drives no device.** Every byte off the +//! wire goes to the node as the link handed it over, and every decision about //! an address, a socket or a segment is the node's. What is here is what the -//! node cannot do and stay pure: the card (`card`), the clock, the kernel's +//! node cannot do and stay pure: the link (`link`), the clock, the kernel's //! random source, the regions clients lend (`lent`), the bells a socket's //! holders are rung on (`pipes`), the clients' connections (`client`) and //! their requests (`serve`). //! -//! **One pass**: the card's link and its received frames, as one batch, go to the node, then -//! every deadline that is due, then the node is offered the card's transmit -//! room until it has no frame left or the card no room; what the node has to +//! **One link, until netstack runs several** +//! (`issues/netstack-drives-one-card.md`): the node is made with the first +//! driver's address, so nothing is served before that driver's hello, and a +//! machine none says hello on within [`LINK_BOUND`] has no network: netstack +//! leaves, as a client then hears. A link that ends leaves every connection +//! to the node, and only a hello with that address is opened again. +//! +//! **One pass**: the link's state and its received frames, as one batch, go to the node, then +//! every deadline that is due, then the node is offered the link's transmit +//! room until it has no frame left or the link no room; what the node has to //! say is written to the log and to the clients that waited for it; and the -//! loop sleeps on the kernel until the card, a client, a watched handle or the +//! loop sleeps on the kernel until the link, a client, a watched handle or the //! node's next deadline wakes it. A request is carried out in the pass that //! read it and its frames leave in the next, which follows at once. //! -//! **A frame leaves only into room the card said it has** (`Card::tx_room`): -//! the node builds none without it, and a card with none wakes the pass that -//! has (`Card::wake_on_room`). +//! **A frame leaves only into room the link said it has**: the node builds +//! none without it, and a full ring rings this program when it has room. //! //! **Every draw is the kernel's**, the stack's secrets and each port and //! transaction id after them, and a kernel that refuses one ends netstack by @@ -33,51 +39,41 @@ use toyos::ipc::{self, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::say; use toyos::AsHandle; -use toyos_abi::syscall::{PciId, PIPE_CAPACITY}; -use toyos_manifest::device::PciMatch; +use toyos::port::Acceptor; +use toyos_abi::syscall::PIPE_CAPACITY; use toyos_dhcp::{HostName, Lease}; -use toyos_i219::Part; use toyos_inspect::Snapshot; +use toyos_link::state::{Counters, Group}; +use toyos_link::wire::{Hello, Refusal}; use toyos_net_ip::Nud; use toyos_net_node::Node; use toyos_net_shard::{Config, Secrets}; -use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +use toyos_net_wire::ethernet::IndividualMac; use toyos_net_wire::Instant; -mod card; mod client; -mod i219; mod lent; +mod link; mod pipes; mod serve; -mod virtio_net; mod watches; -use card::Card; use client::{Client, ClientRx, HANDSHAKE_TIMEOUT, MAX_PENDING_CONNS, PendingConn, Request}; +use link::{Ended, Heard, Pending, Session}; -/// The cards this program can drive, matched by their class and what -/// identifies the card rather than by the slot firmware put it in, and each -/// with the driver that opens it. The manifest row spells the same matcher and -/// the claim arrives under its text, so which of these exists is -/// the supervisor's answer and not this program's — at most one is -/// ever endowed, and a machine with none is a machine netstack leaves. -/// -/// `1af4:1041` is virtio's transitional device id `1000 + 1` for a network -/// device (virtio 1.2 §5.1.1). `8086:15fc` is the ThinkPad T14's onboard I219 -/// 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: [(PciMatch, fn(toyos::PciDev) -> Card); 3] = [ - (ethernet(0x8086, 0x15fc), |c| Card::intel(c, Part::I219)), - (ethernet(0x8086, 0x10d3), |c| Card::intel(c, Part::E82574)), - (ethernet(0x1af4, 0x1041), Card::virtio), -]; - -/// An Ethernet controller, class `02:00:00`. -const fn ethernet(vendor: u16, device: u16) -> PciMatch { - PciMatch::new([0x02, 0x00, 0x00], Some(PciId { vendor, device })) -} +/// How long netstack waits for its first link before it leaves as a machine +/// with no network. Policy: a driver says hello once its device is up, which +/// is seconds of this; a machine with no NIC driver waits it out, and its +/// clients with it. +const LINK_BOUND: Duration = Duration::from_secs(30); + +/// How long an `inspect` waits for the driver's counters. The driver answers +/// in the pass the ask's doorbell starts; one that does not is answered for +/// without them. +const INSPECT_BOUND: Duration = Duration::from_secs(1); + +/// Drivers' connections that have not said hello, watched at once. +const MAX_PENDING_LINKS: u32 = 2; /// The name this machine asks its network to record for it, and answers to on /// it as `.local` once no other host does. One name, because there is @@ -133,8 +129,9 @@ const PLACE_BYTES: u64 = { /// this in favour of a kernel memory limit, not in favour of a bigger number. const PLACE_BUDGET_SHARE: u64 = 8; -/// Watches that are no place's: the service's acceptor and the card's claim. -const FIXED_WATCHES: u32 = 2; +/// Watches that are no place's: the service's two acceptors, the link, and +/// the drivers' connections that have not said hello. +const FIXED_WATCHES: u32 = 3 + MAX_PENDING_LINKS; /// The most places one poller can watch: each place is one watched handle, a /// holder's connection of a stream, a listener or a datagram socket, asked @@ -299,51 +296,155 @@ impl Leases { /// Answer `inspect` with what this pass knows, in one non-blocking write, and /// let the connection close as every other answer does. -fn answer_inspect(request: &Request, card: &Card, leases: &Leases, sockets: &serve::Sockets, node: &Node) { - // The request is a bare header, and anything riding on one is not this - // protocol. - if request.payload_len != 0 { - request.client.error(toyos::net::ERR_INVALID_INPUT); - return; - } +fn answer_inspect(request: &Request, link: Option<(&Session, Option<&Counters>)>, leases: &Leases, sockets: &serve::Sockets, node: &Node) { let mut snap = Snapshot::new(toyos_inspect::NET); - card.inspect(&mut snap); + match link { + Some((link, counters)) => link.inspect(counters, &mut snap), + None => snap.put("link.state", "none"), + } leases.inspect(node, &mut snap); sockets.inspect(node, &mut snap); let encoded = snap.encode().unwrap_or_else(|why| panic!("netstack: its snapshot: {why}")); request.client.snapshot(&encoded); } +/// Open the link `asked` asks for, and say so. +fn open(asked: Asked, groups: &[Group]) -> Option { + let label = asked.hello.label.clone(); + let opened = Session::open(asked.hello, asked.mac, asked.conn, asked.region, groups); + match &opened { + Some(_) => say!("netstack: {label}'s link is open"), + None => say!("netstack: {label} went before its link was opened"), + } + opened +} + +/// Refuse `asked` by `why`, and say so. +fn refuse(asked: &Asked, why: Refusal, because: &str) { + say!("netstack: refused {}'s link: {because}", asked.hello.label); + link::refuse(&asked.conn, why); +} + +/// Every hello the pending drivers said, in the order heard; a connection +/// that went, said what no driver says first, or has said nothing within +/// [`HANDSHAKE_TIMEOUT`] is let go. +fn hear_links(pending: &mut Vec) -> Vec { + pending.retain(|p| p.since.elapsed() < HANDSHAKE_TIMEOUT); + let mut asked = Vec::new(); + let mut i = 0; + while i < pending.len() { + match pending[i].hear() { + Heard::Nothing => i += 1, + Heard::Gone => { + pending.remove(i); + } + Heard::Hello(hello, mac, region) => { + let conn = pending.remove(i).into_conn(); + asked.push(Asked { hello, mac, conn, region }); + } + } + } + asked +} + +/// End `link` for `why`; the node hears a change, not a state. +fn end_link(link: &mut Option, why: Ended, node: &mut Node, now: Instant, faulted: &mut bool) { + let Some(session) = link.take() else { return }; + match why { + Ended::HungUp => say!("netstack: {}'s link ended: it hung up", session.hello().label), + Ended::Fault(fault) => { + say!("netstack: {}'s link ended: it wrote {fault}", session.hello().label); + *faulted = true; + } + } + if session.is_up() { + node.link(now, false, draw); + } +} + +/// A driver's connection, kept to hear its hello while there is room. +fn accept_link(links: &Acceptor, pending: &mut Vec) { + match links.accept() { + Ok(conn) if pending.len() < MAX_PENDING_LINKS as usize => pending.push(Pending::new(conn)), + Ok(_) => say!("netstack: refused a NIC driver's connection: {MAX_PENDING_LINKS} have not said hello yet"), + Err(why) => say!("netstack: the kernel refused a NIC driver's connection: {why:?}"), + } +} + +/// The first link a driver brings, within [`LINK_BOUND`]. +fn first_link(links: &Acceptor, groups: &[Group]) -> Option { + let began = Wall::now(); + let mut pending: Vec = Vec::new(); + let mut poller = watches::Watches::new(1 + MAX_PENDING_LINKS); + loop { + let mut opened = None; + for asked in hear_links(&mut pending) { + match opened { + Some(_) => refuse(&asked, Refusal::OneLink, "netstack drives one link (issues/netstack-drives-one-card.md)"), + None => opened = open(asked, groups), + } + } + if opened.is_some() { + return opened; + } + let left = LINK_BOUND.checked_sub(began.elapsed())?; + poller.watch(links, READABLE, TOKEN_LINKS); + for (i, p) in pending.iter().enumerate() { + p.watch(&mut poller, TOKEN_LINK_PENDING_BASE + i as u64); + } + let mut accept = false; + poller.wait_answers(1, u64::try_from(left.as_nanos()).unwrap_or(u64::MAX), |token, _| accept |= token == TOKEN_LINKS); + if accept { + accept_link(links, &mut pending); + } + } +} + +const TOKEN_ACCEPTOR: u64 = 0; +const TOKEN_LINKS: u64 = 1; +const TOKEN_LINK: u64 = 2; +const TOKEN_LINK_PENDING_BASE: u64 = 3; + const _: () = assert!( toyos_inspect::MAX_SNAPSHOT_BYTES == ipc::MAX_FRAME_LEN as usize, "a snapshot is one frame" ); +/// A driver's hello, with what it came with. +struct Asked { + hello: Hello, + mac: IndividualMac, + conn: toyos::Connection, + region: toyos_link::region::Region, +} + +/// An `inspect` waiting for the driver's counters. +struct Inspecting { + request: Request, + since: Wall, +} + fn main() { // The `netstack` port exists before this process does: a client's // connection is queued on it whether or not this program ever reaches // `accept`, and if netstack exits the queued client sees `Gone` rather // than silence. - let Some((open, claim)) = CARDS - .iter() - .find_map(|(card, open)| endow::claim::(&card.to_string()).map(|c| (*open, c))) - else { - say!("netstack: no NIC on this machine, exiting"); - return; - }; let acceptor = endow::acceptor("netstack") .expect("the manifest declares this program serves `netstack`"); - let card = open(claim); - let mac = card.mac(); - say!( - "netstack: MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] - ); + let links = endow::acceptor(toyos_link::PORT) + .expect("the manifest declares this program serves `link`"); + let groups = [Group::new(toyos_mdns::GROUP_MAC).unwrap_or_else(|| panic!("netstack: mDNS's address is no group's"))]; + let Some(first) = first_link(&links, &groups) else { + say!("netstack: no NIC driver brought a link in {} s; this machine has no network, exiting", LINK_BOUND.as_secs()); + return; + }; + let mac = first.mac(); + let mut link = Some(first); let began = Wall::now(); let clock = || Instant::from_nanos(u64::try_from(began.elapsed().as_nanos()).unwrap_or(u64::MAX)); let config = Config { - mac: IndividualMac::new(MacAddr(mac)).unwrap_or_else(|| panic!("netstack: the card's address is a group's")), + mac, receive_buffer: TCP_BUFFER, send_buffer: toyos::net::stream::SEND_BYTES, secrets: secrets(), @@ -352,9 +453,6 @@ fn main() { let mut node = Node::new(clock(), config, Some(host), draw) .unwrap_or_else(|why| panic!("netstack: the stack refused its configuration: {why:?}")); - if let Card::Intel(nic) = &card { - nic.accept_multicast(toyos_mdns::GROUP_MAC); - } let name = toyos_mdns::Host::new(HOSTNAME).unwrap_or_else(|_| panic!("netstack: {HOSTNAME:?} is no host name")); node.answer_as(clock(), name, draw) .unwrap_or_else(|why| panic!("netstack: a new node refused the multicast DNS port: {why:?}")); @@ -365,57 +463,68 @@ fn main() { let mut sockets = serve::Sockets::new(places); let mut leases = Leases { began, said: None, settled: false }; - // The link as the card came up with it, which the first change a pass - // reports is measured against. Virtio reports no link changes at all. The - // node begins with its link down. - let mut link_up = match &card { - Card::Intel(intel) => intel.link().is_up(), - Card::Virtio(_) => true, - }; - if link_up { - node.link(clock(), true, draw); - } - let mut poller = watches::Watches::new( FIXED_WATCHES + MAX_PLACES + MAX_PENDING_CONNS + serve::LOOKUP_WATCHES, ); - const TOKEN_ACCEPTOR: u64 = 0; - const TOKEN_NIC: u64 = 1; - // Clear of the two above and of a connection's own handle by more than + // Clear of the ones above and of a connection's own handle by more than // `MAX_HANDLES` (4096, `kernel/src/object/handle.rs`); `serve`'s tokens // are all above the low word. const TOKEN_PENDING_BASE: u64 = 0x1_0000; let mut pending: Vec = Vec::new(); + let mut pending_links: Vec = Vec::new(); + let mut inspecting: Vec = Vec::new(); + // The held link ended for what its driver wrote, and its next hello is + // refused for it. + let mut faulted = false; // Accepts the kernel refused since the last it did not. let mut accept_refused: u64 = 0; loop { - // First, because it is what makes the interrupt taken. - if let Some(link) = card.begin_pass() { - // A change of state only: a speed change is no new network. - if link.is_up() != link_up { - link_up = link.is_up(); - node.link(clock(), link_up, draw); + // The node begins with its link down, and hears each change of it here. + let now = clock(); + let mut owed = false; + let mut ended = None; + if let Some(session) = &mut link { + match session.receive(&mut node, now, draw) { + Ok(more) => owed = more, + Err(why) => ended = Some(why), } } - let owed = card.receive(|budget, next| node.receive(clock(), budget, next, draw)); let now = clock(); if node.next_deadline().is_some_and(|at| at <= now) { node.fire(now, draw); } - loop { - // A card with no room is asked to say when it has some, and a - // frame the node still holds leaves in the pass that wakes. - let room = match card.tx_room() { - 0 => card.wake_on_room(), - room => room, - }; - if room == 0 || node.transmit(now, room, |frame| card.tx(frame), draw) < room { - break; + if ended.is_none() + && let Some(session) = &mut link + && let Err(why) = session.transmit(&mut node, now, draw) + { + ended = Some(why); + } + + // The driver's counters, once it answered them or its time to is up. + if !inspecting.is_empty() { + // A link opened since the ask is asked in its turn. + if let Some(session) = &mut link { + session.ask_counters(); + } + let counters = link.as_mut().and_then(Session::counters); + let late = inspecting.first().is_some_and(|i| i.since.elapsed() >= INSPECT_BOUND); + if counters.is_some() || late || link.is_none() { + for waiting in inspecting.drain(..) { + answer_inspect(&waiting.request, link.as_ref().map(|l| (l, counters.as_ref())), &leases, &sockets, &node); + } } } - card.report(); + if ended.is_none() + && let Some(session) = &mut link + && let Err(why) = session.ring() + { + ended = Some(why); + } + if let Some(why) = ended { + end_link(&mut link, why, &mut node, clock(), &mut faulted); + } sockets.settle(&mut node); // Before anything is served: the lease is what gives this machine an @@ -429,7 +538,13 @@ fn main() { } poller.watch(&acceptor, READABLE, TOKEN_ACCEPTOR); - poller.watch(card.claim(), READABLE, TOKEN_NIC); + poller.watch(&links, READABLE, TOKEN_LINKS); + if let Some(session) = &link { + session.watch(&mut poller, TOKEN_LINK); + } + for (i, p) in pending_links.iter().enumerate() { + p.watch(&mut poller, TOKEN_LINK_PENDING_BASE + i as u64); + } sockets.watch(&mut poller); for p in pending.iter() { poller.watch(&p.conn, READABLE, TOKEN_PENDING_BASE + p.conn.as_handle().raw().0 as u64); @@ -437,8 +552,7 @@ fn main() { // The node's next deadline: a retransmission, a lease's timer, a // lookup's wait, a connect's. Zero when one is due, and when the - // receive stopped at its bound: the interrupts of the frames it left - // were taken with this pass's. + // receive stopped at its bound. let nanos = |left: Duration| u64::try_from(left.as_nanos()).unwrap_or(u64::MAX); let mut timeout = node.next_deadline().map_or(u64::MAX, |at| nanos(at.since(clock()))); if owed { @@ -447,11 +561,12 @@ fn main() { if let Some(left) = leases.due_in() { timeout = timeout.min(nanos(left)); } - // A card that never does what it owes sends no interrupt to say so. - timeout = timeout.min(card.pass_due_in().unwrap_or(u64::MAX)); + if let Some(first) = inspecting.first() { + timeout = timeout.min(nanos(INSPECT_BOUND.saturating_sub(first.since.elapsed()))); + } // A client that connects and then says nothing wakes nothing, so the // deadline that removes it is a wake in its own right. - if !pending.is_empty() { + if !pending.is_empty() || !pending_links.is_empty() { timeout = timeout.min(nanos(HANDSHAKE_TIMEOUT)); } @@ -463,6 +578,33 @@ fn main() { }); sockets.bridge(&mut node, clock()); + // A driver's connection: a hello with the address the node was made + // with is opened, once a link it replaces has ended. The kernel closes + // a dead driver's handles before its end is published, so a + // restarted driver's hello finds the old link already hung up. + if ready.contains(&TOKEN_LINKS) { + accept_link(&links, &mut pending_links); + } + for asked in hear_links(&mut pending_links) { + if asked.mac != mac { + refuse(&asked, Refusal::OneLink, "its address is not the one netstack drives (issues/netstack-drives-one-card.md)"); + continue; + } + if std::mem::take(&mut faulted) { + refuse(&asked, Refusal::Faulted, "its last link ended for what it wrote"); + continue; + } + if let Some(session) = &link { + match session.alive() { + Ok(()) => { + refuse(&asked, Refusal::OneLink, "a link of its address is open"); + continue; + } + Err(why) => end_link(&mut link, why, &mut node, clock(), &mut faulted), + } + } + link = open(asked, &groups); + } // On a pass that found nothing ready too: otherwise a silent client // is only ever timed out by some other client's traffic. let now_wall = Wall::now(); @@ -559,7 +701,16 @@ fn main() { for request in requests { if request.msg_type == toyos_inspect::MSG_INSPECT { - answer_inspect(&request, &card, &leases, &sockets, &node); + // The request is a bare header, and anything riding on one is + // not this protocol. + if request.payload_len != 0 { + request.client.error(toyos::net::ERR_INVALID_INPUT); + } else if let Some(session) = link.as_mut().filter(|_| inspecting.len() < MAX_PENDING_CONNS as usize) { + session.ask_counters(); + inspecting.push(Inspecting { request, since: Wall::now() }); + } else { + answer_inspect(&request, link.as_ref().map(|l| (l, None)), &leases, &sockets, &node); + } continue; } sockets.request(&mut node, clock(), request, &acceptor, draw); diff --git a/system/virtio-net/Cargo.toml b/system/virtio-net/Cargo.toml new file mode 100644 index 00000000000..6c29bbfe16c --- /dev/null +++ b/system/virtio-net/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "virtio-net" +description = "The virtio network driver: one process per virtio-net function the kernel does not drive, holding its claim and carrying its frames to netstack over a link." +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } +toyos = { path = "../../sdk/toyos" } +toyos-device-memory = { path = "../../lib/device-memory" } +toyos-link = { path = "../../lib/link" } +toyos-manifest = { path = "../../lib/manifest" } +toyos-pci-claim = { path = "../../lib/pci-claim" } +toyos-virtio = { path = "../../lib/virtio" } + +[dev-dependencies] +toyos-pci-claim = { path = "../../lib/pci-claim", features = ["host-grant"] } + +[package.metadata.toyos.host] +exempt.owns = "one virtio-net function, claimed from the kernel, and its link to netstack" diff --git a/system/virtio-net/src/main.rs b/system/virtio-net/src/main.rs new file mode 100644 index 00000000000..e84f6cf7bda --- /dev/null +++ b/system/virtio-net/src/main.rs @@ -0,0 +1,28 @@ +//! The virtio network driver: one process per virtio-net function the kernel +//! does not drive, holding its claim (a `drives` row) and carrying its frames +//! to the network stack over a link (`toyos-link`). + +use toyos::endow; +use toyos::{say, PciDev}; +use toyos_abi::syscall::PciId; +use toyos_link::driver::Nic as _; +use toyos_manifest::device::PciMatch; + +mod nic; + +/// A virtio network device, class `02:00:00`: device id `0x1041` is virtio's +/// `0x1040 + 1` for one (virtio 1.2 §4.1.2.1). +const FUNCTION: PciMatch = PciMatch::new([0x02, 0x00, 0x00], Some(PciId { vendor: 0x1af4, device: 0x1041 })); + +fn main() { + let claim = endow::claim::(&FUNCTION.to_string()) + .expect("virtio-net: the drives row endows this process its function's claim"); + // A device the claim was minted on that this driver cannot bring up is + // something it said that this driver does not understand: loud, and the + // supervisor's to start again or give up on. + let mut nic = nic::VirtioNet::open(claim) + .unwrap_or_else(|why| panic!("virtio-net: the NIC this program was given is not one it can drive — {why}")); + let m = nic.mac(); + say!("virtio-net: MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5]); + toyos_link::driver::drive("virtio-net", &mut nic); +} diff --git a/system/netstack/src/virtio_net.rs b/system/virtio-net/src/nic.rs similarity index 83% rename from system/netstack/src/virtio_net.rs rename to system/virtio-net/src/nic.rs index 32c28d2931e..ea49a439964 100644 --- a/system/netstack/src/virtio_net.rs +++ b/system/virtio-net/src/nic.rs @@ -1,4 +1,5 @@ -//! The virtio-net driver. It runs here, in netstack, not in the kernel. +//! The virtio-net device, as this program drives it: in userland, not in the +//! kernel. //! //! What the kernel keeps is the *claim*: config space, the interrupt vector it //! programmed into this function's MSI-X table, and the address space the @@ -21,12 +22,11 @@ //! Virtio 1.2 throughout: §4.1.4 for the PCI capability layout, §5.1 for the //! network device. -use std::cell::RefCell; - -use toyos::shm::SharedMemory; -use toyos::{DmaRegion, PciDev}; +use toyos::shm::{ReadBytes, SharedMemory}; +use toyos::{say, DmaRegion, PciDev}; use toyos_abi::syscall::SyscallError; use toyos_device_memory::DmaBuffers; +use toyos_link::state::{Group, Link}; use toyos_pci_claim::virtio::{negotiate, Negotiated, Refusal}; use toyos_pci_claim::{Bar, Grant}; use toyos_virtio::pci::Live; @@ -44,8 +44,8 @@ const TX_QUEUE: u16 = 1; /// `i` is posted at head `i` and nowhere else, so a completion's head *is* the /// buffer it filled and nothing the device wrote is used as an index. const RX_QUEUE_SIZE: u16 = 256; -pub const RX_BUF_COUNT: usize = RX_QUEUE_SIZE as usize; -pub const RX_BUF_SIZE: usize = 4096; +const RX_BUF_COUNT: usize = RX_QUEUE_SIZE as usize; +const RX_BUF_SIZE: usize = 4096; /// One descriptor per transmit buffer, and the two counts are one number for /// the same reason the receive side's are: buffer `i` is published at head `i` /// and nowhere else, so a head this driver holds names a buffer nothing else @@ -53,11 +53,11 @@ pub const RX_BUF_SIZE: usize = 4096; /// emits several frames per poll, and the device reads a descriptor whenever it /// likes. const TX_QUEUE_SIZE: u16 = 16; -pub const TX_BUF_COUNT: usize = TX_QUEUE_SIZE as usize; -pub const TX_BUF_SIZE: usize = 4096; +const TX_BUF_COUNT: usize = TX_QUEUE_SIZE as usize; +const TX_BUF_SIZE: usize = 4096; /// The header virtio 1.0 puts in front of every frame, both directions: always /// twelve bytes with `VERSION_1`, `num_buffers` included (§5.1.6). -pub const NET_HDR_SIZE: usize = 12; +const NET_HDR_SIZE: usize = 12; /// The grant's layout: the receive queue's three parts on a page each, the /// transmit queue's three in one page, then the frames. One grant, because @@ -86,7 +86,7 @@ const _: () = { fn finished(rings: &mut Virtqueue) -> Option { rings .poll_used() - .unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}")) + .unwrap_or_else(|why| panic!("virtio-net: this NIC cannot be driven on — {why}")) } /// The virtio-net function, brought up and driving. @@ -98,8 +98,8 @@ pub struct VirtioNet { _bar: SharedMemory, _region: DmaRegion, grant: Grant, - rx: RefCell>, - tx: RefCell, + rx: Virtqueue, + tx: TxQueue, mac: [u8; 6], } @@ -109,7 +109,7 @@ impl VirtioNet { /// `toyos-virtio`'s types fix, which is virtio 1.2 §3.1.1's. pub fn open(dev: PciDev) -> Result { let Negotiated { setup, mapping, features } = negotiate(&dev, VIRTIO_NET_F_MAC)?; - crate::say!("netstack: {features}"); + say!("virtio-net: {features}"); let (grant, region) = Grant::alloc(&dev, GRANT_BYTES)?; grant.window().zero(); @@ -128,14 +128,14 @@ impl VirtioNet { (setup, *byte) = setup.device_read8(at)?; } - let nic = Self { + let mut nic = Self { dev, device: setup.driver_ok(), _bar: mapping, _region: region, grant, - rx: RefCell::new(rx), - tx: RefCell::new(tx), + rx, + tx, mac, }; @@ -146,43 +146,24 @@ impl VirtioNet { Ok(nic) } - pub fn mac(&self) -> [u8; 6] { - self.mac - } - - /// Drain the interrupt record, so the claim stops reading ready. - /// - /// The count is not acted on — what a message meant is in the rings — but - /// it has to be consumed, or the poller reports the same interrupt for ever. - /// `WouldBlock` is the ordinary "nothing since the last read". Any other - /// refusal is handed up as the kernel worded it: what it means is - /// `Card::begin_pass`'s call, made once for both drivers. - pub fn take_interrupt(&self) -> Result { - match self.dev.irq() { - Ok(record) => Ok(record.count), - Err(SyscallError::WouldBlock) => Ok(0), - Err(why) => Err(why), - } - } - /// Post receive buffer `index` on its own head. - fn post_rx(&self, index: usize) { + fn post_rx(&mut self, index: usize) { let at = OFF_RX_BUFS + index * RX_BUF_SIZE; // The header is zeroed before the buffer is published: what the device // writes there is its own, and what it leaves behind is this driver's. self.grant.window().sub(at, NET_HDR_SIZE).zero(); let buffer = Buffer::writable(self.grant.device_addr(at), RX_BUF_SIZE as u32); - let published = self.rx.borrow_mut().publish(index as u16, &[buffer]); + let published = self.rx.publish(index as u16, &[buffer]); self.device.notify(published); } /// The next received frame as `(buffer index, frame bytes)`, the virtio /// header excluded, or `None`. - pub fn poll_rx(&self) -> Option<(usize, usize)> { + fn poll_rx(&mut self) -> Option<(usize, usize)> { loop { // The head is below this queue's size, which is the buffer count, // and `written` no more than the one buffer its chain is. - let Used { head, written } = finished(&mut self.rx.borrow_mut())?; + let Used { head, written } = finished(&mut self.rx)?; let (index, total) = (head as usize, written as usize); if total <= NET_HDR_SIZE { // Shorter than its own header is nothing to hand up, and the @@ -193,45 +174,63 @@ impl VirtioNet { return Some((index, total - NET_HDR_SIZE)); } } +} + +impl toyos_link::driver::Nic for VirtioNet { + fn mac(&self) -> [u8; 6] { + self.mac + } - /// Give buffer `index` back to the device, after its frame has been read. + fn claim(&self) -> &PciDev { + &self.dev + } + + /// Drain the interrupt record, so the claim stops reading ready. The + /// count is not acted on — what a message meant is in the rings — but it + /// has to be consumed, or the poller reports the same interrupt for ever. /// - /// A buffer never given back is a receive slot lost for the boot: 256 of - /// them and this NIC stops receiving. - pub fn rx_done(&self, index: usize) { - assert!(index < RX_BUF_COUNT, "netstack: buffer {index} is not one this NIC has"); - self.post_rx(index); + /// A claim that refuses the read for anything but `WouldBlock` is the + /// kernel saying this function is no longer this process's, and this + /// dies where it can be read. + /// + /// The link is unreported: this driver accepts no `VIRTIO_NET_F_STATUS`. + fn begin_pass(&mut self) -> Link { + match self.dev.irq() { + Ok(_) | Err(SyscallError::WouldBlock) => Link::Unreported, + Err(why) => panic!("virtio-net: this NIC cannot be driven on — {why:?}"), + } } - /// Where a received frame's bytes are, past the virtio header. - pub fn rx_frame(&self, index: usize, len: usize) -> &[u8] { + /// A buffer is posted again once its frame was taken: one never given + /// back is a receive slot lost for the boot, and 256 of them and this NIC + /// stops receiving. + fn receive(&mut self, take: &mut dyn FnMut(&[u8])) -> bool { + let Some((index, len)) = self.poll_rx() else { return false }; let window = self.grant.window().sub(OFF_RX_BUFS + index * RX_BUF_SIZE + NET_HDR_SIZE, len); // SAFETY: the window is inside the grant, which lives as long as // `self`; the device has finished with this buffer — its used-ring - // element is what said so — and it is not posted again until - // `rx_done`, which the caller makes after this borrow ends. virtio is - // the hypervisor's, inside the trusted base, so its protocol is believed; - // an external or hot-plug device's driver must copy instead. - unsafe { std::slice::from_raw_parts(window.as_ptr() as *const u8, len) } + // element is what said so — and it is not posted again until after + // this borrow ends. virtio is the hypervisor's, inside the trusted + // base, so its protocol is believed; an external or hot-plug device's + // driver must copy instead. + take(unsafe { std::slice::from_raw_parts(window.as_ptr() as *const u8, len) }); + self.post_rx(index); + true } - /// How many frames the transmit queue takes now ([`TxQueue::room`]). A - /// caller answered 0 sleeps on the claim. - pub fn tx_room(&self) -> usize { - self.tx.borrow_mut().room() + fn tx_room(&mut self) -> usize { + self.tx.room() } - /// Copy `frame` into a transmit buffer and hand it to the device - /// ([`TxQueue::send`]). - pub fn tx(&self, frame: &[u8]) { - let ((), published) = self.tx.borrow_mut().send(frame.len(), |slot| slot.copy_from_slice(frame)); + fn send(&mut self, frame: ReadBytes<'_>) { + let ((), published) = self.tx.send(frame.len(), |slot| frame.read(0, slot)); self.device.notify(published); } - /// The claim, for the poller: readable means an interrupt has landed. - pub fn claim(&self) -> &PciDev { - &self.dev - } + /// Nothing to ask: this driver accepts no `VIRTIO_NET_F_CTRL_RX`, so it + /// programs no receive filter, and the frames the stack's groups are sent + /// arrive today without one. + fn join(&mut self, _group: Group) {} } /// The transmit queue: its rings, the heads nothing is in flight on, and the @@ -281,12 +280,12 @@ impl TxQueue { fn send(&mut self, len: usize, fill: impl FnOnce(&mut [u8]) -> R) -> (R, Published) { assert!( NET_HDR_SIZE + len <= TX_BUF_SIZE, - "netstack: a {len}-byte frame does not fit this NIC's transmit buffer" + "virtio-net: a {len}-byte frame does not fit this NIC's transmit buffer" ); let head = self .free .pop() - .expect("netstack: a frame was offered to a transmit queue that had said it has no room"); + .expect("virtio-net: a frame was offered to a transmit queue that had said it has no room"); let at = OFF_TX_BUFS + head as usize * TX_BUF_SIZE; // The header is this driver's and zeroed before the frame goes in. self.grant.window().sub(at, NET_HDR_SIZE).zero(); @@ -379,7 +378,7 @@ mod tests { assert_eq!(queue.room(), 2); } - /// A frame offered to a queue that said it had no room is netstack's own + /// A frame offered to a queue that said it had no room is the driver's own /// bug, and dies by name instead of being written somewhere and dropped. #[test] #[should_panic(expected = "had said it has no room")] diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index 20a5cafd8e7..0546c986e2f 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -13,8 +13,8 @@ use std::path::Path; use super::qemu::{self, is_kernel_line, BootOptions, Profile, QemuInstance}; use super::serial::Serial; -/// The function `tests/images/netcase.toml`'s netstack claims, as its `devices` row spells it. -const NETSTACK_CLAIMS: &str = "1af4:1041"; +/// The function `tests/images/netcase.toml`'s virtio-net row drives, as the row spells it. +const NIC_CLAIMS: &str = "1af4:1041"; /// The boot config that runs `netstack` with a virtio NIC in front of it. fn netcase() -> std::path::PathBuf { @@ -72,10 +72,13 @@ pub fn iommu_virtio_platform() -> Result<(), String> { let said: Vec = if behind_unit { vec![bar_handed(), msix_armed()] } else { + // The NIC's driver is a `drives` row, which the supervisor + // starts after every service: its refusal is the boot's last. vec![ not_handed_over(CLAIMED_AT, NOT_REMAPPED), not_handed_over(NVME_AT, NOT_REMAPPED), not_handed_over(SOUND_AT, NOT_REMAPPED), + not_handed_to_its_driver(NIC_CLAIMS), ] }; let mut text = qemu.boot_log().to_string(); @@ -186,7 +189,7 @@ fn not_handed_over(at: &str, why: &str) -> String { /// would fail. The NVMe controller and the virtio-sound function are refused /// the same. fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { - refused_claim(log, NETSTACK_CLAIMS, NOT_REMAPPED, &[XHCI_AT, NVME_AT, SOUND_AT])?; + refused_claim(log, NIC_CLAIMS, NOT_REMAPPED, &[XHCI_AT, NVME_AT, SOUND_AT])?; log.must_say(¬_handed_over(XHCI_AT, NOT_REMAPPED))?; log.must_say(&format!( "supervisor: usbd was not started on PCI {XHCI_AT}: pci:0c:03:30/1033:0194 is on this machine and could not be handed over" @@ -204,8 +207,8 @@ fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { Ok(()) } -/// The slot QEMU's `-device` order puts the function netstack claims on, and the -/// address every judge below is an assertion about. +/// The slot QEMU's `-device` order puts the function virtio-net drives on, and +/// the address every judge below is an assertion about. /// /// **The address is the harness's own and never the guest's.** A judge that /// reads the function out of the console and then asserts about *that* asserts @@ -279,15 +282,19 @@ fn refused_claim(log: &Serial, claims: &str, why: &str, beside: &[&str]) -> Resu log.must_not_say(&msi_armed())?; log.must_not_say(&bar_handed())?; // All the way out to userland, rather than a kernel that logged a refusal - // and handed netstack a NIC anyway. The supervisor names what it could not mint in the - // config's own spelling, and **with this refusal's own word**: the machine - // has the function, so "no such device on this machine" would be false. - log.must_say(&format!( - "supervisor: netstack: pci:02:00:00/{claims} is on this machine and could not be handed over" - ))?; + // and handed the NIC's driver its function anyway. The supervisor names + // what it could not mint in the config's own spelling, and **with this + // refusal's own word**: the machine has the function, so "no such device on + // this machine" would be false. + log.must_say(¬_handed_to_its_driver(claims))?; Ok(()) } +/// What the supervisor says of the NIC's claim the kernel would not hand over. +fn not_handed_to_its_driver(claims: &str) -> String { + format!("supervisor: virtio-net was not started on PCI {CLAIMED_AT}: pci:02:00:00/{claims} is on this machine and could not be handed over") +} + fn unit_is_first(argv: &[String], name: &str) -> Result<(), String> { let devices: Vec<&str> = @@ -500,28 +507,38 @@ pub fn amdvi_selftest(test_config: &Path) -> Result<(), String> { Ok(()) } -/// What the supervisor says refusing netcase's NIC to netstack, on a machine +/// What the supervisor says refusing netcase's NIC to its driver, on a machine /// whose virtio NIC was created without `iommu_platform=on`. -pub const UNTRANSLATED_NIC: &str = "supervisor: netstack: pci:02:00:00/1af4:1041 is a virtio function that offers \ - no VIRTIO_F_ACCESS_PLATFORM, so no unit translates its DMA (virtio 1.2 §6.1): refused"; +fn untranslated_nic() -> String { + format!( + "supervisor: virtio-net was not started on PCI {CLAIMED_AT}: pci:02:00:00/1af4:1041 is a virtio function that \ + offers no VIRTIO_F_ACCESS_PLATFORM, so no unit translates its DMA (virtio 1.2 §6.1): refused" + ) +} /// A virtio NIC that bypasses the unit ([`Profile::HeadlessNicUntranslated`]) -/// is never handed to netstack: the supervisor reads its offer through the -/// claim, refuses it by name, and netstack runs with no NIC. Only a QEMU -/// machine can make one: every bench function is behind its unit. +/// is never handed to its driver: the supervisor reads its offer through the +/// claim, refuses it by name, starts no NIC driver, and netstack leaves once +/// its wait for a link is out. Only a QEMU machine can make one: every bench +/// function is behind its unit. pub fn a_virtio_function_without_access_platform_is_refused() -> Result<(), String> { let options = BootOptions { profile: Profile::HeadlessNicUntranslated, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(&netcase(), &[], &[], options); - let gone = "netstack: no NIC on this machine"; + let refused = untranslated_nic(); + let gone = "netstack: no NIC driver brought a link in 30 s; this machine has no network, exiting"; let mut text = qemu.boot_log().to_string(); - qemu::await_guest(&mut qemu, &mut text, "the supervisor's refusal and netstack without a NIC", |t| { - t.contains(UNTRANSLATED_NIC) && t.contains(gone) - }) - .map_err(|e| format!("{e}\n{text}"))?; + qemu::await_guest(&mut qemu, &mut text, "the supervisor's refusal", |t| t.contains(&refused)).map_err(|e| format!("{e}\n{text}"))?; + // A quiet guest, for netstack's bound on its first link: a ceiling past it. + if !text.contains(gone) { + text.push_str(&qemu.drain_until(std::time::Duration::from_secs(90), |l| l.contains(gone))); + } + if !text.contains(gone) { + return Err(format!("netstack did not leave a machine whose NIC has no driver\n{text}")); + } let log = Serial::named("boot console", text); log.must_be_clean()?; - log.must_not_say("netstack: VirtIO: PCI")?; - eprintln!(" [iommu] {UNTRANSLATED_NIC}"); + log.must_not_say("virtio-net: VirtIO: PCI")?; + eprintln!(" [iommu] {refused}"); Ok(()) } diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 1d868d7d83a..0e18fd74c0d 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -698,6 +698,9 @@ pub enum Profile { /// `iommu_platform=on`, so behind the unit on paper and bypassing it in /// fact: the function a claim must never be handed. HeadlessNicUntranslated, + /// [`Profile::Headless`] with an `e1000e`, QEMU's 82574L, where its virtio + /// NIC was: a NIC the image's rows name no machine for. + HeadlessE1000e, /// [`Profile::Headless`] with the kernel's virtio console created without /// `iommu_platform=on`: a device the kernel itself must tell physical /// addresses. @@ -805,6 +808,7 @@ impl Profile { | Self::HeadlessUsbSpare | Self::HeadlessNicBehindPort | Self::HeadlessNicUntranslated + | Self::HeadlessE1000e | Self::HeadlessConsoleUntranslated | Self::HeadlessTwoEdu | Self::Metal @@ -931,6 +935,9 @@ enum Nic { BehindPort, /// The virtio NIC without `iommu_platform=on`, whatever unit the machine has. Untranslated, + /// QEMU's `e1000e`, which the unit decodes as it does every function + /// that is not virtio. + E1000e, } /// Everything a profile decides about the machine, in one table. A new @@ -1134,6 +1141,7 @@ impl Profile { Self::HeadlessVirtioGpu => Shape { virtio_gpu: true, ..Self::Headless.shape() }, Self::HeadlessNicBehindPort => Shape { nic: Nic::BehindPort, ..Self::Headless.shape() }, Self::HeadlessNicUntranslated => Shape { nic: Nic::Untranslated, ..Self::Headless.shape() }, + Self::HeadlessE1000e => Shape { nic: Nic::E1000e, ..Self::Headless.shape() }, Self::HeadlessConsoleUntranslated => Shape { console_bypasses: true, ..Self::Headless.shape() }, Self::HeadlessTwoEdu => Shape { probes: Probes::Edus, ..Self::Headless.shape() }, Self::HeadlessUsbSpare => Shape { @@ -2502,6 +2510,10 @@ fn qemu_command( Nic::Untranslated => { qemu.arg("-netdev").arg(&netdev).arg("-device").arg("virtio-net-pci-non-transitional,netdev=net0"); } + // No option ROM: firmware has no use for the NIC. + Nic::E1000e => { + qemu.arg("-netdev").arg(&netdev).arg("-device").arg("e1000e,netdev=net0,romfile="); + } Nic::BehindPort => { qemu.arg("-device").arg("pcie-root-port,id=nicport,chassis=9"); qemu.arg("-netdev").arg(&netdev).arg("-device").arg(format!( diff --git a/tests/guest/rust-tests/Cargo.toml b/tests/guest/rust-tests/Cargo.toml index 51d8b9d0331..4b680d9b18a 100644 --- a/tests/guest/rust-tests/Cargo.toml +++ b/tests/guest/rust-tests/Cargo.toml @@ -23,7 +23,7 @@ acpiserver-api = { path = "../../../lib/acpiserver-api" } toyos-trace = { path = "../../../lib/trace" } # The I219's driver, the claim's register window and grant, and the kernel's # capability walk, for `i219_msi`'s judgement of its interrupts on the T14. -toyos-i219 = { path = "../../../system/netstack/i219" } +toyos-i219 = { path = "../../../system/i219/driver" } toyos-device-memory = { path = "../../../lib/device-memory" } toyos-pci-claim = { path = "../../../lib/pci-claim" } toyos-pci = { path = "../../../kernel/pci" } diff --git a/tests/guest/rust-tests/src/bin/lan_lease.rs b/tests/guest/rust-tests/src/bin/lan_lease.rs new file mode 100644 index 00000000000..a474ec2d426 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/lan_lease.rs @@ -0,0 +1,19 @@ +//! The wired link's lease: this job waits on the boot's served log for +//! netstack's lease line, which follows the NIC driver's link and a DHCP +//! exchange with the bench's router, so the boot's log holds both before the +//! list ends it. The line itself is the judge's to read; this says only that +//! it came. + +#[path = "../served_log.rs"] +mod served_log; + +use std::time::Duration; + +/// A liveness ceiling past netstack's own bound on its first lease, never a +/// timing. +const BOUND: Duration = Duration::from_secs(90); + +fn main() { + served_log::Log::open().until("netstack's lease", BOUND, |line| line.contains("netstack: DHCP: lease ")); + println!("lan_lease: netstack leased an address over its link"); +} diff --git a/tests/guest/rust-tests/src/bin/link_restart.rs b/tests/guest/rust-tests/src/bin/link_restart.rs new file mode 100644 index 00000000000..51992a5581b --- /dev/null +++ b/tests/guest/rust-tests/src/bin/link_restart.rs @@ -0,0 +1,108 @@ +//! A link that ends under a held stream and comes back: netstack keeps the +//! connections it carries across its link's end. +//! +//! This job claims the virtio NIC, which no row of its boot drives, starts +//! `/system/bin/virtio-net` on it and, once netstack has leased, has a line +//! answered on a stream to the harness. It then kills the driver, claims the +//! NIC again once the kernel has taken the claim back, starts the driver +//! again, and has a second line answered on the same stream once netstack +//! says the link ended and is open again; then the stream ends, each side's +//! FIN read by the other. + +#[path = "../served_log.rs"] +mod served_log; +#[path = "../spare.rs"] +mod spare; + +use std::io::{Read, Write}; +use std::net::{Ipv4Addr, Shutdown, SocketAddr, TcpStream}; +use std::os::toyos::process::CommandExt; +use std::process::{Child, Command}; +use std::time::Duration; + +use toyos::endow::{Endowments, DEV_PREFIX, SYSCAP_LABEL}; +use toyos::syscap::SysCap; +use toyos::PciDev; +use toyos_abi::syscall::PciId; + +const DRIVER: &str = "/system/bin/virtio-net"; +const NIC: PciId = PciId { vendor: 0x1af4, device: 0x1041 }; +/// The label the driver looks its claim up by: its row's spelling. +const LABEL: &str = "pci:02:00:00/1af4:1041"; + +/// QEMU's user network's address of its host. +const HOST: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 2); + +/// A liveness ceiling on each wait, never a timing. +const BOUND: Duration = Duration::from_secs(90); + +fn main() { + let port: u16 = std::env::args() + .nth(1) + .and_then(|port| port.parse().ok()) + .expect("link_restart: the harness's port is this job's argument"); + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("link_restart: test-runner endows a device-minting capability"); + let mut log = served_log::Log::open(); + + let mut driver = start(&cap); + log.until("netstack's lease", BOUND, |line| line.contains("netstack: ready, at most")); + let mut stream = TcpStream::connect(SocketAddr::from((HOST, port))).expect("link_restart: a connect through netstack to the harness"); + stream.set_read_timeout(Some(BOUND)).expect("link_restart: a read timeout"); + answered(&mut stream, "before"); + println!(" PASS: a line answered on the stream over the first link"); + + end(&mut driver); + let mut driver = start(&cap); + let mut ended = false; + log.until("the link's end and a link opened after it", BOUND, |line| { + ended |= line.contains("netstack: virtio-net's link ended: it hung up"); + ended && line.contains("netstack: virtio-net's link is open") + }); + println!(" PASS: netstack said the link ended, and opened the restarted driver's"); + + answered(&mut stream, "after"); + println!(" PASS: a line answered on the same stream over the second link"); + // The FIN both ways before the driver ends: one queued after the link is + // gone would never leave. + stream.shutdown(Shutdown::Write).expect("link_restart: the stream's FIN"); + let mut rest = Vec::new(); + stream.read_to_end(&mut rest).expect("link_restart: the harness's FIN"); + assert!(rest.is_empty(), "link_restart: the harness said {:?} after its last answer", String::from_utf8_lossy(&rest)); + end(&mut driver); + println!("link_restart: a stream held across its link's end and a new driver's link"); +} + +/// `virtio-net` on a claim of the NIC, which nothing holds. +fn start(cap: &SysCap) -> Child { + let pci = spare::of(cap, &[NIC]).expect("link_restart: the virtio NIC is held"); + let dev: PciDev = cap + .claim_function(pci.arrival) + .unwrap_or_else(|why| panic!("link_restart: the NIC's claim was refused: {why:?}")); + Command::new(DRIVER) + .endow(&format!("{DEV_PREFIX}{LABEL}"), dev.into()) + .spawn() + .expect("link_restart: spawn virtio-net") +} + +/// Kill the driver and wait for its end: once the wait returns, its claim is +/// back and can be minted again. +fn end(driver: &mut Child) { + driver.kill().expect("link_restart: kill the driver"); + driver.wait().expect("link_restart: the driver's end"); +} + +/// Write `word` as a line and read the harness's `heard ` line back. +fn answered(stream: &mut TcpStream, word: &str) { + stream.write_all(format!("{word}\n").as_bytes()).unwrap_or_else(|why| panic!("link_restart: the write of {word:?}: {why}")); + let owed = format!("heard {word}\n"); + let mut heard = Vec::new(); + let mut byte = [0u8; 1]; + while heard.last() != Some(&b'\n') { + match stream.read(&mut byte) { + Ok(1) => heard.push(byte[0]), + Ok(_) => panic!("link_restart: the harness hung up after {:?}", String::from_utf8_lossy(&heard)), + Err(why) => panic!("link_restart: the answer to {word:?}: {why} after {:?}", String::from_utf8_lossy(&heard)), + } + } + assert_eq!(String::from_utf8_lossy(&heard), owed, "link_restart: the harness's answer"); +} diff --git a/tests/guest/rust-tests/src/bin/netstack_answers.rs b/tests/guest/rust-tests/src/bin/netstack_answers.rs index 8796eb78a52..aad6f6498d3 100644 --- a/tests/guest/rust-tests/src/bin/netstack_answers.rs +++ b/tests/guest/rust-tests/src/bin/netstack_answers.rs @@ -1,7 +1,7 @@ -//! One request netstack answers: a UDP bind on any address. netstack opens its -//! card before it accepts a connection, so the answer follows the card's -//! bring-up and every line that bring-up logged. Exits 0 on the bind, and 1 -//! naming the refusal. +//! One request netstack answers: a UDP bind on any address. netstack serves +//! once a NIC driver's link is open, which the driver opens after its card's +//! bring-up, so the answer follows every line that bring-up logged. Exits 0 +//! on the bind, and 1 naming the refusal. use std::net::{Ipv4Addr, UdpSocket}; use std::process::ExitCode; diff --git a/tests/images/benchcase.toml b/tests/images/benchcase.toml index e172d1a076a..ca6fe4642d5 100644 --- a/tests/images/benchcase.toml +++ b/tests/images/benchcase.toml @@ -18,9 +18,11 @@ [boot] start = ["logkeeper", "diskserver", "fileserver", "acpiserver", "netstack", "test-runner"] +# `log`, which `lan_lease` waits for netstack's lease on. [programs.logkeeper] service = true syscap = ["logread"] +serves = ["log"] # ACPI mode, as a shipped image and the bench's Linux run it. In the legacy # mode the T14's firmware hands over, it stops every CPU for 4.5 ms every 2.2 s @@ -31,19 +33,23 @@ service = true devices = ["acpi"] receives = ["power"] -# netstack on the T14's wired I219, the only card a bench has that ToyOS drives: -# `osbench lan` reaches its peer and `osbench internet` the internet through it. -# On the AMD bench, which has none, netstack leaves and both say so. +# netstack over the T14's wired I219, the only card a bench has that ToyOS +# drives: `osbench lan` reaches its peer and `osbench internet` the internet +# through it. On the AMD bench, which has none, the supervisor starts no +# driver, netstack leaves, and both say so. [programs.netstack] service = true -serves = ["netstack"] -devices = ["pci:02:00:00/8086:15fc"] +serves = ["netstack", "link"] + +[programs.i219] +drives = ["pci:02:00:00/8086:15fc"] +receives = ["link"] # The order `osbench all` runs the groups in. The bound is the metal row's, its # 465 s on the 60 s every list has (`tests/toyos.rs`), restated for a boot of # this config built without the row; a derived metal config states its own. [programs.test-runner] -receives = ["power", "netstack"] +receives = ["power", "netstack", "log"] syscap = ["jobs", "counters", "trace", "roster", "inventory", "dup"] args = [ "--bound-ms=525000", diff --git a/tests/images/linkcase.toml b/tests/images/linkcase.toml new file mode 100644 index 00000000000..882a2db5e7f --- /dev/null +++ b/tests/images/linkcase.toml @@ -0,0 +1,45 @@ +# The boot `a_link_ends_under_a_held_stream_and_comes_back` runs alone: +# netstack and no NIC driver row, so the job claims the virtio NIC itself, +# starts `/system/bin/virtio-net` on it and ends it, and only it does. + +[boot] +start = ["logkeeper", "diskserver", "fileserver", "netstack", "test-runner"] + +# `log` is the port the job reads netstack's lines on. +[programs.logkeeper] +service = true +syscap = ["logread"] +serves = ["log"] + +[programs.netstack] +service = true +serves = ["netstack", "link"] + +# Declared so the image carries it, and started by no row: the job starts it. +[programs.virtio-net] + +# `inventory` and `device`, to find the NIC and claim it, and `dup`, because +# test-runner hands a job its capability as a duplicate; `link`, which the +# job's driver says hello on; `netstack`, which holds the job's stream; and +# `log`. +[programs.test-runner] +receives = ["netstack", "link", "log"] +syscap = ["device", "dup", "inventory"] + +[programs.diskserver] +service = true +restart = true +serves = ["block", "disk"] + +[programs.nvme] +drives = ["pci:01:08:02"] +receives = ["disk"] + +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30@boot"] +receives = ["disk"] + +[programs.fileserver] +restart = true +roles = ["data", "log", "boot"] +receives = ["block"] diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index 1b1dbe6e61c..0e7dbaa0941 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -45,12 +45,16 @@ drives = ["pci:04:03:*"] receives = ["sound-output"] syscap = ["rt"] -# netstack holds the NIC's PCI function and drives it: the virtqueues, the register -# window and the interrupt are its own, and the kernel keeps only the claim. +# netstack over the link a NIC driver brings; on a machine with no virtio NIC +# the supervisor starts no driver, and netstack leaves. [programs.netstack] service = true -serves = ["netstack"] -devices = ["pci:02:00:00/1af4:1041"] +serves = ["netstack", "link"] + +[programs.virtio-net] +drives = ["pci:02:00:00/1af4:1041"] +receives = ["link"] +restart = true [programs.sshserver] service = true diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 1d3ec03f932..19598e0ecec 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -2,8 +2,9 @@ # NVMe controller and soundserver's virtio-sound beside it: `iommu_virtio_platform`'s # machine, where every virtio function is a process's claim or a kernel driver's. # -# netstack's `main` opens the NIC before anything else and returns on `NotFound`, -# so a config with no NIC never reaches a line of the daemon proper. +# netstack serves once a NIC driver says hello, and leaves a machine none says +# hello on within 30 s. Every NIC driver is here, so whichever NIC the machine +# has comes up with no row naming it. # # test-runner is here for the same reason it is in metalcase: it makes the boot # announce itself. @@ -18,14 +19,22 @@ start = ["logkeeper", "diskserver", "fileserver", "netstack", "soundserver", "te service = true syscap = ["logread"] -# netstack holds the NIC's PCI function and drives it: the virtqueues, the register -# window and the interrupt are its own, and the kernel keeps only the claim. -# Named by class, vendor and device rather than by slot, so one row finds the card -# wherever firmware put it. +# netstack over the link a NIC driver brings on `link`; it claims no device. [programs.netstack] service = true -serves = ["netstack"] -devices = ["pci:02:00:00/1af4:1041"] +serves = ["netstack", "link"] + +# The NIC drivers: one process per function a row's parts match, holding that +# function alone; virtio's is started again when it ends other than by exit 0, +# the Intel parts' is not (`images/system.toml`). +[programs.virtio-net] +drives = ["pci:02:00:00/1af4:1041"] +receives = ["link"] +restart = true + +[programs.i219] +drives = ["pci:02:00:00/8086:15fc", "pci:02:00:00/8086:10d3"] +receives = ["link"] [programs.soundserver] service = true diff --git a/tests/toyos.rs b/tests/toyos.rs index 117b7aac09c..8486ffa1c49 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -142,6 +142,10 @@ const RUST_SKIP: &[&str] = &[ // It waits for netstack to answer, which follows the I219's bring-up: the // `i219_leaves_ulp_unarmed` metal row runs it on tests/images/benchcase.toml. "netstack_answers", + // It waits for a lease from the bench's router over the T14's I219: the + // `i219_carries_netstacks_link` metal row runs it on + // tests/images/benchcase.toml. + "lan_lease", // Its product is the T14's counters across an idle span and a spin on // every CPU, which the `counters` metal row judges; on a guest it would be // seconds of four CPUs spinning, read by nothing. @@ -257,6 +261,10 @@ const RUST_SKIP: &[&str] = &[ // A datagram receive bounded by its read timeout, through real netstack, // which a shared boot runs none of: `udp_receive_times_out` runs it. "udp_recv_timeout", + // It claims the NIC and starts its driver itself, which only + // tests/images/linkcase.toml leaves it to: + // `a_link_ends_under_a_held_stream_and_comes_back` runs it. + "link_restart", // It needs a launcher: `a_launch_gives_each_unsent_stdio_slot_a_null` // runs it on tests/images/launchcase.toml. "launch_unsent_stdio", @@ -634,8 +642,10 @@ const MACHINE_TESTS: &[&str] = &[ // processes and the real netstack, which no host test and // `stream_holders`' stand-in show. The harness is the peer, on the host's // loopback that QEMU's user network reaches and forwards one port of, so - // no public network. Both jobs in one boot. x86-64 alone - // (`qemu::SUITE_ARCH`). + // no public network. Both jobs in one boot, once on virtio and once on + // QEMU's `e1000e`, which the image's I219 row matches by its id alone: a + // NIC no row names a machine for, up with no configuration of its own. + // x86-64 alone (`qemu::SUITE_ARCH`). "netstack_holders_exported_to_children", // A datagram receive with a read timeout ends at it, a timeout under a // millisecond included, and a zero timeout is refused: std's conversion @@ -644,6 +654,15 @@ const MACHINE_TESTS: &[&str] = &[ // peer and no public network. Order and completion: QEMU's clock bounds // nothing but the hang. x86-64 alone (`qemu::SUITE_ARCH`). "udp_receive_times_out", + // A stream netstack holds across its link's end: the NIC's driver killed + // and started again on a fresh claim, and a line answered on the stream + // both before and after. netstack keeping its node across a link's end, + // the driver's hello on a new region and the kernel's claim given back + // and minted again cross three processes and a device, which no host test + // holds. QEMU x86-64 (`qemu::SUITE_ARCH`); the harness is the peer, on + // the host's loopback that QEMU's user network reaches, so no public + // network. Order and completion only. + "a_link_ends_under_a_held_stream_and_comes_back", // Login sessions: their owner parents, signs out and ends with each, and // the machine's reboot waits for them until forced, which only processes // in one boot show, as order, completion and counts. The reboot ends the @@ -1176,12 +1195,19 @@ const METAL: &[(&str, metal::Metal)] = &[ ( // What the PHY's ULP registers hold after the I219's disarm and the // full reset, which the stub only assumes. The T14 only: the AMD bench - // has no card ToyOS drives. It rides `osbench`'s boot, whose netstack - // drives the card, and reads two of netstack's lines once its job has - // had netstack answer, which it does only after the bring-up. + // has no card ToyOS drives. It rides `osbench`'s boot, whose i219 row + // drives the card, and reads two of the driver's lines once its job + // has had netstack answer, which it does only after the bring-up. "i219_leaves_ulp_unarmed", metal::Metal { arms: BENCHCASE_ALONE, judge: |b| i219_leaves_ulp_unarmed(b[0]) }, ), + ( + // The I219's driver a process of its own: its link open at netstack, + // up, and a lease from the bench's router carried over it, with no + // link ending. The T14 only, for the same reason, on the same boot. + "i219_carries_netstacks_link", + metal::Metal { arms: BENCHCASE_ALONE, judge: |b| i219_carries_netstacks_link(b[0]) }, + ), // ---- tests/images/jobcase.toml, each image armed to end its boot its own way ---- ( // Its own boot, and it must not share one: it is the only arm in this @@ -1755,20 +1781,20 @@ const OSBENCH_INTERNET: &[metal::Arm] = &[metal::Arm { ..metal::once("netbenchcase", "tests/images/benchcase.toml", &[], &["osbench_internet"]) }]; -/// [`BENCHCASE`]'s boot, with one job: netstack answering, which it does only -/// once the card's bring-up has logged. Without it the list's `reboot` lands -/// while the driver is still reaching the PHY. +/// [`BENCHCASE`]'s boot, with two jobs: netstack answering, which it does only +/// once the card's bring-up has logged, and its lease landing. Without them +/// the list's `reboot` lands while the driver is still reaching the PHY. const BENCHCASE_ALONE: &[metal::Arm] = - &[metal::once("benchcase", "tests/images/benchcase.toml", &[], &["test_rs_netstack_answers"])]; + &[metal::once("benchcase", "tests/images/benchcase.toml", &[], &["test_rs_netstack_answers", "test_rs_lan_lease"])]; -/// netstack's account of the I219's start: what its wake found of Ultra Low -/// Power and disarmed, and the configuration its bring-up read back unarmed. -/// Both lines are printed, because what was found is what the next boot's -/// diagnosis reads. +/// The I219 driver's account of its part's start: what its wake found of +/// Ultra Low Power and disarmed, and the configuration its bring-up read back +/// unarmed. Both lines are printed, because what was found is what the next +/// boot's diagnosis reads. fn i219_leaves_ulp_unarmed(boot: &metal::Readback) -> Result<(), String> { boot.job_passed("test_rs_netstack_answers")?; let log = boot.log(); - let woke = log.must_say("netstack: I219: before the reset the PHY was asked:")?; + let woke = log.must_say("i219: before the reset the PHY was asked:")?; let left = log.must_say("it is left with ULP configuration")?; eprintln!(" [i219] {woke}"); eprintln!(" [i219] {left}"); @@ -1781,6 +1807,19 @@ fn i219_leaves_ulp_unarmed(boot: &metal::Readback) -> Result<(), String> { Ok(()) } +/// The I219's link as netstack holds it: opened from the driver's hello, up, +/// and a lease carried over it, with no link ending in the boot. +fn i219_carries_netstacks_link(boot: &metal::Readback) -> Result<(), String> { + boot.job_passed("test_rs_lan_lease")?; + let log = boot.log(); + log.must_say("netstack: i219's link is open")?; + let up = log.must_say("i219: link up at ")?; + log.must_say("netstack: DHCP: lease ")?; + log.must_not_say("'s link ended")?; + eprintln!(" [i219] {up}"); + Ok(()) +} + /// Every group's job exited 0, which `osbench` does only once each of its /// benchmarks measured, and said it was done. fn osbench_ran(boot: &metal::Readback, jobs: &[&str]) -> Result<(), String> { @@ -4624,6 +4663,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "stream_holders" => stream_holders(test_config), "netstack_holders_exported_to_children" => netstack_holders_exported_to_children(), "udp_receive_times_out" => udp_receive_times_out(), + "a_link_ends_under_a_held_stream_and_comes_back" => a_link_ends_under_a_held_stream_and_comes_back(), "session_lifecycle" => common::sessionlifecycle::session_lifecycle(), "a_launch_gives_each_unsent_stdio_slot_a_null" => common::launchstdio::a_launch_gives_each_unsent_stdio_slot_a_null(), "a_row_reaches_its_own_state_and_no_other" => common::stateview::a_row_reaches_its_own_state_and_no_other(), @@ -5549,8 +5589,18 @@ const SOCKET_EXPORT_PORT: u16 = 7070; /// `tests/images/netcase.toml`, the harness every socket's peer: a stream's /// child's line, then its parent's, then the FIN once both holders are gone; /// then a listener's and a datagram socket's child's datagram, its accept and -/// its line, and the parent's after it. +/// its line, and the parent's after it. Once over virtio, and once over QEMU's +/// `e1000e`, which the image's rows name no machine for: the I219 driver's +/// row matches it, and its link is the one netstack opens. fn netstack_holders_exported_to_children() -> Result<(), String> { + for (profile, driver) in [(qemu::Profile::Headless, "virtio-net"), (qemu::Profile::HeadlessE1000e, "82574")] { + holders_exported_over(profile, driver)?; + eprintln!(" [link] {driver}: a stream, a listener and a datagram socket exported to children"); + } + Ok(()) +} + +fn holders_exported_over(profile: qemu::Profile, driver: &str) -> Result<(), String> { /// netstack's line once its lease landed: before it, a connect is refused. const LEASED: &str = "netstack: ready, at most"; let tests = compile::repo_root().join("tests/guest/rust-tests"); @@ -5564,11 +5614,19 @@ fn netstack_holders_exported_to_children() -> Result<(), String> { .map_err(|e| format!("no loopback port to forward: {e}"))? .port(); let config = compile::repo_root().join("tests/images/netcase.toml"); - let options = BootOptions { forward: Some((forwarded, SOCKET_EXPORT_PORT)), ..BootOptions::default() }; + let options = BootOptions { profile, forward: Some((forwarded, SOCKET_EXPORT_PORT)), ..BootOptions::default() }; let mut qemu = QemuInstance::boot_with_options(&config, &[], &bins, options); serial::Serial::boot(&qemu).must_be_clean()?; - if !qemu.boot_log().contains(LEASED) && !qemu.drain_until(Duration::from_secs(60), |l| l.contains(LEASED)).contains(LEASED) { - return Err("netstack never said its lease landed".into()); + let mut said = qemu.boot_log().to_string(); + if !said.contains(LEASED) { + said.push_str(&qemu.drain_until(Duration::from_secs(60), |l| l.contains(LEASED))); + } + if !said.contains(LEASED) { + return Err(format!("{driver}: netstack never said its lease landed\n{said}")); + } + let opened = format!("netstack: {driver}'s link is open"); + if !said.contains(&opened) { + return Err(format!("{driver}: netstack leased without saying {opened:?}\n{said}")); } stream_exported_to_a_child(&mut qemu)?; sockets_exported_to_a_child(&mut qemu, forwarded) @@ -5695,6 +5753,57 @@ fn peer_verdict(heard: std::thread::JoinHandle>) -> String } } +/// `test_rs_link_restart` on `tests/images/linkcase.toml`'s boot: the peer +/// answers each line the job writes on one connection, the one before the +/// job ends its driver and the one after it starts another, then reads the +/// job's FIN and answers it with its own. +fn a_link_ends_under_a_held_stream_and_comes_back() -> Result<(), String> { + use std::io::{BufRead, BufReader, Write}; + const JOB: &str = "link_restart"; + /// A liveness ceiling on the peer's waits, never a timing. + const CEILING: Duration = Duration::from_secs(240); + let tests = compile::repo_root().join("tests/guest/rust-tests"); + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, JOB); + let config = compile::repo_root().join("tests/images/linkcase.toml"); + let mut qemu = QemuInstance::boot_with_options(&config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + serial::Serial::boot(&qemu).must_be_clean()?; + let peer = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("no loopback listener: {e}"))?; + let port = peer.local_addr().map_err(|e| format!("the listener's port: {e}"))?.port(); + let heard = std::thread::spawn(move || -> Result<(), String> { + let (conn, _) = peer.accept().map_err(|e| format!("the guest's connect: {e}"))?; + conn.set_read_timeout(Some(CEILING)).map_err(|e| e.to_string())?; + let mut reader = BufReader::new(conn.try_clone().map_err(|e| e.to_string())?); + let mut answer = conn; + for word in ["before", "after"] { + let mut line = String::new(); + reader.read_line(&mut line).map_err(|e| format!("the job's {word:?} line: {e}"))?; + if line != format!("{word}\n") { + return Err(format!("the peer heard {line:?} where the job's {word:?} line was owed")); + } + answer.write_all(format!("heard {word}\n").as_bytes()).map_err(|e| format!("the answer to {word:?}: {e}"))?; + } + let mut rest = String::new(); + match reader.read_line(&mut rest) { + // The job's FIN, which this answers with its own as it drops. + Ok(0) => Ok(()), + Ok(_) => Err(format!("the peer heard {rest:?} after the job's last line")), + Err(e) => Err(format!("the stream's FIN: {e}")), + } + }); + let result = qemu.run_test(&format!("test_rs_{JOB} {port}"), CEILING); + if let Some(why) = &result.error { + return Err(format!("{why}\n{}\nthe job said:\n{}", peer_verdict(heard), result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + heard.join().map_err(|_| "the peer's thread panicked".to_string())??; + for line in result.stdout.lines().filter(|l| l.contains("PASS: ")) { + eprintln!(" [link] {}", line.trim()); + } + Ok(()) +} + /// `test_rs_udp_recv_timeout` on `tests/images/netcase.toml`'s boot. fn udp_receive_times_out() -> Result<(), String> { const JOB: &str = "udp_recv_timeout";