From 0ad00b608c47280d5894013a5b41fc353b78ec04 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 28 Oct 2026 19:23:07 +0100 Subject: [PATCH 1/6] netstack: every NIC driver is a process of its own, over a link (P4) netstack held every NIC driver and the stack in one process, behind `Card`, and each image's row named the card its machine has. A driver's fault ended every connection, no driver could start again without the stack's state, and a machine nobody wrote a row for had no network. `toyos-link` is the protocol between them, on `toyos::transport` alone: a driver connects to netstack's `link` port and sends a region it made with its hello. The region's header holds a ring of frame lengths each way and the words each side states and asks with: the driver's link, the multicast groups the stack joined, and the counters `inspect` asks for. Frame `n` is in slot `n` of its ring, so a slot is its producer's again when the consumer releases it, and each side copies a frame once. A doorbell is a byte on the connection; a full ring's producer sets a want that the releasing consumer takes, each side by one AcqRel swap of the want's word, so neither misses the other; `tests/ends_loom.rs` checks it and reds with the swaps Relaxed. Everything the peer writes is bounded before it is believed; the counters are displayed and never acted on. The I219/82574 driver (`system/i219`, with `toyos-i219` moved under it) and the virtio driver (`system/virtio-net`) are `drives` rows: one process per function a row's parts match by class, vendor and device, restarted by the supervisor when one ends other than by exit 0. Each runs `toyos-link`'s pass over its device and connects again when netstack hangs up. netstack claims no device. Its node is made with the first driver's address, so it waits for that driver's hello, and a machine none says hello on within 30 s has no network and netstack leaves. A link that ends leaves every connection to the node, and a hello with that address is opened again; any other is refused `OneLink` until P5 runs several links. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 41 ++- Cargo.toml | 5 +- build/build.rs | 1 + build/ci.rs | 3 + images/system.toml | 24 +- ...e-intel-driver-is-reported-as-no-change.md | 10 +- issues/every-driver-is-still-in-the-kernel.md | 2 +- ...-a-mac-where-it-gates-phy-configuration.md | 2 +- ...-the-t14s-i219-after-a-toyos-warm-reset.md | 2 +- issues/netstack-drives-one-card.md | 23 +- ...or-the-boot-on-a-nic-it-cannot-drive-on.md | 34 -- ...etstack-is-never-told-the-machine-stops.md | 2 +- ...eive-batch-over-64-frames-is-unmeasured.md | 7 +- ...-read-an-intel-nic-across-a-link-change.md | 2 +- ...p-to-a-valid-firmware-without-asking-it.md | 2 +- ...cs-room-and-wake-are-unread-on-hardware.md | 2 +- issues/the-lan-is-not-yet-production-grade.md | 26 +- ...-arbitration-is-unmeasured-on-this-part.md | 4 +- issues/the-tree-says-who-uses-each-thing.md | 6 +- issues/there-is-no-wifi.md | 8 + issues/toyos-has-its-own-network-stack.md | 2 +- lib/link/Cargo.toml | 30 ++ lib/link/src/driver.rs | 257 ++++++++++++++ lib/link/src/ends.rs | 310 +++++++++++++++++ lib/link/src/layout.rs | 107 ++++++ lib/link/src/lib.rs | 33 ++ lib/link/src/region.rs | 55 +++ lib/link/src/state.rs | 218 ++++++++++++ lib/link/src/wire.rs | 105 ++++++ lib/link/tests/ends.rs | 226 +++++++++++++ lib/link/tests/ends_loom.rs | 61 ++++ system/i219/Cargo.toml | 17 + .../{netstack/i219 => i219/driver}/Cargo.toml | 2 +- .../i219 => i219/driver}/src/delivery.rs | 0 .../{netstack/i219 => i219/driver}/src/lib.rs | 4 +- .../{netstack/i219 => i219/driver}/src/pch.rs | 0 .../{netstack/i219 => i219/driver}/src/phy.rs | 0 .../i219 => i219/driver}/src/power.rs | 0 .../i219 => i219/driver}/src/regs.rs | 0 .../i219 => i219/driver}/src/stub.rs | 0 .../i219 => i219/driver}/src/tests.rs | 4 +- .../{netstack/i219 => i219/driver}/src/ulp.rs | 0 .../i219 => i219/driver}/src/wake.rs | 0 system/i219/src/main.rs | 43 +++ .../{netstack/src/i219.rs => i219/src/nic.rs} | 243 ++++++------- system/netstack/Cargo.toml | 14 +- system/netstack/src/card.rs | 217 ------------ system/netstack/src/link.rs | 264 +++++++++++++++ system/netstack/src/main.rs | 320 ++++++++++++------ system/virtio-net/Cargo.toml | 20 ++ system/virtio-net/src/main.rs | 28 ++ .../virtio_net.rs => virtio-net/src/nic.rs} | 135 ++++---- tests/common/iommu.rs | 65 ++-- tests/common/qemu.rs | 12 + tests/guest/rust-tests/Cargo.toml | 2 +- tests/guest/rust-tests/src/bin/lan_lease.rs | 19 ++ .../guest/rust-tests/src/bin/link_restart.rs | 108 ++++++ .../rust-tests/src/bin/netstack_answers.rs | 8 +- tests/images/benchcase.toml | 19 +- tests/images/linkcase.toml | 45 +++ tests/images/metalcase.toml | 12 +- tests/images/netcase.toml | 26 +- tests/toyos.rs | 145 +++++++- 63 files changed, 2707 insertions(+), 675 deletions(-) delete mode 100644 issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md create mode 100644 lib/link/Cargo.toml create mode 100644 lib/link/src/driver.rs create mode 100644 lib/link/src/ends.rs create mode 100644 lib/link/src/layout.rs create mode 100644 lib/link/src/lib.rs create mode 100644 lib/link/src/region.rs create mode 100644 lib/link/src/state.rs create mode 100644 lib/link/src/wire.rs create mode 100644 lib/link/tests/ends.rs create mode 100644 lib/link/tests/ends_loom.rs create mode 100644 system/i219/Cargo.toml rename system/{netstack/i219 => i219/driver}/Cargo.toml (97%) rename system/{netstack/i219 => i219/driver}/src/delivery.rs (100%) rename system/{netstack/i219 => i219/driver}/src/lib.rs (99%) rename system/{netstack/i219 => i219/driver}/src/pch.rs (100%) rename system/{netstack/i219 => i219/driver}/src/phy.rs (100%) rename system/{netstack/i219 => i219/driver}/src/power.rs (100%) rename system/{netstack/i219 => i219/driver}/src/regs.rs (100%) rename system/{netstack/i219 => i219/driver}/src/stub.rs (100%) rename system/{netstack/i219 => i219/driver}/src/tests.rs (99%) rename system/{netstack/i219 => i219/driver}/src/ulp.rs (100%) rename system/{netstack/i219 => i219/driver}/src/wake.rs (100%) create mode 100644 system/i219/src/main.rs rename system/{netstack/src/i219.rs => i219/src/nic.rs} (62%) delete mode 100644 system/netstack/src/card.rs create mode 100644 system/netstack/src/link.rs create mode 100644 system/virtio-net/Cargo.toml create mode 100644 system/virtio-net/src/main.rs rename system/{netstack/src/virtio_net.rs => virtio-net/src/nic.rs} (83%) create mode 100644 tests/guest/rust-tests/src/bin/lan_lease.rs create mode 100644 tests/guest/rust-tests/src/bin/link_restart.rs create mode 100644 tests/images/linkcase.toml 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..2ec0bf3bf14 100644 --- a/images/system.toml +++ b/images/system.toml @@ -132,14 +132,26 @@ 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. The supervisor starts the NIC drivers before it and tells it how many. [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. Started again when one ends other than by +# exit 0, and netstack keeps every connection across it. +[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. +[programs.i219] +drives = ["pci:02:00:00/8086:15fc", "pci:02:00:00/8086:10d3"] +receives = ["link"] +restart = true # 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/netstack-drives-one-card.md b/issues/netstack-drives-one-card.md index 19821a1f369..455156d05a7 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 second +driver's hello, or one with another address after the first link ended, is +refused `OneLink` (`system/netstack/src/main.rs`, `hear_links` and `open`). +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 index 2e0e57ddcb7..db400655d63 100644 --- a/issues/netstack-is-never-told-the-machine-stops.md +++ b/issues/netstack-is-never-told-the-machine-stops.md @@ -16,7 +16,7 @@ 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 +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`, 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..9ee07c5f99b 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,7 +8,7 @@ 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 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..cf03d0f1d53 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 @@ -9,7 +9,7 @@ opened: 2026-10-08 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 +driver's own model (`system/i219/driver/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. 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..79d22215841 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -192,6 +192,14 @@ 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 second link by name. In a guest: + `a_link_ends_under_a_held_stream_and_comes_back`, and + `netstack_holders_exported_to_children` on virtio and on QEMU's `e1000e`. +- **Owed:** the T14 rows `i219_carries_netstacks_link` and + `i219_leaves_ulp_unarmed`, and S2. **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..43e78084935 --- /dev/null +++ b/lib/link/src/driver.rs @@ -0,0 +1,257 @@ +//! 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**: the port queues +//! the connection for the stack started in its place, and a fresh region +//! begins the new link. The device stays up meanwhile. A 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, 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, and on +/// a manifest that gives this driver no link: nothing drives on from either. +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(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 hello = Hello { mac: nic.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..3cbb5987c17 --- /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: the one size shared memory comes in. +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..a4a2a0de04d --- /dev/null +++ b/lib/link/src/wire.rs @@ -0,0 +1,105 @@ +//! 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; + +/// 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, and its name for the stack's lines. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Hello { + pub mac: [u8; 6], + 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); + out.extend_from_slice(self.label.as_bytes()); + out + } + + /// `None` for bytes [`Self::encode`] does not make: too short, a name + /// too long, or one that is not UTF-8. + pub fn decode(bytes: &[u8]) -> Option { + let (mac, label) = bytes.split_first_chunk::<6>()?; + if label.len() > MAX_LABEL { + return None; + } + Some(Self { mac: *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 another holds it, or held it with + /// another address (`issues/netstack-drives-one-card.md`). + OneLink, + /// 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, + } + .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), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_hello_reads_back_as_written() { + let hello = Hello { mac: [0x52, 0x54, 0, 0x12, 0x34, 0x56], label: "virtio-net".into() }; + assert_eq!(Hello::decode(&hello.encode()), Some(hello)); + let unnamed = Hello { mac: [2; 6], label: String::new() }; + assert_eq!(Hello::decode(&unnamed.encode()), Some(unnamed)); + } + + #[test] + fn a_hello_no_driver_writes_is_refused() { + assert_eq!(Hello::decode(&[1, 2, 3, 4, 5]), None, "short of an address"); + assert_eq!(Hello::decode(&[[0u8; 6].as_slice(), &[b'a'; MAX_LABEL + 1]].concat()), None, "a name too long"); + assert_eq!(Hello::decode(&[0, 0, 0, 0, 0, 0, 0xff]), None, "a name that is not UTF-8"); + } + + #[test] + fn every_refusal_reads_back_and_no_other_word_does() { + for why in [Refusal::OneLink, Refusal::Malformed] { + 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..d632d018572 --- /dev/null +++ b/lib/link/tests/ends_loom.rs @@ -0,0 +1,61 @@ +//! 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 loom::sync::atomic::AtomicU32; +use loom::sync::{Arc, Notify}; +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 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 page: Arc<[Shared; WORDS]> = Arc::new(core::array::from_fn(|_| Shared(AtomicU32::new(0)))); + let mut driver = Driver::new(&page); + let mut stack = Stack::new(&page); + while stack.room(&page).unwrap() > 0 { + stack.send(&page, 60).unwrap(); + } + stack.publish(&page); + let bell = Arc::new(Notify::new()); + let consumer = { + let (page, bell) = (Arc::clone(&page), Arc::clone(&bell)); + loom::thread::spawn(move || { + assert!(driver.to_send(&page).unwrap().is_some(), "a frame published"); + if driver.release(&page) { + bell.notify(); + } + }) + }; + while stack.want_room(&page).unwrap() == 0 { + 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..e8156f9689d --- /dev/null +++ b/system/netstack/src/link.rs @@ -0,0 +1,264 @@ +//! 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::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 connection is [`Pending::into_conn`]. + Hello(Hello, 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 region = self.rx.take_handles::<1>().and_then(|[region]| Region::lent(region).ok()); + match (hello, region) { + (Some(hello), Some(region)) => Heard::Hello(hello, 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, + /// 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, 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, said: Link::Down, frame: Box::new([0; SLOT_BYTES]), asked: false, ring: true }) + } + + pub fn hello(&self) -> &Hello { + &self.hello + } + + /// 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 { + 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; + 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..dc75c812060 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::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,128 @@ 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 `hello` asks for, unless the stack holds one or held one with +/// another address. +fn open(hello: Hello, conn: toyos::Connection, region: toyos_link::region::Region, held: Option<[u8; 6]>, groups: &[Group]) -> Option { + if held.is_some_and(|mac| mac != hello.mac) { + say!("netstack: refused {}'s link: netstack drives the one link it began with", hello.label); + link::refuse(&conn, Refusal::OneLink); + return None; + } + let label = hello.label.clone(); + let opened = Session::open(hello, conn, region, groups); + match &opened { + Some(_) => say!("netstack: {label}'s link is open"), + None => say!("netstack: {label} went before its link was opened"), + } + opened +} + +/// Hear each pending driver; a hello is opened, or refused while `link` holds +/// one. Answers a link opened. +fn hear_links(pending: &mut Vec, link: &Option, mac: Option<[u8; 6]>, groups: &[Group]) -> Option { + let mut opened = None; + let mut i = 0; + while i < pending.len() { + match pending[i].hear() { + Heard::Nothing => i += 1, + Heard::Gone => { + pending.remove(i); + } + Heard::Hello(hello, region) => { + let conn = pending.remove(i).into_conn(); + if link.is_some() || opened.is_some() { + say!("netstack: refused {}'s link: netstack drives one link (issues/netstack-drives-one-card.md)", hello.label); + link::refuse(&conn, Refusal::OneLink); + } else { + opened = open(hello, conn, region, mac, groups); + } + } + } + } + opened +} + +/// 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 { + if let Some(link) = hear_links(&mut pending, &None, None, groups) { + return Some(link); + } + let Some(left) = LINK_BOUND.checked_sub(began.elapsed()) else { return None }; + 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" ); +/// 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.hello().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: IndividualMac::new(MacAddr(mac)).unwrap_or_else(|| panic!("netstack: the link's address is a group's")), receive_buffer: TCP_BUFFER, send_buffer: toyos::net::stream::SEND_BYTES, secrets: secrets(), @@ -352,9 +426,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 +436,70 @@ 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(); // 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() { + 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); + } + } + } + if ended.is_none() + && let Some(session) = &mut link + && let Err(why) = session.ring() + { + ended = Some(why); + } + if let Some(why) = ended + && let Some(session) = link.take() + { + 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), + } + // The node hears a change, not a state. + if session.is_up() { + node.link(clock(), false, draw); } } - card.report(); sockets.settle(&mut node); // Before anything is served: the lease is what gives this machine an @@ -429,7 +513,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 +527,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 +536,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 +553,15 @@ fn main() { }); sockets.bridge(&mut node, clock()); + // A driver's connection: a link that ended is opened again on its + // driver's hello, with the address the node was made with. + if ready.contains(&TOKEN_LINKS) { + accept_link(&links, &mut pending_links); + } + pending_links.retain(|p| p.since.elapsed() < HANDSHAKE_TIMEOUT); + if let Some(opened) = hear_links(&mut pending_links, &link, Some(mac), &groups) { + link = Some(opened); + } // 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 +658,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..d5eac30d32e 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,24 @@ 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"] +restart = true # 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..80e4c556e18 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -2,8 +2,10 @@ # 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. +# The supervisor starts the NIC drivers before netstack and says how many, and +# netstack with none leaves, so a machine with no NIC never reaches a line of +# the daemon proper. 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 +20,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, and started again when one ends other than by exit 0. +[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"] +restart = true [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"; From 8d2a3b2ec6c27f5ae5e207c4cdb04ada9a9d0078 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 28 Oct 2026 22:39:59 +0100 Subject: [PATCH 2/6] issues: P4's T14 rows read the I219's link up and leased Both rows passed on one benchcase boot at 8bdf49156, the orchestrator's, judged from the readback; what P4 still owes is S2. Co-Authored-By: Claude Opus 5.5 --- issues/there-is-no-wifi.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 79d22215841..3833a3974ba 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -198,8 +198,10 @@ stage 3 of `issues/the-lan-is-not-yet-production-grade.md`, whole. refuses a second link by name. In a guest: `a_link_ends_under_a_held_stream_and_comes_back`, and `netstack_holders_exported_to_children` on virtio and on QEMU's `e1000e`. -- **Owed:** the T14 rows `i219_carries_netstacks_link` and - `i219_leaves_ulp_unarmed`, and S2. +- **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). +- **Owed:** S2. **P5, netstack over several links.** Owner: the orchestrator; its defect is `issues/netstack-drives-one-card.md`. Depends on: P4. From e66d017047128b53f639e264281fe9a8ae9a67a5 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 28 Oct 2026 23:08:50 +0100 Subject: [PATCH 3/6] review r1 of #1106: a hello's address is an individual one, a faulted link ends its driver, and the I219 does not restart - A hello carries an `IndividualMac`, and a group's address, or a name that is not ASCII letters, digits and `-`, is refused `Malformed` at decode, so no driver's hello reaches a panic in netstack. - A link whose driver wrote what no driver writes ends, and that address's next hello is refused `Faulted`; the driver ends on it, so the supervisor's restart budget bounds a driver that keeps doing it. - netstack refuses `OneLink` only a hello with another address, or with its own while that link is alive, read on the connection then: the kernel closes a dead driver's handles before its end is published, so a restarted driver's hello finds the old link hung up. - The I219 rows take no `restart`: its start resets the part, which over a ring a link change left it is not known to be safe. - Counters are read only for an ask the link was given; a pending driver connection in the first wait is let go after the handshake bound. - Comments that described the rejected `--drivers` and a restarting stack are corrected, and the stop and room records name the driver that now owns their subject; the stop record's slug says so. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + images/system.toml | 11 +- issues/netstack-drives-one-card.md | 6 +- ...-read-an-intel-nic-across-a-link-change.md | 37 +++--- ...driver-is-never-told-the-machine-stops.md} | 15 ++- ...cs-room-and-wake-are-unread-on-hardware.md | 19 +-- issues/there-is-no-wifi.md | 12 +- lib/link/Cargo.toml | 2 + lib/link/src/driver.rs | 23 +++- lib/link/src/layout.rs | 2 +- lib/link/src/wire.rs | 55 +++++--- system/netstack/src/link.rs | 10 +- system/netstack/src/main.rs | 123 ++++++++++++------ tests/images/benchcase.toml | 1 - tests/images/netcase.toml | 11 +- 15 files changed, 212 insertions(+), 116 deletions(-) rename issues/{netstack-is-never-told-the-machine-stops.md => the-i219-driver-is-never-told-the-machine-stops.md} (55%) diff --git a/Cargo.lock b/Cargo.lock index 64cca460091..f7a9a19333b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3807,6 +3807,7 @@ dependencies = [ "loom", "toyos", "toyos-abi", + "toyos-net-wire", ] [[package]] diff --git a/images/system.toml b/images/system.toml index 2ec0bf3bf14..9e2ef83dc4b 100644 --- a/images/system.toml +++ b/images/system.toml @@ -133,25 +133,28 @@ receives = ["sound-output"] syscap = ["rt"] # The network stack, over the link a NIC driver brings on `link`; it claims no -# device. The supervisor starts the NIC drivers before it and tells it how many. +# 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", "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. Started again when one ends other than by -# exit 0, and netstack keeps every connection across it. +# 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"] -restart = true # 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/netstack-drives-one-card.md b/issues/netstack-drives-one-card.md index 455156d05a7..7843af9cb34 100644 --- a/issues/netstack-drives-one-card.md +++ b/issues/netstack-drives-one-card.md @@ -6,9 +6,9 @@ opened: 2026-10-12 # netstack drives one card -netstack opens the first link a NIC driver brings, and no other: a second -driver's hello, or one with another address after the first link ended, is -refused `OneLink` (`system/netstack/src/main.rs`, `hear_links` and `open`). +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. 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 9ee07c5f99b..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 @@ -12,8 +12,8 @@ them the part's, on the 82574 and on the I219 alike 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/netstack-is-never-told-the-machine-stops.md b/issues/the-i219-driver-is-never-told-the-machine-stops.md similarity index 55% rename from issues/netstack-is-never-told-the-machine-stops.md rename to issues/the-i219-driver-is-never-told-the-machine-stops.md index db400655d63..c6f20b12e01 100644 --- a/issues/netstack-is-never-told-the-machine-stops.md +++ b/issues/the-i219-driver-is-never-told-the-machine-stops.md @@ -4,16 +4,17 @@ kind: defect opened: 2026-10-28 --- -# netstack is never told the machine stops +# 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`). 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. +`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 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 cf03d0f1d53..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/i219/driver/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/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 3833a3974ba..70f5cd95c30 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -195,8 +195,16 @@ stage 3 of `issues/the-lan-is-not-yet-production-grade.md`, whole. - **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 second link by name. In a guest: - `a_link_ends_under_a_held_stream_and_comes_back`, 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 diff --git a/lib/link/Cargo.toml b/lib/link/Cargo.toml index 315dfb818b1..e6bdd858836 100644 --- a/lib/link/Cargo.toml +++ b/lib/link/Cargo.toml @@ -21,6 +21,8 @@ mutate-want-relaxed = [] toyos-abi = { path = "../../sdk/abi" } # The rings on the region's header page: `toyos::transport`. toyos = { path = "../../sdk/toyos" } +# The hello's address, an individual one, as the stack makes its node with. +toyos-net-wire = { path = "../../system/netstack/wire" } [lints.rust] warnings = "deny" diff --git a/lib/link/src/driver.rs b/lib/link/src/driver.rs index 43e78084935..5cce1cacdb9 100644 --- a/lib/link/src/driver.rs +++ b/lib/link/src/driver.rs @@ -8,10 +8,13 @@ //! 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**: the port queues -//! the connection for the stack started in its place, and a fresh region -//! begins the new link. The device stays up meanwhile. A refusal, or a port -//! closed for good, ends [`drive`]: nothing this driver does changes either. +//! **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}; @@ -19,6 +22,7 @@ use toyos::poller::{Poller, READABLE}; use toyos::shm::ReadBytes; use toyos::{say, PciDev}; use toyos_abi::syscall::SyscallError; +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; use crate::ends::{Driver, Fault}; use crate::layout::{RX_DEPTH, SLOT_BYTES, TX_DEPTH}; @@ -82,8 +86,10 @@ const TOKEN_LINK: u64 = 1; /// stack refuses it or its port closes for good. /// /// # Panics -/// On anything the stack writes that no stack of the protocol writes, and on -/// a manifest that gives this driver no link: nothing drives on from either. +/// 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) { @@ -96,6 +102,7 @@ pub fn drive(label: &str, nic: &mut impl Nic) { }; 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; @@ -128,7 +135,9 @@ impl<'a> Session<'a> { 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 hello = Hello { mac: nic.mac(), label: label.into() }; + let mac = IndividualMac::new(MacAddr(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. diff --git a/lib/link/src/layout.rs b/lib/link/src/layout.rs index 3cbb5987c17..d8b1aab19dc 100644 --- a/lib/link/src/layout.rs +++ b/lib/link/src/layout.rs @@ -5,7 +5,7 @@ use toyos::transport::{Geometry, Place, Run}; -/// A link's whole region: the one size shared memory comes in. +/// 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. diff --git a/lib/link/src/wire.rs b/lib/link/src/wire.rs index a4a2a0de04d..dd1d9ad5e52 100644 --- a/lib/link/src/wire.rs +++ b/lib/link/src/wire.rs @@ -9,6 +9,8 @@ use alloc::string::String; use alloc::vec::Vec; +use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; + /// Driver to stack: a [`Hello`]. Handles: the region. pub const MSG_HELLO: u32 = 1; /// Stack to driver: the link is open. No payload. @@ -19,10 +21,11 @@ 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, and its name for the stack's lines. +/// 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: [u8; 6], + pub mac: IndividualMac, pub label: String, } @@ -31,28 +34,34 @@ impl Hello { pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(6 + self.label.len()); - out.extend_from_slice(&self.mac); + out.extend_from_slice(&self.mac.get().0); out.extend_from_slice(self.label.as_bytes()); out } - /// `None` for bytes [`Self::encode`] does not make: too short, a name - /// too long, or one that is not UTF-8. + /// `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>()?; - if label.len() > MAX_LABEL { + let mac = IndividualMac::new(MacAddr(*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: *mac, label: String::from(core::str::from_utf8(label).ok()?) }) + 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 another holds it, or held it with - /// another address (`issues/netstack-drives-one-card.md`). + /// 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, } @@ -62,6 +71,7 @@ impl Refusal { match self { Self::OneLink => 1u32, Self::Malformed => 2, + Self::Faulted => 3, } .to_le_bytes() } @@ -70,6 +80,7 @@ impl Refusal { match u32::from_le_bytes(bytes.try_into().ok()?) { 1 => Some(Self::OneLink), 2 => Some(Self::Malformed), + 3 => Some(Self::Faulted), _ => None, } } @@ -79,24 +90,36 @@ impl Refusal { mod tests { use super::*; + fn mac(bytes: [u8; 6]) -> IndividualMac { + IndividualMac::new(MacAddr(bytes)).unwrap() + } + #[test] fn a_hello_reads_back_as_written() { - let hello = Hello { mac: [0x52, 0x54, 0, 0x12, 0x34, 0x56], label: "virtio-net".into() }; + let hello = Hello { mac: mac([0x52, 0x54, 0, 0x12, 0x34, 0x56]), label: "virtio-net".into() }; assert_eq!(Hello::decode(&hello.encode()), Some(hello)); - let unnamed = Hello { mac: [2; 6], label: String::new() }; - assert_eq!(Hello::decode(&unnamed.encode()), Some(unnamed)); + 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() { - assert_eq!(Hello::decode(&[1, 2, 3, 4, 5]), None, "short of an address"); - assert_eq!(Hello::decode(&[[0u8; 6].as_slice(), &[b'a'; MAX_LABEL + 1]].concat()), None, "a name too long"); - assert_eq!(Hello::decode(&[0, 0, 0, 0, 0, 0, 0xff]), None, "a name that is not UTF-8"); + 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] { + 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); diff --git a/system/netstack/src/link.rs b/system/netstack/src/link.rs index e8156f9689d..65dde197e8d 100644 --- a/system/netstack/src/link.rs +++ b/system/netstack/src/link.rs @@ -117,6 +117,11 @@ impl Session { &self.hello } + /// 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() @@ -208,6 +213,9 @@ impl Session { /// 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) @@ -245,7 +253,7 @@ impl Session { /// 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; + let m = self.hello.mac.get().0; 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 { diff --git a/system/netstack/src/main.rs b/system/netstack/src/main.rs index dc75c812060..93cf42d9047 100644 --- a/system/netstack/src/main.rs +++ b/system/netstack/src/main.rs @@ -48,7 +48,6 @@ 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::Instant; mod client; @@ -308,16 +307,10 @@ fn answer_inspect(request: &Request, link: Option<(&Session, Option<&Counters>)> request.client.snapshot(&encoded); } -/// Open the link `hello` asks for, unless the stack holds one or held one with -/// another address. -fn open(hello: Hello, conn: toyos::Connection, region: toyos_link::region::Region, held: Option<[u8; 6]>, groups: &[Group]) -> Option { - if held.is_some_and(|mac| mac != hello.mac) { - say!("netstack: refused {}'s link: netstack drives the one link it began with", hello.label); - link::refuse(&conn, Refusal::OneLink); - return None; - } - let label = hello.label.clone(); - let opened = Session::open(hello, conn, region, groups); +/// 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.conn, asked.region, groups); match &opened { Some(_) => say!("netstack: {label}'s link is open"), None => say!("netstack: {label} went before its link was opened"), @@ -325,10 +318,18 @@ fn open(hello: Hello, conn: toyos::Connection, region: toyos_link::region::Regio opened } -/// Hear each pending driver; a hello is opened, or refused while `link` holds -/// one. Answers a link opened. -fn hear_links(pending: &mut Vec, link: &Option, mac: Option<[u8; 6]>, groups: &[Group]) -> Option { - let mut opened = None; +/// 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() { @@ -338,16 +339,26 @@ fn hear_links(pending: &mut Vec, link: &Option, mac: Option<[u } Heard::Hello(hello, region) => { let conn = pending.remove(i).into_conn(); - if link.is_some() || opened.is_some() { - say!("netstack: refused {}'s link: netstack drives one link (issues/netstack-drives-one-card.md)", hello.label); - link::refuse(&conn, Refusal::OneLink); - } else { - opened = open(hello, conn, region, mac, groups); - } + asked.push(Asked { hello, conn, region }); } } } - opened + 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. @@ -365,8 +376,15 @@ fn first_link(links: &Acceptor, groups: &[Group]) -> Option { let mut pending: Vec = Vec::new(); let mut poller = watches::Watches::new(1 + MAX_PENDING_LINKS); loop { - if let Some(link) = hear_links(&mut pending, &None, None, groups) { - return Some(link); + 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 Some(left) = LINK_BOUND.checked_sub(began.elapsed()) else { return None }; poller.watch(links, READABLE, TOKEN_LINKS); @@ -391,6 +409,13 @@ const _: () = assert!( "a snapshot is one frame" ); +/// A driver's hello, with what it came with. +struct Asked { + hello: Hello, + conn: toyos::Connection, + region: toyos_link::region::Region, +} + /// An `inspect` waiting for the driver's counters. struct Inspecting { request: Request, @@ -417,7 +442,7 @@ fn main() { 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 link's address is a group's")), + mac, receive_buffer: TCP_BUFFER, send_buffer: toyos::net::stream::SEND_BYTES, secrets: secrets(), @@ -447,6 +472,9 @@ fn main() { 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; @@ -474,6 +502,10 @@ fn main() { // 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() { @@ -488,17 +520,8 @@ fn main() { { ended = Some(why); } - if let Some(why) = ended - && let Some(session) = link.take() - { - 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), - } - // The node hears a change, not a state. - if session.is_up() { - node.link(clock(), false, draw); - } + if let Some(why) = ended { + end_link(&mut link, why, &mut node, clock(), &mut faulted); } sockets.settle(&mut node); @@ -553,14 +576,32 @@ fn main() { }); sockets.bridge(&mut node, clock()); - // A driver's connection: a link that ended is opened again on its - // driver's hello, with the address the node was made with. + // 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); } - pending_links.retain(|p| p.since.elapsed() < HANDSHAKE_TIMEOUT); - if let Some(opened) = hear_links(&mut pending_links, &link, Some(mac), &groups) { - link = Some(opened); + for asked in hear_links(&mut pending_links) { + if asked.hello.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. diff --git a/tests/images/benchcase.toml b/tests/images/benchcase.toml index d5eac30d32e..ca6fe4642d5 100644 --- a/tests/images/benchcase.toml +++ b/tests/images/benchcase.toml @@ -44,7 +44,6 @@ serves = ["netstack", "link"] [programs.i219] drives = ["pci:02:00:00/8086:15fc"] receives = ["link"] -restart = true # 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 diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 80e4c556e18..19598e0ecec 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -2,10 +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. # -# The supervisor starts the NIC drivers before netstack and says how many, and -# netstack with none leaves, so a machine with no NIC never reaches a line of -# the daemon proper. Every NIC driver is here, so whichever NIC the machine has -# comes up with no row naming it. +# 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. @@ -26,7 +25,8 @@ service = true serves = ["netstack", "link"] # The NIC drivers: one process per function a row's parts match, holding that -# function alone, and started again when one ends other than by exit 0. +# 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"] @@ -35,7 +35,6 @@ restart = true [programs.i219] drives = ["pci:02:00:00/8086:15fc", "pci:02:00:00/8086:10d3"] receives = ["link"] -restart = true [programs.soundserver] service = true From cc1e2ecfce7d16e866ea5361b0c1ff3aaf86ef55 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 28 Oct 2026 23:23:32 +0100 Subject: [PATCH 4/6] issues: S2's reading of the link hop on the T14 Two processes over one link region moved 1,514-byte frames on the T14 at about 3,300 TSC cycles of one CPU a frame with a doorbell per frame, 920 per batch of 8 and 620 per batch of 32, both directions alike; full rate needs 200,163 frames a second each way. Reported, deciding nothing by itself. Co-Authored-By: Claude Opus 5.5 --- issues/there-is-no-wifi.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index 70f5cd95c30..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 @@ -209,7 +209,12 @@ stage 3 of `issues/the-lan-is-not-yet-production-grade.md`, whole. - **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). -- **Owed:** S2. +- **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. From 11b50e10d6957f6a2bdb751e097d77893188a21e Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 28 Oct 2026 23:43:44 +0100 Subject: [PATCH 5/6] review r2 of #1106: the link's address is its own type, and netstack converts it `toyos-link` depended on netstack's wire crate for one six-byte type, which put Ethernet, IPv4 and TCP wire formats and toyos-crypto into both drivers. The link keeps its own checked `Address` instead, an individual one refused otherwise at decode; a driver checks its device's address against it, and netstack builds its `IndividualMac` from it once, as it hears the hello, refusing `Malformed` should that ever fail. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - lib/link/Cargo.toml | 2 -- lib/link/src/driver.rs | 5 ++--- lib/link/src/wire.rs | 26 ++++++++++++++++++++------ system/netstack/src/link.rs | 23 ++++++++++++++++------- system/netstack/src/main.rs | 14 ++++++++------ 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7a9a19333b..64cca460091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3807,7 +3807,6 @@ dependencies = [ "loom", "toyos", "toyos-abi", - "toyos-net-wire", ] [[package]] diff --git a/lib/link/Cargo.toml b/lib/link/Cargo.toml index e6bdd858836..315dfb818b1 100644 --- a/lib/link/Cargo.toml +++ b/lib/link/Cargo.toml @@ -21,8 +21,6 @@ mutate-want-relaxed = [] toyos-abi = { path = "../../sdk/abi" } # The rings on the region's header page: `toyos::transport`. toyos = { path = "../../sdk/toyos" } -# The hello's address, an individual one, as the stack makes its node with. -toyos-net-wire = { path = "../../system/netstack/wire" } [lints.rust] warnings = "deny" diff --git a/lib/link/src/driver.rs b/lib/link/src/driver.rs index 5cce1cacdb9..4216b88ff89 100644 --- a/lib/link/src/driver.rs +++ b/lib/link/src/driver.rs @@ -22,13 +22,12 @@ use toyos::poller::{Poller, READABLE}; use toyos::shm::ReadBytes; use toyos::{say, PciDev}; use toyos_abi::syscall::SyscallError; -use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; 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, Hello, Refusal}; +use crate::wire::{self, Address, Hello, Refusal}; use crate::PORT; /// The device under a link, as its driver drives it. @@ -135,7 +134,7 @@ impl<'a> Session<'a> { 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 = IndividualMac::new(MacAddr(nic.mac())) + 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:?}")); diff --git a/lib/link/src/wire.rs b/lib/link/src/wire.rs index dd1d9ad5e52..7673a8a5591 100644 --- a/lib/link/src/wire.rs +++ b/lib/link/src/wire.rs @@ -9,7 +9,21 @@ use alloc::string::String; use alloc::vec::Vec; -use toyos_net_wire::ethernet::{IndividualMac, MacAddr}; +/// 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; @@ -25,7 +39,7 @@ pub const MAX_LABEL: usize = 32; /// name for the stack's lines: ASCII letters, digits and `-`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Hello { - pub mac: IndividualMac, + pub mac: Address, pub label: String, } @@ -34,7 +48,7 @@ impl Hello { pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(6 + self.label.len()); - out.extend_from_slice(&self.mac.get().0); + out.extend_from_slice(&self.mac.bytes()); out.extend_from_slice(self.label.as_bytes()); out } @@ -43,7 +57,7 @@ impl Hello { /// 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 = IndividualMac::new(MacAddr(*mac))?; + 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; @@ -90,8 +104,8 @@ impl Refusal { mod tests { use super::*; - fn mac(bytes: [u8; 6]) -> IndividualMac { - IndividualMac::new(MacAddr(bytes)).unwrap() + fn mac(bytes: [u8; 6]) -> Address { + Address::new(bytes).unwrap() } #[test] diff --git a/system/netstack/src/link.rs b/system/netstack/src/link.rs index 65dde197e8d..0ab6bd16d40 100644 --- a/system/netstack/src/link.rs +++ b/system/netstack/src/link.rs @@ -17,6 +17,7 @@ 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; @@ -35,8 +36,9 @@ 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 connection is [`Pending::into_conn`]. - Hello(Hello, Region), + /// 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 { @@ -58,9 +60,10 @@ impl Pending { 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, region) { - (Some(hello), Some(region)) => Heard::Hello(hello, region), + 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); @@ -93,6 +96,7 @@ pub struct Session { region: Region, end: Stack, hello: Hello, + mac: IndividualMac, /// The link as the node was last told it. said: Link, frame: Box<[u8; SLOT_BYTES]>, @@ -104,19 +108,24 @@ pub struct Session { 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, conn: Connection, region: Region, groups: &[Group]) -> Option { + 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, said: Link::Down, frame: Box::new([0; SLOT_BYTES]), asked: false, ring: true }) + 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() @@ -253,7 +262,7 @@ impl Session { /// 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.get().0; + 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 { diff --git a/system/netstack/src/main.rs b/system/netstack/src/main.rs index 93cf42d9047..f334489fada 100644 --- a/system/netstack/src/main.rs +++ b/system/netstack/src/main.rs @@ -48,6 +48,7 @@ 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; use toyos_net_wire::Instant; mod client; @@ -310,7 +311,7 @@ fn answer_inspect(request: &Request, link: Option<(&Session, Option<&Counters>)> /// 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.conn, asked.region, groups); + 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"), @@ -337,9 +338,9 @@ fn hear_links(pending: &mut Vec) -> Vec { Heard::Gone => { pending.remove(i); } - Heard::Hello(hello, region) => { + Heard::Hello(hello, mac, region) => { let conn = pending.remove(i).into_conn(); - asked.push(Asked { hello, conn, region }); + asked.push(Asked { hello, mac, conn, region }); } } } @@ -386,7 +387,7 @@ fn first_link(links: &Acceptor, groups: &[Group]) -> Option { if opened.is_some() { return opened; } - let Some(left) = LINK_BOUND.checked_sub(began.elapsed()) else { return None }; + 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); @@ -412,6 +413,7 @@ const _: () = assert!( /// A driver's hello, with what it came with. struct Asked { hello: Hello, + mac: IndividualMac, conn: toyos::Connection, region: toyos_link::region::Region, } @@ -436,7 +438,7 @@ fn main() { say!("netstack: no NIC driver brought a link in {} s; this machine has no network, exiting", LINK_BOUND.as_secs()); return; }; - let mac = first.hello().mac; + let mac = first.mac(); let mut link = Some(first); let began = Wall::now(); @@ -584,7 +586,7 @@ fn main() { accept_link(&links, &mut pending_links); } for asked in hear_links(&mut pending_links) { - if asked.hello.mac != mac { + if asked.mac != mac { refuse(&asked, Refusal::OneLink, "its address is not the one netstack drives (issues/netstack-drives-one-card.md)"); continue; } From 68079356bd7c444a08e481e146379f7c78ace4f9 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 31 Oct 2026 17:02:15 +0100 Subject: [PATCH 6/6] link: the ring-want model shares its page by Rc, and builds it on the heap The merge queue's `mutate-want-relaxed` control found no FAILED line: the deadlock it should report aborted the test binary first. Two causes, each enough on its own. The model held its page and doorbell in loom's `Arc`, whose drop panics while a deadlocked model unwinds (issues/loom-arc-panics-in-its-drop-while-a-failed-model-unwinds.md); and its 768-word page, built inline, left too little of loom's 32 KiB coroutine stack to unwind, which on Linux overflowed and aborted before the harness printed its verdict. The model now shares one `Rc`, as transport_loom does, and collects the page on the heap. With `Rc` and an inline page the coroutine still overflows (printed on macOS, fatal on the CI runner). Co-Authored-By: Claude Opus 5.5 --- ...n-its-drop-while-a-failed-model-unwinds.md | 3 +- lib/link/tests/ends_loom.rs | 46 +++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) 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/lib/link/tests/ends_loom.rs b/lib/link/tests/ends_loom.rs index d632d018572..6471a28feb7 100644 --- a/lib/link/tests/ends_loom.rs +++ b/lib/link/tests/ends_loom.rs @@ -5,9 +5,10 @@ //! 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 loom::sync::{Arc, Notify}; use toyos::transport::Word; use toyos_link::ends::{Driver, Stack}; use toyos_link::layout::WORDS; @@ -31,30 +32,47 @@ impl Word for Shared { } } +/// 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 page: Arc<[Shared; WORDS]> = Arc::new(core::array::from_fn(|_| Shared(AtomicU32::new(0)))); - let mut driver = Driver::new(&page); - let mut stack = Stack::new(&page); - while stack.room(&page).unwrap() > 0 { - stack.send(&page, 60).unwrap(); + 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(&page); - let bell = Arc::new(Notify::new()); + stack.publish(&link.page); let consumer = { - let (page, bell) = (Arc::clone(&page), Arc::clone(&bell)); + let link = Rc::clone(&link); loom::thread::spawn(move || { - assert!(driver.to_send(&page).unwrap().is_some(), "a frame published"); - if driver.release(&page) { - bell.notify(); + assert!(driver.to_send(&link.page).unwrap().is_some(), "a frame published"); + if driver.release(&link.page) { + link.bell.notify(); } }) }; - while stack.want_room(&page).unwrap() == 0 { - bell.wait(); + while stack.want_room(&link.page).unwrap() == 0 { + link.bell.wait(); } consumer.join().unwrap(); });