Skip to content

toyos-virtio: the virtio 1.2 PCI transport and split virtqueue as one pure crate, with netstack's virtio-net its first client; toyos-device-memory: the one boundary toyos-i219 and toyos-virtio are written against - #796

Merged
Japabu merged 6 commits into
mainfrom
wt/toyos-arm-u1
Oct 9, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

toyos-virtio is the virtio 1.2 PCI transport and split virtqueue as one pure, no_std, forbid(unsafe_code) crate, and netstack's virtio-net driver is its first client. toyos-device-memory is the shared device boundary, and two crates are written against it, toyos-i219 and toyos-virtio; diskserver's NVMe driver and soundserver's virtio still carry their own barriers and issues/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md owns moving them.

The tree's stages this serves: issues/every-driver-is-still-in-the-kernel.md (the owner's target, virtio nowhere in the kernel; its stage 1 names the next clients) and stages 6 and 7 of issues/toyos-runs-on-arm64.md.

Head 90e9f6451ec73e037393202cd0c07448cecd3137, with origin/main at 55e4e1dd2 (#795) merged in: the merge brought two issues/ files, issues/toyos-runs-on-arm64.md and src/build.rs, none of them this branch's, and no conflict. #793 had not landed when this was measured.

What changed, per decision

One shared device boundary: the owner's ruling, 2026-10-09. Asked "Userland drivers reach their device through a small interface (read/write registers, DMA buffers, two memory barriers). The Intel NIC crate has its own copy and the new virtio crate added a second, identical one. An issue in the tree reserves this call for you: one shared interface, or one per driver crate?", he answered: "One shared interface (Recommended)".

  • toyos-device-memory declares Registers, DmaBuffers and the order of the two barriers, stated once. It is named for what it declares, the two kinds of memory a driver and its device share. Not toyos-driver, which would invite everything a driver needs; not toyos-dma, which is the kernel's and is half of it. It has no dependency and no test: it is two traits. The contract states two orders and says of the third, a store followed by a load of what the device wrote in answer (a suppression word, VIRTIO_F_EVENT_IDX), that neither barrier gives it and the first driver that needs it adds a method. Neither driver here makes that pair.
  • The width of an access is in the method's name, because the width is a decision a register file makes. Each method is one a driver in the tree calls: 8, 16 and 32 bits of a register (virtio), 16, 32 and 64 bits of the grant (virtio's rings, the Intel descriptors).
  • toyos-i219 loses its own two declarations. Its change is its accesses renamed to the width they always had: read32/write32 for a register, read64/write64 for a descriptor half. Clock and Interrupts stay its own: one driver needs them. Its 88 tests are 90. What changed in stub.rs and tests.rs is the trait implementations, which panic on an access of any other width, and the two order tests below.
  • netstack implements the boundary once, in device.rs, over toyos::volatile::Window. Both drivers use it, so the two fence calls exist in one body. A driver's holder keeps the mappings; the Intel wrapper drops the driver, the BAR mapping, the grant and the claim in the order it did on main.
  • The barrier instruction is read, not measured. An empty publish or observe in device.rs passes every host test and every x86 guest, where TSO hides it. Nothing runs these drivers on AArch64 before stage 7 of the ARM track. What is measured is the order of the calls, in each crate's host suite: m06 and m07 in toyos-virtio, m31 to m34 in toyos-i219.
  • The Intel model records the two barriers. Its publish and observe were empty, and three mutations at 459e1f3db stayed green, 88 of 88 (the control, below). stub.rs now records every store, load and barrier the driver makes on the grant and every register it writes, as toyos-virtio's stub does, and two tests read that trace over frames moved both ways on both parts, eight seeds each:
    • a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store: no store into the grant stands between the last publish and a write of TDT or RDT. It also counts that TDT moved over a stored descriptor once per frame sent, so it is not vacuous.
    • a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd: after the load that first finds DD in a descriptor, nothing is loaded from the grant before an observe. It counts one such load per frame received and per descriptor sent.
    • What the trace cannot see: a frame's bytes, which the driver's caller copies in and out and are no access of the crate's.
    • Test files only. git diff 459e1f3db HEAD -- toyos-i219/src/lib.rs toyos-i219/src/pch.rs toyos-i219/src/phy.rs userland/netstack/src/device.rs userland/netstack/src/i219.rs userland/netstack/src/card.rs is empty, exit 0 (r3-driver-source-diff.log). toyos-device-memory/src/lib.rs differs from 459e1f3db in five comment lines: with every comment-only line stripped from both, diff exits 0, 26 lines each side (r3-device-memory-code-diff.log).
    • Stage 6 of issues/toyos-runs-on-arm64.md is not given the I219: what its list owes NVMe and xHCI is a test of the order, and for the I219 these two tests are that test on the host tier. What stays unmeasured is the instruction in netstack's device.rs, which issues/assembly-outside-an-arch-module-in-userland-and-guest-probes.md already owns.
  • issues/a-userland-device-driver-has-no-shared-boundary-to-be-written-against.md is deleted: its exit was the ruling and a second driver written against the answer. Nothing else cited it. Its one durable line is in the new crate's header: what one driver alone needs of its substrate is that driver crate's own trait.

Every used-ring refusal ends the driver. finished in virtio_net.rs panics with "this NIC cannot be driven on" and the refusal's own words for each of Head, NoChain, Written and Jumped. rx_refused, TxQueue::refused, reported and VirtioNet::report are gone, and Card::report does nothing for virtio. main.rs is untouched.

  • Why here: this branch's queue never lets the used index pass the available one and takes a refused element, so a counted drop spent an element a chain in flight was owed, and a Written on a transmit chain kept its head for the boot.
  • What ending costs today. system.toml gives restart = true to diskserver and fileserver only. netstack ends by name and the machine has no network until it boots again. Filed as issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md; whether netstack restarts is not decided here.

The transport. Layout::of(&[VendorCap]) takes the capability walk's result; Offer::acknowledge → Offer::accept → Setup → Setup::driver_ok → Live.

  • Every Setup step now takes the Setup by value and a refusal does not give it back. DRIVER_OK cannot be written over FAILED (§3.1.1): there is no value left to call driver_ok on.
  • NO_VECTOR is the stub's constant; the crate compares against the entry it wrote.
  • The oracle is VIRTIO Version 1.2, OASIS Committee Specification 01; each rule cites its section at the site. stub.rs is the device written from it and asserts what the specification requires of the driver. No other system's driver source was read.

The device is untrusted. Every device-written word is bounded before it is an offset, index or length:

Input Bound Refusal
capability bar 0..=5 (§4.1.4) ignored (§4.1.4.1), so MissingCap
capability offset, length inside the BAR, summed in 64 bits; long enough; aligned OutsideBar, TooShort, Misaligned
device_status after reset reads 0 ResetUnanswered
feature bits only offered ones written; VERSION_1 required; FEATURES_OK read back NotVersion1, FeaturesRefused
queue_size at least the driver's rings QueueTooShallow
queue_notify_off × multiplier two bytes inside the notification structure, even Doorbell
vector fields read back what was written NoVector
device-structure field inside the capability's length PastDeviceConfig
used id below the table UsedRefusal::Head
used id a head with a chain in flight UsedRefusal::NoChain
used len the chain's device-writable bytes (§2.7.8) UsedRefusal::Written
used idx never past the driver's own available count UsedRefusal::Jumped

What changes in the shipped virtio NIC driver, each from the specification:

  • Register widths follow §4.1.3.1. main wrote device_status as 32 bits and a queue address as one 64-bit store.
  • A used len is bounded by the chain's writable bytes, which for a transmit chain is 0.
  • All four used-ring violations end netstack; on main three were counted and the fourth did not exist.
  • A notify offset outside the notification structure and a device field past the device structure were Window assertions a device could trip; both are refusals.
  • A device without VIRTIO_F_VERSION_1 is refused. A refusal after the acknowledgement sets FAILED.
  • The feature line is printed after FEATURES_OK is read back; its text is the same.

Left for the client that needs it, and not built:

  • Device-structure reads wider than a byte, writes to it, and reads while live (virtio-gpu, virtio-sound, virtio-input need them, at §4.1.3.1's widths). The NIC reads six bytes before DRIVER_OK.
  • The capability walk stays in netstack until a second client exists. Stage 1 of issues/every-driver-is-still-in-the-kernel.md now says the second client moves it into the crate, and that it measures what QEMU reports as len on a device-readable chain before it ends a device on Written there.
  • The walk's config_read(..).unwrap_or(0) and its unmasked links are as on main, filed as issues/the-virtio-capability-walk-reads-a-refused-configuration-read-as-zeros.md.
  • The kernel's transport (kernel/src/drivers/virtio.rs) is not shared: the track's target deletes it.

Gates

At the head above unless the row says otherwise. File names are in the step-log directory the brief names.

Gate Command Exit
boundary crate, 0 tests cargo test -p toyos-virtio -p toyos-device-memory (r3-crate.log), and in the host gate 0
toyos-virtio, 33 tests same command, and in the host gate 0
toyos-i219, 90 tests (88 on main) cargo test -p toyos-i219 (r3-i219.log), and in the host gate 0
netstack, 30 tests cargo test in userland/netstack (r3-netstack.log), and in the host gate 0
host gate, 78 steps, clippy and the source gates among them cargo run -- --ci host (r3-ci-host.log) 0
image cargo run -- --build-only (r3-build-only.log) 0
three network guest tests, 3 of 3, at 459e1f3db cargo test --test toyos-build -- netstack_socket_churn libc_sockets iommu_virtio_platform (r2-guest-net.log) 0
whole guest suite, 37 of 37, at 459e1f3db cargo test (r2-guest-suite.log) 0

The T14 reading, taken (the orchestrator's, in the comment "The T14 on the Intel driver written against the shared boundary"). The Intel driver's crate changed, so a reading was owed. No boot on main runs netstack on the T14's wired card, so it was staged on a measurement branch with no pull request, wt/toyos-arm-u1-t14 at 795c67656: 459e1f3db with origin/wt/toyos-move-rows-edge merged. The driver-path diff between the two is empty, and so is the driver-path diff from 459e1f3db to this head (above), so the reading carries.

  • Command: cargo test --test toyos-build -- --metal --metal-readback <dir> boot:outbound. Exit 0, 2 passed, 0 failed (outbound_router, outbound_internet), one boot. Image sha256 bf95ff896355b150b39b0de9fa462ffe7de3b29e85277e1a862a228307f14122, as staged.
main's driver at #782 795c67656
link up after the driver came up 2721 ms 2761 ms
lease after netstack came up 13337 ms 13322 ms
both anchors connected connected
ring line full=7 wake_armed=2 wake_taken=1 untaken=1 stranded=0 descriptors_sent=73 wire_sent=73 taken=60 full=6 wake_armed=2 wake_taken=1 untaken=1 stranded=0 descriptors_sent=73 wire_sent=73 taken=60
cannot be driven on / offered to a transmit ring that refuses it 0 0
  • SAME, as the round-2 review judged it: every descriptor sent reached the wire, none stranded, no refusal line. full counts askings that found no room and differs by one between two boots of the same job.

Negative controls

34 mutations, each a checked patch, built (exit 0), tested, reversed, tree clean after each. Every one is red (test exit 101). m01 to m30 were run at 459e1f3db and are in the round-2 mutation comment; m31 to m34 were run at this head and are in the round-3 comment, patches included.

  • Used ring (m01 to m05), ordering (m06, m07, m22, m23), transport bounds (m08 to m21), the driver's own mistake (m24).
  • m25 to m28: each of Head, NoChain, Written, Jumped returned to a counted drop in finished. Each reds exactly the netstack test that names that refusal.
  • m29, m30: the Intel driver reading a descriptor as 32 bits, and writing TDT as 16. Red in 9 and 12 of its tests, at the model's width assertion.
  • m31 to m33, with their control. self.dma.publish(); deleted before TDT (m31) and before RDT (m32), self.dma.observe(); deleted in receive between the two loads of the descriptor (m33). At 459e1f3db, each alone: build exit 0, test exit 0, 88 passed, 0 failed (r3-mutations/control-summary.txt). At this head: test exit 101, 89 passed, 1 failed; m31 and m32 red at the tail-register test, m33 at the descriptor-fields test (r3-mutations/head-summary.txt).
  • m34, not asked for: self.dma.observe(); deleted in the transmit reclaim. Red at the descriptor-fields test, 89 passed, 1 failed. It has no control run; at 459e1f3db the deleted call was to an empty function.

Growth

git diff --shortstat origin/main...HEAD: 25 files, 3332 insertions, 839 deletions.

  • Production, net +668 lines: toyos-virtio +893 (lib.rs 68, pci.rs 457, queue.rs 368), toyos-device-memory +96, netstack's device.rs +114; virtio_net.rs 810 to 453, netstack's i219.rs 401 to 358, toyos-i219/src/lib.rs -36. Round 2 was +663; the five lines are the contract's sentence.
  • Tests: the virtio crate's stub.rs and tests.rs are 1,456 lines; netstack's go from 126 to 136; the Intel crate's stub and tests gain 231, of which 76 are trait implementations and 155 are the recorded trace and the two order tests.
  • No new guest test. No new external dependency.

Unsure of

  • The width-named methods cost the Intel crate's test stubs four panicking methods each. A generic read::<W> would be shorter there and would let a width be inferred at a call site, which is the mistake §4.1.3.1 exists for. I chose names.
  • The PCI citation in the new capability-walk issue is from memory, and the file says so.
  • The reset is one write and one read, and the MAC is read without the generation check §2.5.1 recommends; both as shipped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A

…and netstack's virtio-net is its first client

toyos-virtio holds what every virtio driver in userland decides before its
device type begins: which capabilities name the device's structures and
whether the BAR holds them, the initialisation virtio 1.2 §3.1.1 orders, the
feature negotiation §2.2 bounds, a queue's configuration, the publication
order §2.7.13 fixes, and what a used-ring element has to satisfy. It is
written against two traits, the register window and the DMA grant, which
netstack implements one instruction deep; its host tests drive it with a
device written from the specification and with rings no device would write.

The order of the initialisation is in the types: only a negotiated device
configures a queue, a queue is enabled by the call that gave it its addresses
and its vector, and only a live device is notified.

What changes for the shipped NIC driver, each from the specification:

- Register accesses take the width §4.1.3.1 requires: a byte for
  device_status, where a 32-bit write also covered config_generation and
  queue_select, and two 32-bit halves for a queue's addresses.
- A used element's length is bounded by its chain's device-writable bytes
  (§2.7.8), which for a transmitted frame is none.
- The available index is the driver's own count and is never read back from
  memory the device reaches.
- A used index past the chains made available is refused, and netstack ends
  by name on it: nothing in the ring can be told from a stale entry.
- A notify offset the notification structure does not hold, and a device
  field past the device structure, are refusals where they were window
  assertions.
- A device without VIRTIO_F_VERSION_1 is refused; a refusal after the
  acknowledgement sets FAILED.

The frames, the room and wake contract and the outward interface of
`VirtioNet` are as they were. The kernel's own transport is not shared: it is
written against the kernel's `Mmio` and `Dma`, waits on the kernel's clock,
and loses two of its three users to the same plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches and their verdicts

At head 1e41c43949ab164ad1eba7382c91a4de52939f6b. Each: git apply --check, git apply, cargo test --no-run (build exit), cargo test (test exit), git apply -R; the tree was clean afterwards. m25 runs netstack's suite, the rest cargo test -p toyos-virtio.

m01-head-bound build_exit=0 test_exit=101 red=[a_head_past_the_table_is_refused a_hostile_device_is_believed_only_where_the_model_agrees a_refused_element_does_not_hide_the_ones_behind_it ]
m02-written-bound build_exit=0 test_exit=101 red=[a_hostile_device_is_believed_only_where_the_model_agrees more_bytes_than_the_chain_may_be_written_is_refused ]
m03-jump build_exit=0 test_exit=101 red=[a_hostile_device_is_believed_only_where_the_model_agrees a_used_index_past_what_was_made_available_is_refused_and_nothing_is_read ]
m04-no-chain build_exit=0 test_exit=101 red=[a_head_with_no_chain_in_flight_is_refused a_hostile_device_is_believed_only_where_the_model_agrees ]
m05-writable-is-whole-chain build_exit=0 test_exit=101 red=[a_hostile_device_is_believed_only_where_the_model_agrees more_bytes_than_the_chain_may_be_written_is_refused ]
m06-publish-barrier build_exit=0 test_exit=101 red=[a_chain_is_whole_before_its_index_and_its_index_before_its_notification ]
m07-observe-barrier build_exit=0 test_exit=101 red=[a_used_element_is_read_after_the_index_that_counts_it ]
m08-outside-bar build_exit=0 test_exit=101 red=[a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m09-outside-bar-wraps build_exit=0 test_exit=101 red=[a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m10-doorbell-bound build_exit=0 test_exit=101 red=[a_notify_offset_outside_the_notification_structure_is_refused ]
m11-accepts-unoffered build_exit=0 test_exit=101 red=[only_what_the_device_offers_is_accepted ]
m12-device-config-bound build_exit=0 test_exit=101 red=[a_field_past_the_device_structure_is_refused_and_not_read ]
m13-vector-unverified build_exit=0 test_exit=101 red=[a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled ]
m14-features-ok-unread build_exit=0 test_exit=101 red=[a_device_that_does_not_keep_features_ok_is_refused_and_told_so ]
m15-reserved-bar build_exit=0 test_exit=101 red=[a_device_missing_a_structure_is_refused_by_its_name the_first_capability_of_each_type_in_a_real_bar_is_the_one_used ]
m16-shallow-queue build_exit=0 test_exit=101 red=[a_queue_shallower_than_the_drivers_rings_is_refused ]
m17-reset-unread build_exit=0 test_exit=101 red=[a_device_that_never_finishes_its_reset_is_refused_and_written_nothing_more ]
m18-version-1 build_exit=0 test_exit=101 red=[a_device_without_version_1_is_refused_and_told_so ]
m19-too-short build_exit=0 test_exit=101 red=[a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m20-misaligned build_exit=0 test_exit=101 red=[a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m21-split-bars build_exit=0 test_exit=101 red=[structures_in_two_bars_are_refused ]
m22-status-32bit build_exit=0 test_exit=101 red=[a_chain_on_a_queue_the_device_was_never_given_is_the_drivers_mistake a_chain_is_whole_before_its_index_and_its_index_before_its_notification a_device_that_does_not_keep_features_ok_is_refused_and_told_so a_device_without_version_1_is_refused_and_told_so a_field_past_the_device_structure_is_refused_and_not_read a_notification_is_the_queues_index_at_the_queues_address a_notify_offset_outside_the_notification_structure_is_refused a_queue_shallower_than_the_drivers_rings_is_refused a_structure_its_bar_does_not_hold_is_refused_and_never_reached a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled only_what_the_device_offers_is_accepted the_first_capability_of_each_type_in_a_real_bar_is_the_one_used the_initialisation_is_the_sequence_the_specification_orders ]
m23-enable-before-vector build_exit=0 test_exit=101 red=[a_chain_is_whole_before_its_index_and_its_index_before_its_notification a_chain_on_a_queue_the_device_was_never_given_is_the_drivers_mistake a_notification_is_the_queues_index_at_the_queues_address a_notify_offset_outside_the_notification_structure_is_refused a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled the_initialisation_is_the_sequence_the_specification_orders ]
m24-in-flight-overlap build_exit=0 test_exit=101 red=[a_chain_over_a_descriptor_in_flight_is_the_drivers_mistake ]
m25-netstack-jump-not-fatal build_exit=0 test_exit=101 red=[virtio_net::a_used_ring_that_cannot_be_read_ends_the_driver ]
m01-head-bound.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -345,7 +345,7 @@
         let len = Untrusted::new(self.mem.read32(at + USED_ELEM_LEN));
         self.last_used = self.last_used.wrapping_add(1);
 
-        let first = id.index(self.slots.len()).map_err(UsedRefusal::Head)?;
+        let first = id.index(usize::MAX).map_err(UsedRefusal::Head)?;
         // Exact: `index` proved it below the table's length, a `u16`.
         let head = first as u16;
         let chain = self.slots[first];
m02-written-bound.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -353,7 +353,7 @@
             return Err(UsedRefusal::NoChain { head });
         }
         let written = len
-            .at_most(chain.writable as u64)
+            .at_most(u64::MAX)
             .map_err(|len| UsedRefusal::Written { head, len })?;
         for slot in &mut self.slots[first..first + chain.descs as usize] {
             *slot = Slot::default();
m03-jump.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -334,9 +334,6 @@
         // §2.7.8: an element "matches an entry placed in the available ring by
         // the guest earlier", so the device's count never passes the driver's.
         let available = self.next_avail.wrapping_sub(self.last_used);
-        if pending > available {
-            return Err(UsedRefusal::Jumped { used, taken: self.last_used, available: self.next_avail });
-        }
         self.mem.observe();
         let at = self.parts.used
             + RING_ENTRIES
m04-no-chain.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -349,9 +349,6 @@
         // Exact: `index` proved it below the table's length, a `u16`.
         let head = first as u16;
         let chain = self.slots[first];
-        if chain.descs == 0 {
-            return Err(UsedRefusal::NoChain { head });
-        }
         let written = len
             .at_most(chain.writable as u64)
             .map_err(|len| UsedRefusal::Written { head, len })?;
m05-writable-is-whole-chain.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -309,7 +309,7 @@
             self.slots[first + nth].held = true;
         }
         self.slots[first].descs = chain.len() as u16;
-        self.slots[first].writable = writable;
+        self.slots[first].writable = total;
 
         let entry = (self.next_avail % size) as usize;
         self.mem.write16(self.parts.avail + RING_ENTRIES + entry * AVAIL_ENTRY_BYTES, head);
m06-publish-barrier.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:55
@@ -313,7 +313,6 @@
 
         let entry = (self.next_avail % size) as usize;
         self.mem.write16(self.parts.avail + RING_ENTRIES + entry * AVAIL_ENTRY_BYTES, head);
-        self.mem.publish();
         self.next_avail = self.next_avail.wrapping_add(1);
         self.mem.write16(self.parts.avail + RING_IDX, self.next_avail);
         self.mem.publish();
m07-observe-barrier.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:56
@@ -337,7 +337,6 @@
         if pending > available {
             return Err(UsedRefusal::Jumped { used, taken: self.last_used, available: self.next_avail });
         }
-        self.mem.observe();
         let at = self.parts.used
             + RING_ENTRIES
             + (self.last_used % self.size) as usize * USED_ELEM_BYTES;
m08-outside-bar.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -226,9 +226,6 @@
         align: u32,
     ) -> Result<Self, Refusal> {
         // Sixty-four bits, so two device-chosen dwords cannot wrap.
-        if cap.offset as u64 + cap.length as u64 > window as u64 {
-            return Err(Refusal::OutsideBar(what));
-        }
         if (cap.length as usize) < need {
             return Err(Refusal::TooShort(what));
         }
m09-outside-bar-wraps.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -226,7 +226,7 @@
         align: u32,
     ) -> Result<Self, Refusal> {
         // Sixty-four bits, so two device-chosen dwords cannot wrap.
-        if cap.offset as u64 + cap.length as u64 > window as u64 {
+        if cap.offset.wrapping_add(cap.length) as u64 > window as u64 {
             return Err(Refusal::OutsideBar(what));
         }
         if (cap.length as usize) < need {
m10-doorbell-bound.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -401,9 +401,6 @@
         // product fits sixty-four bits.
         let notify_off = self.wires.regs.read16(at + common::QUEUE_NOTIFY_OFF);
         let into = notify_off as u64 * self.wires.notify_off_multiplier as u64;
-        if into + 2 > self.wires.notify.bytes as u64 || !into.is_multiple_of(2) {
-            return Err(self.wires.refuse(Refusal::Doorbell { queue: index, notify_off }));
-        }
         let doorbell = self.wires.notify.at + into as usize;
 
         self.bind(common::QUEUE_MSIX_VECTOR, entry, Source::Queue(index))?;
m11-accepts-unoffered.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -323,7 +323,7 @@
         if self.offered & VIRTIO_F_VERSION_1 == 0 {
             return Err(self.wires.refuse(Refusal::NotVersion1 { offered: self.offered }));
         }
-        let features = self.offered & (wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM);
+        let features = wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM;
         let wires = &mut self.wires;
         let at = wires.common;
         wires.regs.write32(at + common::DRIVER_FEATURE_SELECT, 0);
m12-device-config-bound.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -415,9 +415,6 @@
     /// One byte of the device-specific structure, `at` bytes into it.
     pub fn device_read8(&mut self, at: usize) -> Result<u8, Refusal> {
         let Region { at: base, bytes } = self.wires.device;
-        if at >= bytes {
-            return Err(self.wires.refuse(Refusal::PastDeviceConfig { at, bytes }));
-        }
         Ok(self.wires.regs.read8(base + at))
     }
 
m13-vector-unverified.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -363,9 +363,6 @@
         let at = self.wires.common + field;
         self.wires.regs.write16(at, entry);
         // §4.1.5.1.2.2: "on success, the previously written value is returned".
-        if self.wires.regs.read16(at) != entry {
-            return Err(self.wires.refuse(Refusal::NoVector(source)));
-        }
         Ok(())
     }
 
m14-features-ok-unread.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -332,9 +332,6 @@
         wires.regs.write32(at + common::DRIVER_FEATURE, (features >> 32) as u32);
         wires.set_status(status::FEATURES_OK);
         let answered = wires.regs.read8(at + common::DEVICE_STATUS);
-        if answered & status::FEATURES_OK == 0 {
-            return Err(wires.refuse(Refusal::FeaturesRefused { accepted: features, status: answered }));
-        }
         Ok(Setup { wires: self.wires, features, doorbells: Vec::new() })
     }
 }
m15-reserved-bar.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -189,7 +189,7 @@
     pub fn of(caps: &[VendorCap]) -> Result<Self, Refusal> {
         let first = |cfg_type: u8, what: &'static str| {
             caps.iter()
-                .find(|cap| cap.cfg_type == cfg_type && cap.bar <= LAST_BAR)
+                .find(|cap| cap.cfg_type == cfg_type)
                 .copied()
                 .ok_or(Refusal::MissingCap(what))
         };
m16-shallow-queue.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -382,9 +382,6 @@
         self.wires.regs.write16(at + common::QUEUE_SELECT, index);
         // The most the device takes, and 0 for a queue it does not have.
         let offered = self.wires.regs.read16(at + common::QUEUE_SIZE);
-        if offered < wanted {
-            return Err(self.wires.refuse(Refusal::QueueTooShallow { queue: index, offered, wanted }));
-        }
         self.wires.regs.write16(at + common::QUEUE_SIZE, wanted);
         for (field, addr) in [
             (common::QUEUE_DESC, queue.desc_addr()),
m17-reset-unread.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -293,9 +293,6 @@
 
         let at = wires.common;
         wires.regs.write8(at + common::DEVICE_STATUS, 0);
-        if wires.regs.read8(at + common::DEVICE_STATUS) != 0 {
-            return Err(Refusal::ResetUnanswered);
-        }
         wires.set_status(status::ACKNOWLEDGE);
         wires.set_status(status::DRIVER);
 
m18-version-1.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -320,9 +320,6 @@
     /// wherever it is offered. Nothing the device did not offer is written
     /// (§2.2.1).
     pub fn accept(mut self, wanted: u64) -> Result<Setup<R>, Refusal> {
-        if self.offered & VIRTIO_F_VERSION_1 == 0 {
-            return Err(self.wires.refuse(Refusal::NotVersion1 { offered: self.offered }));
-        }
         let features = self.offered & (wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM);
         let wires = &mut self.wires;
         let at = wires.common;
m19-too-short.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -229,9 +229,6 @@
         if cap.offset as u64 + cap.length as u64 > window as u64 {
             return Err(Refusal::OutsideBar(what));
         }
-        if (cap.length as usize) < need {
-            return Err(Refusal::TooShort(what));
-        }
         if !cap.offset.is_multiple_of(align) {
             return Err(Refusal::Misaligned(what));
         }
m20-misaligned.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -232,9 +232,6 @@
         if (cap.length as usize) < need {
             return Err(Refusal::TooShort(what));
         }
-        if !cap.offset.is_multiple_of(align) {
-            return Err(Refusal::Misaligned(what));
-        }
         Ok(Self { at: cap.offset as usize, bytes: cap.length as usize })
     }
 }
m21-split-bars.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -197,9 +197,6 @@
         let notify = first(CFG_NOTIFY, "NOTIFY_CFG")?;
         let isr = first(CFG_ISR, "ISR_CFG")?;
         let device = first(CFG_DEVICE, "DEVICE_CFG")?;
-        if [notify, isr, device].iter().any(|cap| cap.bar != common.bar) {
-            return Err(Refusal::SplitAcrossBars);
-        }
         Ok(Self { common, notify, device })
     }
 
m22-status-32bit.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:56
@@ -253,7 +253,7 @@
 impl<R: Registers> Wires<R> {
     fn set_status(&mut self, bit: u8) {
         self.status |= bit;
-        self.regs.write8(self.common + common::DEVICE_STATUS, self.status);
+        self.regs.write32(self.common + common::DEVICE_STATUS, self.status as u32);
     }
 
     /// Give the device up (§3.1.1), and hand the reason back.
m23-enable-before-vector.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 12:30:57
+++ b/toyos-virtio/src/pci.rs	2026-10-09 12:30:57
@@ -406,8 +406,8 @@
         }
         let doorbell = self.wires.notify.at + into as usize;
 
-        self.bind(common::QUEUE_MSIX_VECTOR, entry, Source::Queue(index))?;
         self.wires.regs.write16(at + common::QUEUE_ENABLE, 1);
+        self.bind(common::QUEUE_MSIX_VECTOR, entry, Source::Queue(index))?;
         self.doorbells.push((index, doorbell));
         Ok(())
     }
m24-in-flight-overlap.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 12:30:57
+++ b/toyos-virtio/src/queue.rs	2026-10-09 12:30:57
@@ -280,11 +280,6 @@
         let mut writable = 0u32;
         for (nth, buffer) in chain.iter().enumerate() {
             assert!(
-                !self.slots[first + nth].held,
-                "virtqueue {queue}: a chain at head {head} takes descriptor {}, which is in flight",
-                first + nth
-            );
-            assert!(
                 buffer.writable || writable == 0,
                 "virtqueue {queue}: the chain at head {head} has a device-readable element after \
                  a device-writable one"
m25-netstack-jump-not-fatal.patch
--- a/userland/netstack/src/virtio_net.rs	2026-10-09 12:30:57
+++ b/userland/netstack/src/virtio_net.rs	2026-10-09 12:30:57
@@ -224,9 +224,7 @@
             Err(
                 UsedRefusal::Head(_) | UsedRefusal::NoChain { .. } | UsedRefusal::Written { .. },
             ) => *refused = refused.saturating_add(1),
-            Err(why @ UsedRefusal::Jumped { .. }) => {
-                panic!("netstack: this NIC cannot be driven on — {why}")
-            }
+            Err(UsedRefusal::Jumped { .. }) => return None,
         }
     }
 }

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1, at 1e41c4394 against origin/main d9f4a3ae7

Read: the one commit, the diff, every changed file whole, the mutation comment, main's virtio_net.rs, and VIRTIO Version 1.2 cs01 fetched from docs.oasis-open.org. No build, test or QEMU run. The step logs the body names are not in my reach: every line below about a gate rests on the body's command and exit code, and the orchestrator opens the logs.

Net lines (git diff --shortstat origin/main...1e41c4394): 10 files, +2683 -546. Production: toyos-virtio +924 (lib.rs 107, pci.rs 453, queue.rs 364), virtio_net.rs 809 to 571; net +686. Tests: crate +1433 (stub.rs 540, tests.rs 893), netstack 127 to 110. The growth is accepted as an input boundary's own crate; what it can still lose is named under the second BLOCKER and the third.

BLOCKER

  • toyos-virtio/src/lib.rs:67 and :90 — a second Registers and a second DmaBuffers, beside toyos-i219/src/lib.rs:114 and :149, same names, same two barriers — issues/a-userland-device-driver-has-no-shared-boundary-to-be-written-against.md leaves exactly this to the owner ("whether there is one boundary at all, or whether each driver crate keeps its own ... The call is the owner's, and nothing is owed until a second userland driver exists to be written against it") and names this moment as its trigger. The branch takes the second answer, does not say so, and leaves the issue as it was. netstack now carries the barrier pair twice, userland/netstack/src/i219.rs:92-97 and userland/netstack/src/virtio_net.rs:171-177, and three more clients would make it five. Owed: the owner's ruling, recorded in that issue in this diff (closed with it, or one declaration both crates are written against).

  • userland/netstack/src/virtio_net.rs:220-232 — finished counts and drops Head, NoChain and Written, and under this branch's own queue that is no longer what the shipped driver did; it is a failure hidden until later. Two things this diff adds make it so. (a) queue.rs:336-339: the used index may never pass the available one, and a refused element is taken (queue.rs:346), so each one spends an element a real chain was owed. The branch's own test shows the end of it: a_used_ring_that_cannot_be_read_ends_the_driver sends two frames, has one element refused, and then sets the used index to 3, which is the device giving both frames back; netstack dies there with "the used index reads 3", one refusal away from its cause. (b) Written leaves its chain in flight (queue.rs:355-357) with the element spent, and a transmit chain's bound is now 0, so a transmit head refused once never returns to free; sixteen of them and tx_room() answers 0 for the rest of the boot, with one counted line. None of the three is something a conforming device produces: §2.7.8 has id match an entry the driver placed in the available ring, and §2.7.8.2 has the device "write at least len bytes to descriptor, beginning at the first device-writable buffer", which no chain can hold past its writable bytes; the tolerance §2.7.8.1 describes is the legacy interface's, which VERSION_1 rules out here. Owed in this pull request, because this pull request is what turned the counted arm into a deferred one: all four end the driver in finished, each by its own UsedRefusal; rx_refused, TxQueue::refused, reported and VirtioNet::report go, with card.rs:177's arm; and the netstack test holds each arm. Mutation to run after the change: any one of Head, NoChain, Written returned to *refused += 1 must red a netstack test that names that refusal.

  • toyos-virtio/src/pci.rs:68 — pub const NO_VECTOR has no reader outside stub.rs; bind compares against entry. It ships for the test alone. It belongs in stub.rs.

NOTE

  • toyos-virtio/src/pci.rs:358, :375, :416, :425 — a refusal sets FAILED and leaves the Setup alive, so driver_ok() after a refused queue compiles and writes DRIVER_OK over FAILED; §3.1.1 has the driver not continue. "The order is in the types" does not hold past the first refusal. The second client is the one that will probe a queue and carry on.
  • toyos-virtio/src/pci.rs:416 — the device structure is one byte, read, before DRIVER_OK. virtio-gpu and virtio-sound read 32-bit fields, virtio-input writes select and subsel, and virtio-gpu reads events_read while live. Those are widths and a lifetime, not device knowledge, so the shape carries them; they are growth the next stage owes a reason for, at §4.1.3.1's widths.
  • toyos-virtio/src/queue.rs:355 — before a later client makes Written fatal on a chain the device only reads, measure what QEMU's device reports there. My recollection, unverified, is that its virtio-input reports the bytes it consumed on the status queue. The NIC's transmit queue is answered by the guest suite: it could not pass with heads leaking.
  • userland/netstack/src/virtio_net.rs:555 — config_read(..).unwrap_or(0) reads a refused configuration read as zeros, and the walk does not mask the pointer's two reserved bits. As on main; file it, since the walk is the thirty lines the second client moves into the crate.
  • The capability walk staying in netstack until a second client exists is the right order. It is recorded only in this body; it belongs in stage 1 of issues/every-driver-is-still-in-the-kernel.md, where the implementer of virtio-gpu reads.
  • Body: "Stage U1 of the ARM desktop plan" and "stages U2 and U4" name nothing in the tree. What the tree says is issues/every-driver-is-still-in-the-kernel.md (the owner's target: virtio nowhere in the kernel) and stages 6 and 7 of issues/toyos-runs-on-arm64.md.
  • Body: bar_map_again claims the NIC itself and does not run netstack's driver; the guest tests that do are netstack_socket_churn, libc_sockets and iommu_virtio_platform.

What was checked and holds

  • §4.1.3.1 widths. main wrote device_status (0x14, one byte) as 32 bits: common.write::<u32>(COMMON_DEVICE_STATUS, ..), five times, each also storing config_generation (0x15) and queue_select (0x16). It wrote queue_desc, queue_driver and queue_device as one 64-bit store, where the section gives "32-bit wide and aligned accesses for 32-bit and 64-bit wide fields". Both are real and both are fixed; the stub reds on any other width (m22).
  • §3.1.1, §2.2. Reset written and read; ACKNOWLEDGE, DRIVER; both feature halves; only offered bits written; VERSION_1 required; FEATURES_OK read back; FAILED over the bits set. One read of the reset and a refusal is inside §4.1.4.3.2: the device is never reinitialised without a 0.
  • §2.7. The driver loads nothing but the used ring (queue.rs:329, :344, :345); the available index and what is in flight are its own memory, so a looped table has no reader, and the test asserts no load of the table. Indices wrap in u16; size is a power of two at most 32768. Always notifying is inside §2.7.10.1 ("If flags is 1, the driver SHOULD NOT"), and avail.flags 0 has the device notify per buffer.
  • Ordering. The sequence is the crate's: entry, publish, index, publish, and only Live::notify takes the Published; index, observe, element. m06 and m07 red at tests named for it. The instruction is each implementation's, and an empty one passes every host test and every x86 guest; nothing runs netstack on AArch64 before stage 7 of the ARM track, so that half is read, not measured. One declaration (first BLOCKER) would make it one body.
  • Device-caused panics. The two the body names were real on main: notify.sub(at, 2) on a device's product, and device_cfg.read::<u8>(i) past a window of length.max(4). Both are refusals now. Left in the crate and in virtio_net.rs: none but the named end in finished and Card::undrivable.
  • Mutations. Ten read against the tests they red: m01, m02, m03, m05, m06, m07, m09, m10, m22, m25. Each is red at a test that names the claim; m01 is the negative control, a bound removed and a host test red.
  • VirtioNet's outward interface is unchanged and card.rs and main.rs are untouched at this head. The second BLOCKER's fix touches card.rs:177.
  • The kernel's transport not shared: right. The track's target deletes it; a crate serving both would be built for a user that is leaving.

Answers the brief asked for

  • All four fatal: right, and here, for the reasons under the second BLOCKER. Its cost is larger than the brief supposes: system.toml gives restart = true to diskserver and fileserver only, so netstack ends by name and the machine has no network until it boots again. That is still the right end for a device that broke the ring once; whether netstack restarts is its own issue.
  • T14: no reading is owed. The change targets a device the T14 does not have, and Card::intel's path is not in the diff. No boot of the metal profile judges netstack on a wire; the LAN hold boot holds a boot open and judges nothing, so it would be no evidence.
  • Order with the move: this lands first. The move then merges main, is written against the VirtioNet and Card::report that land here, and runs the three guest tests above at its merged head.
  • At the head that lands: host green (its workspace step runs the crate's suite and its userland/netstack step runs netstack's) and guest / suite green with netstack_socket_churn, libc_sockets and iommu_virtio_platform in it. Once a later round finds the three BLOCKERs closed, with the mutation above run, the orchestrator may land on reading those two checks. Not at this head.

SEND BACK

Japabu and others added 2 commits October 9, 2026 13:15
…ng refusal ends netstack's driver; a refused Setup is gone

The owner's ruling, 2026-10-09. Asked "Userland drivers reach their device
through a small interface (read/write registers, DMA buffers, two memory
barriers). The Intel NIC crate has its own copy and the new virtio crate added
a second, identical one. An issue in the tree reserves this call for you: one
shared interface, or one per driver crate?", he answered: "One shared
interface (Recommended)".

toyos-device-memory declares it: `Registers`, `DmaBuffers` and the order of
the two barriers, stated once. The widths are in the method names, each one an
access a driver in the tree makes. toyos-i219 and toyos-virtio are written
against it and their own declarations are gone; the Intel crate's change is
its accesses renamed to the width they always had (32-bit registers, 64-bit
descriptor halves), and its model now reds on any other. netstack implements
the boundary once, in device.rs, over `toyos::volatile::Window`, so the two
fences are one body; a driver's holder keeps the mappings, in the order the
Intel wrapper dropped them before. `Clock` and `Interrupts` stay toyos-i219's:
one driver needs them.

issues/a-userland-device-driver-has-no-shared-boundary-to-be-written-against.md
is closed by that: its exit was the ruling and a second driver written against
the answer.

netstack ends on each of the four `UsedRefusal`s, by the refusal's own words.
The branch's queue made a counted drop a deferred failure: the used index may
not pass the available one and a refused element is taken, so each one spent
an element a chain in flight was owed, and a `Written` on a transmit chain
kept its head for the boot. `rx_refused`, `TxQueue::refused`, `reported` and
`VirtioNet::report` go with it. What ending costs is
issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md.

`Setup`'s steps take it by value and a refusal does not give it back, so
`DRIVER_OK` cannot be written over `FAILED`. `NO_VECTOR` is the stub's.

Filed: the capability walk's `unwrap_or(0)` and unmasked links, owned by the
second client, which moves the walk into the crate; stage 1 of
issues/every-driver-is-still-in-the-kernel.md says so, and that QEMU's `len`
on a device-readable chain is measured before a client ends on it there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches and their verdicts, round 2

At head 459e1f3dbf0eb8a1a0d0fc9049727aed8c38acc0 (the head file beside the patches says which). Each was applied with git apply after git apply --check, built with cargo test --no-run (exit 0 for every one), tested, and reversed with git apply -R; git status --porcelain --ignore-submodules=none was empty after each. Every one is red: test exit 101.

m01 to m24 are round 1's, written again against this head because both of their files changed. m25 to m28 are the review's: each of the four UsedRefusals returned to a counted drop in netstack's finished, red at the one test that names that refusal. m29 and m30 are new: the Intel driver making an access of another width than its register file and its descriptors have, red at its model.

m01-head-bound build_exit=0 test_exit=101 tree=clean red=[tests::a_head_past_the_table_is_refused tests::a_hostile_device_is_believed_only_where_the_model_agrees tests::a_refused_element_does_not_hide_the_ones_behind_it ]
m02-written-bound build_exit=0 test_exit=101 tree=clean red=[tests::a_hostile_device_is_believed_only_where_the_model_agrees tests::more_bytes_than_the_chain_may_be_written_is_refused ]
m03-jump build_exit=0 test_exit=101 tree=clean red=[tests::a_hostile_device_is_believed_only_where_the_model_agrees tests::a_used_index_past_what_was_made_available_is_refused_and_nothing_is_read ]
m04-no-chain build_exit=0 test_exit=101 tree=clean red=[tests::a_head_with_no_chain_in_flight_is_refused tests::a_hostile_device_is_believed_only_where_the_model_agrees ]
m05-writable-is-whole-chain build_exit=0 test_exit=101 tree=clean red=[tests::a_hostile_device_is_believed_only_where_the_model_agrees tests::more_bytes_than_the_chain_may_be_written_is_refused ]
m06-publish-barrier build_exit=0 test_exit=101 tree=clean red=[tests::a_chain_is_whole_before_its_index_and_its_index_before_its_notification ]
m07-observe-barrier build_exit=0 test_exit=101 tree=clean red=[tests::a_used_element_is_read_after_the_index_that_counts_it ]
m08-outside-bar build_exit=0 test_exit=101 tree=clean red=[tests::a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m09-outside-bar-wraps build_exit=0 test_exit=101 tree=clean red=[tests::a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m10-doorbell-bound build_exit=0 test_exit=101 tree=clean red=[tests::a_notify_offset_outside_the_notification_structure_is_refused ]
m11-accepts-unoffered build_exit=0 test_exit=101 tree=clean red=[tests::only_what_the_device_offers_is_accepted ]
m12-device-config-bound build_exit=0 test_exit=101 tree=clean red=[tests::a_field_past_the_device_structure_is_refused_and_not_read ]
m13-vector-unverified build_exit=0 test_exit=101 tree=clean red=[tests::a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled ]
m14-features-ok-unread build_exit=0 test_exit=101 tree=clean red=[tests::a_device_that_does_not_keep_features_ok_is_refused_and_told_so ]
m15-reserved-bar build_exit=0 test_exit=101 tree=clean red=[tests::a_device_missing_a_structure_is_refused_by_its_name tests::the_first_capability_of_each_type_in_a_real_bar_is_the_one_used ]
m16-shallow-queue build_exit=0 test_exit=101 tree=clean red=[tests::a_queue_shallower_than_the_drivers_rings_is_refused ]
m17-reset-unread build_exit=0 test_exit=101 tree=clean red=[tests::a_device_that_never_finishes_its_reset_is_refused_and_written_nothing_more ]
m18-version-1 build_exit=0 test_exit=101 tree=clean red=[tests::a_device_without_version_1_is_refused_and_told_so ]
m19-too-short build_exit=0 test_exit=101 tree=clean red=[tests::a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m20-misaligned build_exit=0 test_exit=101 tree=clean red=[tests::a_structure_its_bar_does_not_hold_is_refused_and_never_reached ]
m21-split-bars build_exit=0 test_exit=101 tree=clean red=[tests::structures_in_two_bars_are_refused ]
m22-status-32bit build_exit=0 test_exit=101 tree=clean red=[tests::a_chain_on_a_queue_the_device_was_never_given_is_the_drivers_mistake - should panic tests::a_device_that_does_not_keep_features_ok_is_refused_and_told_so tests::a_device_without_version_1_is_refused_and_told_so tests::a_chain_is_whole_before_its_index_and_its_index_before_its_notification tests::a_field_past_the_device_structure_is_refused_and_not_read tests::a_notification_is_the_queues_index_at_the_queues_address tests::a_notify_offset_outside_the_notification_structure_is_refused tests::a_queue_shallower_than_the_drivers_rings_is_refused tests::a_structure_its_bar_does_not_hold_is_refused_and_never_reached tests::a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled tests::only_what_the_device_offers_is_accepted tests::the_first_capability_of_each_type_in_a_real_bar_is_the_one_used tests::the_initialisation_is_the_sequence_the_specification_orders ]
m23-enable-before-vector build_exit=0 test_exit=101 tree=clean red=[tests::a_chain_is_whole_before_its_index_and_its_index_before_its_notification tests::a_chain_on_a_queue_the_device_was_never_given_is_the_drivers_mistake - should panic tests::a_notification_is_the_queues_index_at_the_queues_address tests::a_notify_offset_outside_the_notification_structure_is_refused tests::a_vector_the_device_does_not_map_is_refused_and_its_queue_stays_disabled tests::the_initialisation_is_the_sequence_the_specification_orders ]
m24-in-flight-overlap build_exit=0 test_exit=101 tree=clean red=[tests::a_chain_over_a_descriptor_in_flight_is_the_drivers_mistake - should panic ]
m25-netstack-head-counted-not-fatal build_exit=0 test_exit=101 tree=clean red=[virtio_net::tests::a_used_head_past_the_table_ends_the_driver - should panic ]
m26-netstack-no-chain-counted-not-fatal build_exit=0 test_exit=101 tree=clean red=[virtio_net::tests::a_used_head_with_no_chain_in_flight_ends_the_driver - should panic ]
m27-netstack-written-counted-not-fatal build_exit=0 test_exit=101 tree=clean red=[virtio_net::tests::a_used_length_past_the_chains_writable_bytes_ends_the_driver - should panic ]
m28-netstack-jumped-counted-not-fatal build_exit=0 test_exit=101 tree=clean red=[virtio_net::tests::a_used_index_past_every_frame_offered_ends_the_driver - should panic ]
m29-i219-descriptor-read-as-32-bits build_exit=0 test_exit=101 tree=clean red=[tests::a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told tests::a_full_transmit_ring_answers_room_0 tests::a_ring_a_link_change_left_full_stays_the_parts tests::a_ring_never_written_back_is_refused tests::a_seeded_workload_loses_nothing_and_invents_nothing tests::a_written_back_descriptor_returns_room tests::the_driver_and_the_macs_statistics_count_the_same_frames tests::the_transmit_cause_is_armed_only_when_asked tests::transmit_descriptors_are_reclaimed_under_batched_write_back ]
m30-i219-register-written-as-16-bits build_exit=0 test_exit=101 tree=clean red=[tests::a_burst_past_the_ring_leaves_whole_on_the_room_it_is_told tests::a_frame_longer_than_a_transmit_buffer_is_refused_and_not_truncated tests::a_full_transmit_ring_answers_room_0 tests::a_frame_goes_out_whole tests::a_ring_a_link_change_left_full_stays_the_parts tests::a_ring_never_written_back_is_refused tests::a_seeded_workload_loses_nothing_and_invents_nothing tests::a_written_back_descriptor_returns_room tests::causes_are_acknowledged_by_writing_them_back tests::the_driver_and_the_macs_statistics_count_the_same_frames tests::the_transmit_cause_is_armed_only_when_asked tests::transmit_descriptors_are_reclaimed_under_batched_write_back ]
m01-head-bound.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
@@ -349,7 +349,7 @@
         let len = Untrusted::new(self.mem.read32(at + USED_ELEM_LEN));
         self.last_used = self.last_used.wrapping_add(1);
 
-        let first = id.index(self.slots.len()).map_err(UsedRefusal::Head)?;
+        let first = id.index(usize::MAX).map_err(UsedRefusal::Head)?;
         // Exact: `index` proved it below the table's length, a `u16`.
         let head = first as u16;
         let chain = self.slots[first];
m02-written-bound.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
@@ -357,7 +357,7 @@
             return Err(UsedRefusal::NoChain { head });
         }
         let written = len
-            .at_most(chain.writable as u64)
+            .at_most(u64::MAX)
             .map_err(|len| UsedRefusal::Written { head, len })?;
         for slot in &mut self.slots[first..first + chain.descs as usize] {
             *slot = Slot::default();
m03-jump.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
@@ -338,9 +338,6 @@
         // §2.7.8: an element "matches an entry placed in the available ring by
         // the guest earlier", so the device's count never passes the driver's.
         let available = self.next_avail.wrapping_sub(self.last_used);
-        if pending > available {
-            return Err(UsedRefusal::Jumped { used, taken: self.last_used, available: self.next_avail });
-        }
         self.mem.observe();
         let at = self.parts.used
             + RING_ENTRIES
m04-no-chain.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
@@ -353,9 +353,6 @@
         // Exact: `index` proved it below the table's length, a `u16`.
         let head = first as u16;
         let chain = self.slots[first];
-        if chain.descs == 0 {
-            return Err(UsedRefusal::NoChain { head });
-        }
         let written = len
             .at_most(chain.writable as u64)
             .map_err(|len| UsedRefusal::Written { head, len })?;
m05-writable-is-whole-chain.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:29
@@ -313,7 +313,7 @@
             self.slots[first + nth].held = true;
         }
         self.slots[first].descs = chain.len() as u16;
-        self.slots[first].writable = writable;
+        self.slots[first].writable = total;
 
         let entry = (self.next_avail % size) as usize;
         self.mem.write16(self.parts.avail + RING_ENTRIES + entry * AVAIL_ENTRY_BYTES, head);
m06-publish-barrier.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
@@ -317,7 +317,6 @@
 
         let entry = (self.next_avail % size) as usize;
         self.mem.write16(self.parts.avail + RING_ENTRIES + entry * AVAIL_ENTRY_BYTES, head);
-        self.mem.publish();
         self.next_avail = self.next_avail.wrapping_add(1);
         self.mem.write16(self.parts.avail + RING_IDX, self.next_avail);
         self.mem.publish();
m07-observe-barrier.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
@@ -341,7 +341,6 @@
         if pending > available {
             return Err(UsedRefusal::Jumped { used, taken: self.last_used, available: self.next_avail });
         }
-        self.mem.observe();
         let at = self.parts.used
             + RING_ENTRIES
             + (self.last_used % self.size) as usize * USED_ELEM_BYTES;
m08-outside-bar.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -226,9 +226,6 @@
         align: u32,
     ) -> Result<Self, Refusal> {
         // Sixty-four bits, so two device-chosen dwords cannot wrap.
-        if cap.offset as u64 + cap.length as u64 > window as u64 {
-            return Err(Refusal::OutsideBar(what));
-        }
         if (cap.length as usize) < need {
             return Err(Refusal::TooShort(what));
         }
m09-outside-bar-wraps.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -226,7 +226,7 @@
         align: u32,
     ) -> Result<Self, Refusal> {
         // Sixty-four bits, so two device-chosen dwords cannot wrap.
-        if cap.offset as u64 + cap.length as u64 > window as u64 {
+        if cap.offset.wrapping_add(cap.length) as u64 > window as u64 {
             return Err(Refusal::OutsideBar(what));
         }
         if (cap.length as usize) < need {
m10-doorbell-bound.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -404,9 +404,6 @@
         // product fits sixty-four bits.
         let notify_off = self.wires.regs.read16(at + common::QUEUE_NOTIFY_OFF);
         let into = notify_off as u64 * self.wires.notify_off_multiplier as u64;
-        if into + 2 > self.wires.notify.bytes as u64 || !into.is_multiple_of(2) {
-            return Err(self.wires.refuse(Refusal::Doorbell { queue: index, notify_off }));
-        }
         let doorbell = self.wires.notify.at + into as usize;
 
         let mut bound = self.bind(common::QUEUE_MSIX_VECTOR, entry, Source::Queue(index))?;
m11-accepts-unoffered.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -323,7 +323,7 @@
         if self.offered & VIRTIO_F_VERSION_1 == 0 {
             return Err(self.wires.refuse(Refusal::NotVersion1 { offered: self.offered }));
         }
-        let features = self.offered & (wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM);
+        let features = wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM;
         let wires = &mut self.wires;
         let at = wires.common;
         wires.regs.write32(at + common::DRIVER_FEATURE_SELECT, 0);
m12-device-config-bound.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -418,9 +418,6 @@
     /// One byte of the device-specific structure, `at` bytes into it.
     pub fn device_read8(mut self, at: usize) -> Result<(Self, u8), Refusal> {
         let Region { at: base, bytes } = self.wires.device;
-        if at >= bytes {
-            return Err(self.wires.refuse(Refusal::PastDeviceConfig { at, bytes }));
-        }
         let byte = self.wires.regs.read8(base + at);
         Ok((self, byte))
     }
m13-vector-unverified.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -366,9 +366,6 @@
         let at = self.wires.common + field;
         self.wires.regs.write16(at, entry);
         // §4.1.5.1.2.2: "on success, the previously written value is returned".
-        if self.wires.regs.read16(at) != entry {
-            return Err(self.wires.refuse(Refusal::NoVector(source)));
-        }
         Ok(self)
     }
 
m14-features-ok-unread.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -332,9 +332,6 @@
         wires.regs.write32(at + common::DRIVER_FEATURE, (features >> 32) as u32);
         wires.set_status(status::FEATURES_OK);
         let answered = wires.regs.read8(at + common::DEVICE_STATUS);
-        if answered & status::FEATURES_OK == 0 {
-            return Err(wires.refuse(Refusal::FeaturesRefused { accepted: features, status: answered }));
-        }
         Ok(Setup { wires: self.wires, features, doorbells: Vec::new() })
     }
 }
m15-reserved-bar.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -189,7 +189,7 @@
     pub fn of(caps: &[VendorCap]) -> Result<Self, Refusal> {
         let first = |cfg_type: u8, what: &'static str| {
             caps.iter()
-                .find(|cap| cap.cfg_type == cfg_type && cap.bar <= LAST_BAR)
+                .find(|cap| cap.cfg_type == cfg_type)
                 .copied()
                 .ok_or(Refusal::MissingCap(what))
         };
m16-shallow-queue.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -385,9 +385,6 @@
         self.wires.regs.write16(at + common::QUEUE_SELECT, index);
         // The most the device takes, and 0 for a queue it does not have.
         let offered = self.wires.regs.read16(at + common::QUEUE_SIZE);
-        if offered < wanted {
-            return Err(self.wires.refuse(Refusal::QueueTooShallow { queue: index, offered, wanted }));
-        }
         self.wires.regs.write16(at + common::QUEUE_SIZE, wanted);
         for (field, addr) in [
             (common::QUEUE_DESC, queue.desc_addr()),
m17-reset-unread.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -293,9 +293,6 @@
 
         let at = wires.common;
         wires.regs.write8(at + common::DEVICE_STATUS, 0);
-        if wires.regs.read8(at + common::DEVICE_STATUS) != 0 {
-            return Err(Refusal::ResetUnanswered);
-        }
         wires.set_status(status::ACKNOWLEDGE);
         wires.set_status(status::DRIVER);
 
m18-version-1.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -320,9 +320,6 @@
     /// wherever it is offered. Nothing the device did not offer is written
     /// (§2.2.1).
     pub fn accept(mut self, wanted: u64) -> Result<Setup<R>, Refusal> {
-        if self.offered & VIRTIO_F_VERSION_1 == 0 {
-            return Err(self.wires.refuse(Refusal::NotVersion1 { offered: self.offered }));
-        }
         let features = self.offered & (wanted | VIRTIO_F_VERSION_1 | VIRTIO_F_ACCESS_PLATFORM);
         let wires = &mut self.wires;
         let at = wires.common;
m19-too-short.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -229,9 +229,6 @@
         if cap.offset as u64 + cap.length as u64 > window as u64 {
             return Err(Refusal::OutsideBar(what));
         }
-        if (cap.length as usize) < need {
-            return Err(Refusal::TooShort(what));
-        }
         if !cap.offset.is_multiple_of(align) {
             return Err(Refusal::Misaligned(what));
         }
m20-misaligned.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -232,9 +232,6 @@
         if (cap.length as usize) < need {
             return Err(Refusal::TooShort(what));
         }
-        if !cap.offset.is_multiple_of(align) {
-            return Err(Refusal::Misaligned(what));
-        }
         Ok(Self { at: cap.offset as usize, bytes: cap.length as usize })
     }
 }
m21-split-bars.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -197,9 +197,6 @@
         let notify = first(CFG_NOTIFY, "NOTIFY_CFG")?;
         let isr = first(CFG_ISR, "ISR_CFG")?;
         let device = first(CFG_DEVICE, "DEVICE_CFG")?;
-        if [notify, isr, device].iter().any(|cap| cap.bar != common.bar) {
-            return Err(Refusal::SplitAcrossBars);
-        }
         Ok(Self { common, notify, device })
     }
 
m22-status-32bit.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -253,7 +253,7 @@
 impl<R: Registers> Wires<R> {
     fn set_status(&mut self, bit: u8) {
         self.status |= bit;
-        self.regs.write8(self.common + common::DEVICE_STATUS, self.status);
+        self.regs.write32(self.common + common::DEVICE_STATUS, self.status as u32);
     }
 
     /// Give the device up (§3.1.1), and hand the reason back.
m23-enable-before-vector.patch
--- a/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/pci.rs	2026-10-09 13:22:30
@@ -409,8 +409,8 @@
         }
         let doorbell = self.wires.notify.at + into as usize;
 
+        self.wires.regs.write16(at + common::QUEUE_ENABLE, 1);
         let mut bound = self.bind(common::QUEUE_MSIX_VECTOR, entry, Source::Queue(index))?;
-        bound.wires.regs.write16(at + common::QUEUE_ENABLE, 1);
         bound.doorbells.push((index, doorbell));
         Ok(bound)
     }
m24-in-flight-overlap.patch
--- a/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
+++ b/toyos-virtio/src/queue.rs	2026-10-09 13:22:30
@@ -284,11 +284,6 @@
         let mut writable = 0u32;
         for (nth, buffer) in chain.iter().enumerate() {
             assert!(
-                !self.slots[first + nth].held,
-                "virtqueue {queue}: a chain at head {head} takes descriptor {}, which is in flight",
-                first + nth
-            );
-            assert!(
                 buffer.writable || writable == 0,
                 "virtqueue {queue}: the chain at head {head} has a device-readable element after \
                  a device-writable one"
m25-netstack-head-counted-not-fatal.patch
--- a/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
+++ b/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
@@ -130,9 +130,17 @@
 /// writable ones, and an index past what was made available. Nothing drives
 /// this NIC from there, and this dies where it can be read.
 fn finished(rings: &mut Virtqueue<Grant>) -> Option<Used> {
-    rings
-        .poll_used()
-        .unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}"))
+    let mut refused = 0u32;
+    loop {
+        match rings.poll_used() {
+            Ok(done) => return done,
+            Err(toyos_virtio::queue::UsedRefusal::Head(_)) => {
+                refused = refused.saturating_add(1);
+                continue;
+            }
+            Err(why) => panic!("netstack: this NIC cannot be driven on — {why}"),
+        }
+    }
 }
 
 /// The bound the capability walk rests on, asked once before the walk.
m26-netstack-no-chain-counted-not-fatal.patch
--- a/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
+++ b/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
@@ -130,9 +130,17 @@
 /// writable ones, and an index past what was made available. Nothing drives
 /// this NIC from there, and this dies where it can be read.
 fn finished(rings: &mut Virtqueue<Grant>) -> Option<Used> {
-    rings
-        .poll_used()
-        .unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}"))
+    let mut refused = 0u32;
+    loop {
+        match rings.poll_used() {
+            Ok(done) => return done,
+            Err(toyos_virtio::queue::UsedRefusal::NoChain { .. }) => {
+                refused = refused.saturating_add(1);
+                continue;
+            }
+            Err(why) => panic!("netstack: this NIC cannot be driven on — {why}"),
+        }
+    }
 }
 
 /// The bound the capability walk rests on, asked once before the walk.
m27-netstack-written-counted-not-fatal.patch
--- a/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
+++ b/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
@@ -130,9 +130,17 @@
 /// writable ones, and an index past what was made available. Nothing drives
 /// this NIC from there, and this dies where it can be read.
 fn finished(rings: &mut Virtqueue<Grant>) -> Option<Used> {
-    rings
-        .poll_used()
-        .unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}"))
+    let mut refused = 0u32;
+    loop {
+        match rings.poll_used() {
+            Ok(done) => return done,
+            Err(toyos_virtio::queue::UsedRefusal::Written { .. }) => {
+                refused = refused.saturating_add(1);
+                continue;
+            }
+            Err(why) => panic!("netstack: this NIC cannot be driven on — {why}"),
+        }
+    }
 }
 
 /// The bound the capability walk rests on, asked once before the walk.
m28-netstack-jumped-counted-not-fatal.patch
--- a/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
+++ b/userland/netstack/src/virtio_net.rs	2026-10-09 13:22:30
@@ -130,9 +130,17 @@
 /// writable ones, and an index past what was made available. Nothing drives
 /// this NIC from there, and this dies where it can be read.
 fn finished(rings: &mut Virtqueue<Grant>) -> Option<Used> {
-    rings
-        .poll_used()
-        .unwrap_or_else(|why| panic!("netstack: this NIC cannot be driven on — {why}"))
+    let mut refused = 0u32;
+    loop {
+        match rings.poll_used() {
+            Ok(done) => return done,
+            Err(toyos_virtio::queue::UsedRefusal::Jumped { .. }) => {
+                refused = refused.saturating_add(1);
+                return None;
+            }
+            Err(why) => panic!("netstack: this NIC cannot be driven on — {why}"),
+        }
+    }
 }
 
 /// The bound the capability walk rests on, asked once before the walk.
m29-i219-descriptor-read-as-32-bits.patch
--- a/toyos-i219/src/lib.rs	2026-10-09 13:22:30
+++ b/toyos-i219/src/lib.rs	2026-10-09 13:22:30
@@ -1396,7 +1396,7 @@
     fn reclaim_tx(&mut self) {
         while self.tx_clean != self.tx_next {
             let at = OFF_TX_RING + self.tx_clean * tx_desc::BYTES;
-            let word = self.dma.read64(at + 8);
+            let word = self.dma.read32(at + 8) as u64;
             let status = ((word >> tx_desc::STATUS_SHIFT) & tx_desc::STATUS_MASK) as u8;
             if status & tx_desc::STATUS_DD == 0 {
                 return;
m30-i219-register-written-as-16-bits.patch
--- a/toyos-i219/src/lib.rs	2026-10-09 13:22:30
+++ b/toyos-i219/src/lib.rs	2026-10-09 13:22:30
@@ -1385,7 +1385,7 @@
         // tail pointer", so the descriptor and the frame both have to be
         // visible before the tail moves over them.
         self.dma.publish();
-        self.regs.write32(regs::TDT, self.tx_next as u32);
+        self.regs.write16(regs::TDT, self.tx_next as u16);
     }
 
     /// Take back every transmit descriptor the device has written back.

@Japabu Japabu changed the title toyos-virtio: the virtio 1.2 PCI transport and split virtqueue as one pure crate, bounded against the device and host-tested; netstack's virtio-net is its first client toyos-virtio: the virtio 1.2 PCI transport and split virtqueue as one pure crate, with netstack's virtio-net its first client; toyos-device-memory: the one boundary toyos-i219 and toyos-virtio are written against Oct 9, 2026
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Answer to the round-1 review, at 459e1f3db

origin/main 965e62bb1 (#794) is merged in. Gates and exits are in the body; the mutation patches are in the round-2 mutation comment.

BLOCKERs

  1. Two Registers, two DmaBuffers. Fixed, by the owner's ruling of 2026-10-09 ("One shared interface (Recommended)", quoted whole in the body and the commit). toyos-device-memory declares both traits and the barrier order once; toyos-i219 and toyos-virtio are written against it and their own copies are gone; netstack implements it once, in device.rs, so the two fences are one body. The issue is deleted with its exit met. Measured: toyos-i219 88 of 88 (88 on main), toyos-virtio 33 of 33, the host gate exit 0.
  2. Three refusals counted and dropped. Fixed. finished ends the driver on each of the four by its own UsedRefusal; rx_refused, TxQueue::refused, reported, VirtioNet::report and card.rs's arm are gone. Four netstack tests, one per refusal. Your mutation, run four ways (m25 to m28): each of Head, NoChain, Written, Jumped returned to a counted drop is red, test exit 101, at exactly the test that names it. The cost, no network until the next boot, is in the body and filed as issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md.
  3. NO_VECTOR shipped for the test. Fixed: it is stub.rs's private constant.

NOTEs

  • driver_ok() after a refusal. Fixed in the type: every Setup step takes it by value and a refusal does not return it.
  • Device-structure reads at other widths. Not built; the body says they are left for the client that needs them.
  • Written on a device-readable chain. Recorded in stage 1 of issues/every-driver-is-still-in-the-kernel.md as owed by the client that first ends a device there.
  • config_read(..).unwrap_or(0) and the unmasked pointer. Filed: issues/the-virtio-capability-walk-reads-a-refused-configuration-read-as-zeros.md. Its PCI citation is from memory and says so.
  • The walk's move. Recorded in the same stage 1.
  • Body's stage names. Corrected to the two issue files and their stages.
  • bar_map_again. Struck; the three tests that run the driver are named and were run by name, 3 of 3.

Where this round differs from your answers

  • T14. You said no reading is owed. The first BLOCKER's fix changes toyos-i219, so one is now. It is staged on wt/toyos-arm-u1-t14 (795c67656, no pull request) and not taken; the driver paths there are byte-identical to this head.
  • AArch64 barriers. As you wrote: read, not measured. One body now.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 on the Intel driver written against the shared boundary (the orchestrator's reading). One boot of the outbound image staged from the measurement branch wt/toyos-arm-u1-t14 at 795c67656, which is 459e1f3db with the outbound rows merged; the driver-path diff between the two is empty. Worktree clean before and after; the image's sha256 checked against the request in the command that flashed it. Judged with --metal --metal-readback <dir> boot:outbound: exit 0, 2 passed, 0 failed (outbound_router, outbound_internet).

main's driver at #782 this head
link up after the driver came up 2721 ms 2761 ms
lease after netstack came up 13337 ms 13322 ms
both anchors connected connected
ring line full=7 wake_armed=2 wake_taken=1 untaken=1 stranded=0 descriptors_sent=73 wire_sent=73 taken=60 full=6 wake_armed=2 wake_taken=1 untaken=1 stranded=0 descriptors_sent=73 wire_sent=73 taken=60
cannot be driven on / offered to a transmit ring that refuses it 0 0

SAME: every datagram taken, every descriptor sent reached the wire, none stranded, no refusal line, no anomaly line about frames left in the ring. full counts askings that found no room, not fills, and differs by one between two boots of the same job.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 2, at 459e1f3db against origin/main 55e4e1dd2

Read from git objects: git diff e9a76c948 459e1f3db (round 2's one commit; the merge e9a76c948 changes nothing under toyos-virtio, toyos-i219 or userland/netstack/src), every file it changes whole, the body, the two round-2 comments, the r2-* step logs, the mutation logs and the T14 readback. No build, test or QEMU run.

Net lines (git diff --shortstat origin/main...459e1f3db): 25 files, +3164 -831. Production +663: toyos-virtio +893, toyos-device-memory +91, netstack device.rs +114, virtio_net.rs -357, netstack i219.rs -43, toyos-i219/src/lib.rs -36, card.rs +1. Tests +1542: the virtio crate's stub and tests 1456, the Intel crate's 76, netstack's 10. Round 2 alone: +728 -532. The growth round 1 accepted is smaller by 23 lines and nothing new in it is deletable.

Round 1's BLOCKERs

  1. Two Registers, two DmaBuffers: CLOSED. git grep -E 'trait (Registers|DmaBuffers)' 459e1f3db outside the kernel answers toyos-device-memory/src/lib.rs:38 and :74 and nothing else; fence( in netstack is userland/netstack/src/device.rs:115 and :119 and nowhere else. The ruling is quoted with its question in the body and the commit. The issue is deleted, its slug has no hit left in the tree, and its durable line is the new crate's header. Host gate exit 0, 78 steps, started ten seconds after the commit (r2-ci-host.log).
  2. Three refusals counted and dropped: CLOSED. finished (userland/netstack/src/virtio_net.rs:132) is one unwrap_or_else(panic!) over all four; rx_refused, TxQueue::refused, reported, VirtioNet::report have no hit at the head and card.rs:178 is an empty arm. m25 to m28 read in their logs: each is 29 passed, 1 failed, and the one failure is the test named for that refusal, "test did not panic as expected".
  3. NO_VECTOR shipped for the test: CLOSED. toyos-virtio/src/stub.rs:46, private; pci.rs has none.

BLOCKER

  • toyos-i219/src/stub.rs:2535-2536 — the Intel model's publish and observe are {} and nothing in toyos-i219 reads whether either was called, so the body's "What is measured is the order of the calls, in each crate's host suite (m06, m07)" is true of toyos-virtio only: m06 and m07 are both patches to toyos-virtio/src/queue.rs. This commit makes the order the shared contract and writes into toyos-i219/src/lib.rs:32-38 that "publish is rung before a tail register is written" and "observe is taken after the load that found DD set"; for that crate no test can fail on either sentence, and on x86 no boot can either, the T14 included. Mutations I expect to stay green, 88 of 88, each alone: delete self.dma.publish(); at toyos-i219/src/lib.rs:1387 (before TDT), at :1306 (before RDT), and self.dma.observe(); at :1235 (receive, between the two loads of the descriptor). Owed: the implementer runs the three. If they are green, either the model records the two barriers and each mutation reds a toyos-i219 test named for the order, as toyos-virtio/src/stub.rs:465-470 does (test files only, so the T14 reading below still carries), or the body's sentence is withdrawn and the gap is an issue with an owner and that exit. Stage 6 of issues/toyos-runs-on-arm64.md already owes this kind of test for NVMe and xHCI and does not name the I219.

NOTE

  • toyos-device-memory/src/lib.rs:60-73 — the contract states two orders and is silent on the third a ring driver meets: a store followed by a load of what the device wrote in answer (publish an index, then read the device's suppression word to decide whether to notify; §2.7.10 once a client stops always notifying, or accepts VIRTIO_F_EVENT_IDX). Neither a release nor an acquire barrier orders that pair, on x86 either. Neither driver here needs it: toyos-virtio always notifies and leaves avail.flags 0. One sentence saying the two barriers do not give that order, so the client that first needs it adds a method rather than reaching for publish. For the two orders it does state, the wording is exact enough: what each guarantees, and the store or load on either side of the call.
  • toyos-device-memory/src/lib.rs:38, :74 — what the type enforces is the width and nothing of the order: Registers and DmaBuffers are independent, so a register write that points the device at unpublished memory compiles. toyos-virtio closes that inside itself for the notification (Published is the only thing Live::notify takes); toyos-i219 holds it by placement alone, which is the BLOCKER above. No change asked of the boundary: a token threaded through two traits would be a design made for one caller.
  • Body: "toyos-device-memory is the one boundary every userland driver is written against" is wider than the tree. userland/diskserver/src/nvme.rs:452 and :474 and userland/soundserver/src/virtio.rs:176-205 carry their own barriers over Window and are written against no boundary; issues/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md owns moving them. The ruling as given covers it; the sentence should say two crates are on it.
  • Body: "The T14 reading is staged, not taken" is no longer true; see below.

What was checked and holds

  • The Intel crate's change is the declaration moving out. toyos-i219/src/lib.rs, every non-import hunk: the header's paragraph on the boundary rewritten and one added on widths and barriers (:21-38); the two trait declarations deleted (old :109-123, :138-169); and after that only regs.read to read32, regs.write to write32, dma.read to read64, dma.write to write64, 66 lines, with no line added, removed or reordered between them and all six barrier calls where they were. pch.rs and phy.rs: ten renames. stub.rs and tests.rs: the trait implementations, with a panic on every width the part does not have. 88 tests in r2-i219-first.log and again in the host gate at the head. m29 and m30 read: red at the model's width panic.
  • netstack's Intel wrapper changed in one thing that is not an import: the BAR mapping and the DMA region moved from inside Bar and Grant to two fields of Nic (userland/netstack/src/i219.rs:87-91), because the shared Bar and Grant are Copy windows that own nothing. Drop order is main's: the driver first, whose Drop writes CTRL_EXT (toyos-i219/src/lib.rs:785), then the mapping, the region, the claim; on a refused I219::open both mappings are locals that outlive the call. The type no longer ties a window to its mapping; the field order and its comment do, as Nic::frames already did on main.
  • Setup is complete. Offer::accept, config_vector, bind, enable, device_read8 and driver_ok all take self; neither Offer nor Setup is Clone; Live's fields are private and driver_ok is its only constructor. Every refuse is on a path that returns Err without the value. No Live is reachable past a refusal. Nothing holds "does not compile" and nothing should: it is a move in six signatures, which a reader checks, and a compile-fail harness would be a gate for what reading sees.
  • The boundary crate owes no test. It has no body: two traits. What is testable is each client's use of the order, which is the BLOCKER's subject, not this crate's.
  • Nothing in the crate is dead, and nothing is one driver's in the sense that matters: read8, read16, write8, write16 of a register and the 16- and 32-bit grant accesses are virtio's alone and read64 is the Intel driver's alone, but a width is not a capability, and Clock and Interrupts, which are one driver's, stayed out. The name says what it declares.
  • The barrier on AArch64 is read, not measured, as the body says: one body now, fence(Release) and fence(Acquire), and nothing runs netstack on AArch64 before stage 7.
  • Issues. Both new files are kind: defect, status: open, with an owner that exists and an exit a test can fail; every file they cite exists, and section 3 of issues/the-lan-is-not-yet-production-grade.md is the split the first one names. Stage 1 of issues/every-driver-is-still-in-the-kernel.md carries the walk's move and the len measurement. The deleted finding is closed as issues/README.md closes one.
  • Mutations, nine read in their test logs at head.txt = this head: m12, m13, m23 (rewritten for Setup by value), m25 to m28, m29, m30. Each red where the summary says.
  • Gates, from the logs: host 78 steps all green; --build-only exit 0; netstack_socket_churn, libc_sockets, iommu_virtio_platform 3 of 3; the whole suite 37 passed of 37, those three in it by name, 1-minute load 47.14 before and 59.27 after.

The T14 reading

Taken, and judged here from the readback: SAME.

What it had to show to land was exactly that list: both times within the spread of #782's, both anchors connected, full, wake_armed and wake_taken at least 1, descriptors_sent == wire_sent, stranded=0, neither refusal line. full=0 would have been nothing read. The body should carry the command, the exits and those lines, with no address from the log.

At the head that lands

SEND BACK

Japabu and others added 3 commits October 9, 2026 13:48
…e driver to their order

The Intel model's `publish` and `observe` were empty, so no test in the
crate could fail on the order its header states. Three mutations at
459e1f3, each alone, stayed green at 88 of 88: `publish` deleted before
`TDT`, before `RDT`, and `observe` deleted in receive between the two
loads of a descriptor.

The model now records every store, load and barrier the driver makes on
the grant and every register it writes, as `toyos-virtio`'s stub does.
Two tests read that trace over frames moved both ways on both parts: a
tail register is written only after a `publish` that follows the last
store into the grant, and after the load that first finds `DD` in a
descriptor nothing is loaded before an `observe`. Test files only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…load of the device's answer

The contract stated two orders and was silent on the third a ring driver
meets once it reads a suppression word or accepts VIRTIO_F_EVENT_IDX.
Neither driver written against the boundary makes that pair, so the
contract says who adds the method. A doc comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3: answer to the round-2 review, at 90e9f6451

origin/main 55e4e1dd2 (#795) is merged in, no conflict: it brought two issues/ files, issues/toyos-runs-on-arm64.md and src/build.rs. #793 has not landed.

BLOCKER: the Intel model's publish and observe were {}

Run as predicted, then closed by tests.

Control, at 459e1f3db, each mutation alone (r3-mutations/control-summary.txt):

m31-i219-no-publish-before-tdt build_exit=0 test_exit=0 tree=clean result=[88 passed; 0 failed 0 passed; 0 failed ] red=[]
m32-i219-no-publish-before-rdt build_exit=0 test_exit=0 tree=clean result=[88 passed; 0 failed 0 passed; 0 failed ] red=[]
m33-i219-no-observe-in-receive build_exit=0 test_exit=0 tree=clean result=[88 passed; 0 failed 0 passed; 0 failed ] red=[]
DONE

All three green, 88 of 88 (the second pair in each result is the doc-test run).

The fix, test files only. toyos-i219/src/stub.rs records every store, load and barrier the driver makes on the grant and every register it writes, in one trace, as toyos-virtio's stub does. Two tests read it over frames moved both ways, on both parts, eight seeds each:

  • a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store
  • a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd

Each counts what it checked (TDT moved over a stored descriptor once per frame sent; one DD-finding load per frame received and per descriptor sent), so neither passes on an empty trace.

At 90e9f6451, each mutation alone (r3-mutations/head-summary.txt):

m31-i219-no-publish-before-tdt build_exit=0 test_exit=101 tree=clean result=[89 passed; 1 failed ] red=[tests::a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store ]
m32-i219-no-publish-before-rdt build_exit=0 test_exit=101 tree=clean result=[89 passed; 1 failed ] red=[tests::a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store ]
m33-i219-no-observe-in-receive build_exit=0 test_exit=101 tree=clean result=[89 passed; 1 failed ] red=[tests::a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd ]
m34-i219-no-observe-in-reclaim build_exit=0 test_exit=101 tree=clean result=[89 passed; 1 failed ] red=[tests::a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd ]
DONE

m34 is mine, not the review's: the observe in the transmit reclaim. It has no control run; at 459e1f3db the call it deletes was to an empty function.

The T14 reading carries. git diff 459e1f3db 90e9f6451 -- toyos-i219/src/lib.rs toyos-i219/src/pch.rs toyos-i219/src/phy.rs userland/netstack/src/device.rs userland/netstack/src/i219.rs userland/netstack/src/card.rs is empty, exit 0. The review's list also names toyos-device-memory, which the first NOTE changes: five comment lines. With every comment-only line stripped from both sides, diff of toyos-device-memory/src/lib.rs exits 0, 26 lines each side.

NOTEs

  1. The third order. One sentence in DmaBuffers' contract: neither barrier orders a store against a later load of what the device wrote in answer, no driver here makes that pair, and the first that does adds a method and does not reach for publish.
  2. The type enforces the width and nothing of the order. No change asked, none made. For toyos-i219 the placement is now held by the two tests above.
  3. Body, "every userland driver". Now: two crates are written against it; diskserver's NVMe driver and soundserver's virtio still carry their own barriers, and the issue that owns moving them is named. Not changed: toyos-i219/Cargo.toml's comment on the dependency says the same wider thing, and the brief for this round is test files only.
  4. Body, the T14. The section carries the reading as taken: the command, exit 0, 2 passed, the table, no address.

Stage 6 of issues/toyos-runs-on-arm64.md is not given the I219. What its list owes NVMe and xHCI is a test of the order that x86's TSO hides; for the I219 the two tests above are that test, on the host tier. What stays unmeasured is the barrier instruction in netstack's device.rs, which issues/assembly-outside-an-arch-module-in-userland-and-guest-probes.md already owns.

Gates at 90e9f6451

Gate Exit
cargo test -p toyos-i219: 90 passed 0
cargo test -p toyos-virtio -p toyos-device-memory: 33 and 0 passed 0
cargo test in userland/netstack: 30 passed 0
cargo run -- --ci host: 78 steps, all green 0
cargo run -- --build-only 0

No guest run at this head: the merge brought no netstack source. Round 2's guest runs are at 459e1f3db and the body says so.

The patches

m31-i219-no-publish-before-tdt.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1384,7 +1384,6 @@
         // §7.2.4.1: "The 82574 NEVER fetches descriptors beyond the descriptor
         // tail pointer", so the descriptor and the frame both have to be
         // visible before the tail moves over them.
-        self.dma.publish();
         self.regs.write32(regs::TDT, self.tx_next as u32);
     }
 

m32-i219-no-publish-before-rdt.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1303,7 +1303,6 @@
         self.rx_tail = tail;
         // §7.1.8: the device fetches on the tail write, so the descriptors have
         // to be there before the write is.
-        self.dma.publish();
         self.regs.write32(regs::RDT, tail as u32);
     }
 

m33-i219-no-observe-in-receive.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1232,7 +1232,6 @@
             }
             // §7.1.7.1: the descriptor was written back in a batch, so its
             // other fields are read only after the load that found `DD`.
-            self.dma.observe();
             let word = self.dma.read64(at + 8);
             self.rx_next = (index + 1) % RX_RING;
             self.rx_budget -= 1;

m34-i219-no-observe-in-reclaim.patch

--- a/toyos-i219/src/lib.rs
+++ b/toyos-i219/src/lib.rs
@@ -1401,7 +1401,6 @@
             if status & tx_desc::STATUS_DD == 0 {
                 return;
             }
-            self.dma.observe();
             self.tx_clean = (self.tx_clean + 1) % TX_RING;
             self.counters.sent = self.counters.sent.saturating_add(1);
             // Stranded descriptors are the ring's oldest, so each one taken

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 3, at 90e9f6451

Read from git objects and the step logs: git diff 459e1f3db 90e9f6451 less the merge of #795, the three files it changes whole, the body, the round-3 answer, the r3-* logs and every log under r3-mutations/. No build, test or QEMU run. main has moved since the head was measured: #793 is merged, main is 1621281ae, and this head carries 55e4e1dd2.

Net lines (git diff --shortstat 55e4e1dd2...90e9f6451): 25 files, +3332 -839. Production +668, five more than round 2, all five comment lines of the contract. Tests: this round is +163 -8 in toyos-i219/src/stub.rs and tests.rs. Nothing in it is deletable: the trace is read by both tests and by nothing else, and stub and tests are #[cfg(test)] modules (toyos-i219/src/lib.rs:113-116), so nothing ships.

Round 2's BLOCKER

The Intel model ignored both barriers: CLOSED.

  • Control, r3-mutations/control-head.txt = 459e1f3db: m31, m32, m33 each alone, build exit 0 with Compiling toyos-i219 in each build log, test exit 0, running 88 tests, 88 passed, 0 failed. The prediction held: nothing in the crate could fail on either order.
  • Head, r3-mutations/head-head.txt = 90e9f6451: m31 to m34 each alone, build exit 0 and recompiled, test exit 101, 89 passed, 1 failed. Read in each test log, not from the summary: m31 panics at tests.rs:1391 on TDT (0x3818) written over the store at grant offset 4096, the first transmit descriptor; m32 at the same line on RDT (0x2818) over the store at offset 0; m33 at tests.rs:1430, offset 0x8 loaded a second time after the load that found DD there; m34 at the same line, a receive status loaded after the load that found DD at 4104, the first transmit status. Each is the test named for that order and no other test moves. All four patches still apply at the head.
  • m34 without a control: accepted. At 459e1f3db the only observe the suite can call is fn observe(&self) {} and no test reads a trace, so the patch deletes a call to an empty body; that is read off the diff, and a run would measure the same sentence.

Do the two tests state the two orders, and not "a barrier somewhere before"? Yes, for both rings.

  • a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store: any store into the grant sets a mark, only publish clears it, and a write of RDT or TDT with the mark set is red. So a publish moved above either descriptor store, or between the two, is red, and so is either arming's publish deleted (lib.rs:1005, :1020), since the trace starts at open. It is not vacuous: TDT must move over a stored descriptor exactly sent + 1 times and RDT more than once, per part and seed.
  • a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd: after the first load that finds DD in a status word since the driver last stored it, any load from the grant before an observe is red, and so is a DD found and never followed by one. An observe moved above the load is red. The count is exact: one such load per frame received and per descriptor sent.
  • What the second holds is that order and nothing past it: that nothing is loaded between the DD load and the observe. It does not hold that the length the driver parses comes from the load after the observe (lib.rs:1236 deleted would, I expect, stay green: the model's memory answers both loads alike). Not a finding: status, length and errors are one aligned 64-bit word, so a driver that parsed the first load would have loaded nothing the word covers before the barrier, and the shared contract asks no more. The frame's bytes are the caller's loads, after poll_rx returns, and the body says the trace does not see them.

Test files only: reproduced. git diff 459e1f3db 90e9f6451 -- toyos-i219/src/lib.rs toyos-i219/src/pch.rs toyos-i219/src/phy.rs userland/netstack/src/device.rs userland/netstack/src/i219.rs userland/netstack/src/card.rs is empty, and so is the same diff over toyos-i219/Cargo.toml, toyos-virtio, userland/netstack, Cargo.lock, kernel, toyos and toyos-abi. toyos-device-memory/src/lib.rs with every comment-only line stripped from both sides: diff exit 0. The T14 reading at 459e1f3db's source carries to this head.

BLOCKER

None.

NOTE

The round-2 NOTEs and the questions put

  • The third order: the sentence at toyos-device-memory/src/lib.rs:75-78 says what was asked, and says who adds the method.
  • The body: "two crates are written against it" with the two drivers that are not and the issue that owns them, which exists; the T14 section carries the command, the exit, 2 passed and the table, with no address.
  • Stage 6 of the ARM track is not given the I219: right. What stage 6 owes NVMe and xHCI is a test that reds on the wrong order, because nothing below a guest holds theirs. The I219's order is now held on the host tier, the cheapest that reaches it, and no stage puts an I219 on virt. What is left unmeasured is what fence emits on AArch64, and the issue above owns that by its exit.
  • toyos-i219/Cargo.toml's "every userland driver": a source comment's wording, not raised and not owed before landing. toyos-i219/src/lib.rs:23-24 says the same, inside the fenced file, so changing the manifest alone would leave it standing. If either is ever edited, a comment changes no built byte and the carry would rest on the same comment-stripped diff exiting 0; leaving both for the change that next touches the crate costs nothing.
  • Gates at the head, from the logs, r3-head.txt = 90e9f6451: toyos-i219 90 passed with both order tests ok by name; toyos-virtio 33 and toyos-device-memory 0; netstack 30, the four virtio_net refusal tests ok by name; host gate exit 0, Host: 78 step(s), all green, its only FAILED lines inside the control steps that exist to fail; --build-only exit 0, Build finished.
  • No guest run at this head: accepted for the review, not for the landing. Since 459e1f3db the delta is two #[cfg(test)] files, five comment lines, issues/, and calc, snake and doom are on the AArch64 ROOT: the list that left them out is deleted, and the owner's word of 2026-10-09 ships doom there #795's deletion in src/build.rs of a list whose every row was AArch64's, so the x86-64 image's programs are the ones round 2's guest rows booted. The measurement that counts is the one below.

At the head that lands

#793 is in main now and changed userland/netstack/src/main.rs, mdns.rs and tests/toyos.rs: netstack_socket_churn and libc_sockets now also wait for the mDNS claim line. Nothing has yet run this branch's virtio driver under that wait; the checks on the merge with 1621281ae are the first measurement of it. GitHub reports the pull request mergeable against it, and the two change no file in common.

LAND

@Japabu
Japabu marked this pull request as ready for review October 9, 2026 12:01
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI at 90e9f6451, on the merge with main at 1621281ae (#793 in it), read from each job's own log (the orchestrator).

  • host: [ci] Host: 79 step(s), all green; toyos-i219's two order tests ok by name (a_tail_register_is_written_only_after_a_publish_that_follows_the_last_descriptor_store, a_descriptors_fields_are_read_only_after_an_observe_that_follows_the_load_that_found_dd); netstack's four a_used_*_ends_the_driver tests ok by name.
  • guest / suite: test result: ok. 37 passed, 37 total; netstack_socket_churn, libc_sockets and iommu_virtio_platform each PASS by name, the first two now also waiting on the mDNS claim line: the first run of this driver under that wait; no FAIL line.
  • The fence: git diff 459e1f3db 90e9f6451 over the six driver-path files is empty, so the T14 reading above (SAME) carries.
    git merge-tree against main at 1621281ae exits 0 with no conflict. toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome, RFC 6762 §8.1's bound on what a peer's messages cost, and the link's return in both callers #793 landed after this head was measured locally; netstack's count on the merged tree is the check's, not the body's 30.

@Japabu
Japabu added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit bd5d0ad Oct 9, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-arm-u1 branch October 9, 2026 12:36
Japabu added a commit that referenced this pull request Oct 9, 2026
…Me (#797), into the one-boot merge

No conflict. In tests/toyos.rs main's two hunks are the
`nvme_disk_keeps_log_and_home` machine test's row in MACHINE_TESTS and its
arm and function after `run_machine_test`, both guest tests and neither in
the METAL table or near `shared_metal`. tests/common/qemu.rs moved on main
only; this branch does not touch it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Japabu added a commit that referenced this pull request Oct 9, 2026
…d the installed NVMe image (#797), into the IORT decode and the SMMUv3 and ITS encodings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Japabu added a commit that referenced this pull request Oct 9, 2026
…796), into the entropy stage

No hunk conflicted. The Headless shape main brings (one NVMe disk, no
stick) already carries `rng: false`, and no Shape literal it adds lacks
the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
github-merge-queue Bot pushed a commit that referenced this pull request Oct 9, 2026
…nd the two debug timing rows ride shared-debug: 23 boots to 20 (#799)

Stage 3b of step B of the one-boot plan for the T14's metal rows: merge
boots. Stage 1 was #785, stage 2 #789, stage 3a #794. Head `01fd3aedd`,
which holds `main` at `558283168` (#797, with #796 under it) by a merge
with no conflict: in `tests/toyos.rs` `main`'s two hunks are the
`nvme_disk_keeps_log_and_home` machine test's row in `MACHINE_TESTS` and
its arm and function after `run_machine_test`, guest tests both, neither
in the `METAL` table nor near `shared_metal`; `tests/common/qemu.rs`
moved on `main` alone, and this branch does not touch it. `--metal
--list` is unchanged by it.

`--metal --list` before (`965e62bb1`): 61 registrations and 232 shared
members over **23** boots. After: 61 and 232 over **20**. `shared`,
`ccorpus` and `testcases-debug` are gone; no test is.

**The T14 read `2c7e1be1a`: 266 passed, 1 failed.** The red was this
merge's own: see the first decision. This head is staged and owed a
reading: see "The T14".

## What changed, per decision

- **`endowment_denied` holds a link against toybox's policy table only
where the link's target has a `[programs]` row.** The red at
`2c7e1be1a`: `"/system/bin/134_double_to_signed" is a link to something
else: a second multicall binary needs its own policy table, not this
one`, after every earlier phase passed. The member walks `/system/bin`
and asserted every link lands on `/system/bin/toybox`; the merged ROOT
carries the C corpus, whose 137 cases are links to the comparator
`test_rs_ccheck`, which reads `argv[0]` so the kernel records each run
under the case's name. On `shared`'s own ROOT there were none. **The
walk was too wide, and the corpus is not a second multicall binary in
the sense the claim is about.** What a link carries is its target's row:
the supervisor's `declared` follows one link and matches a row by its
whole path, and a target no row names is answered `NotDeclared`, which
the caller spawns itself with what it holds; the build's
`unnamed_program` says the same of every harness binary ("a harness
binary has none, holding only what its spawner moved in"), and
`test-runner` spawns every job directly in any case. A policy table
limits what a row grants; a binary with no row is granted nothing by
one. So the walk skips a link whose target the image's manifest names no
row for, matched by the whole path as the supervisor matches it (its own
parse of the manifest, as before), and a second binary that *does* have
a row behind a link reds as before. The applets line counts the links it
passed over. The corpus's shape is unchanged: one comparator under one
link per case is what lets a stick say which case failed. Nothing of the
endowment policy for C programs is decided here: the corpus's cases hold
what `test-runner` gives every job, as on `ccorpus`' own boot.
- **`shared` and `ccorpus` ride `testcases`.** Its list is the ten jobs
its rows name, then the 85 discovered Rust binaries, then the 137 C
cases, then the three bounds programs (`LAST_MEMBERS`), then `reboot`:
234 jobs, since `fault_gates` is a row's job and a member and runs once,
among the rows'. `c_corpus_metal` gives the corpus's part of that boot
and `shared_metal` puts the Rust members around it.
- **`testcases-debug` rides `shared-debug`.** `tlb_shootdown_waits` and
`trace_record_cost` name that boot, its kernel and its parameters, and
their two jobs run before its seven members.
- **A member says what it adds to its list's bound** (`metal::Member`),
where a shared boot said one number for all of its members: one boot now
carries Rust tests at 300 ms and C cases at 100.
- **A boot's last job ends its rows' jobs, and the members run behind
it.** This reverses what stage 3a built. `acpi_hold` gives up 54 000 ms
after boot (`UNTIL_MS`); behind the members it would have started 51.8
to 52.5 s in by the readings before the merge. On the T14 at
`2c7e1be1a`, before them, it started 43 095 ms into the kernel's clock
with 10.9 s left and exited 0. It also keeps the three bounds programs
the last jobs of the boot. **The compromise under it is filed, not
fixed**:
`issues/acpi-hold-gives-up-at-a-time-counted-from-boot-whatever-its-runner-was-given.md`,
owner the harness, exit the hold bounded from its own start or by the
bound its runner was given; it now carries the T14's start too.
- **Two jobs of one boot the kernel records under one name are
refused**, in `batches`, for every boot. The C corpus checked its own
cases; the merged list puts Rust members, C cases and rows' jobs under
one log, so the check is the boot's now and the corpus's own is deleted.
- **The judge prints a shared boot's members' time summed between each
member's own markers**, against what they add to the bound, and says
over how many: `its members took <ms> ms of the <ms> ms they add to the
list's bound, summed over the <n> of <all> with both markers`, and a
line `<k> without a start and an end marker, the first <job>` only where
there is one. It is a line a reader reads and reds nothing; no host test
holds it. On the T14: `9253 ms of the 40100 ms … summed over the 225 of
225`.
- **The record rows of `shared`, `ccorpus` and `testcases-debug` are
deleted** from the machine's record under `tests/metal/`. The three
`testcases-window` rows stay:
`issues/the-t14s-record-keeps-three-rows-of-a-boot-nothing-stages.md`
owns them and half its exit.
- **The allowance issue's exit is three parts**, by the orchestrator's
ruling on the round-1 review: (1) each allowance derived from the
members' sum between their own markers by one stated factor, the judge
redding a boot whose members' sum is past that share; (2) what a boot's
rows' jobs are given derived the same way from what they take, the judge
redding a boot whose rows' jobs end past their share; (3) `testcases`
reading both inside, the pair from the kernel's own lines, the margin to
the list bound beside them. None of it is built and the issue stays
open. Its present numbers are now the T14's reading of the merged boot,
beside the sum of the three boots it was.

## What the merged boots arm

`--metal --list` and the staging's own lines at `01fd3aedd`; the wait is
`metal::return_secs`' arithmetic. The T14 read the same pairs off the
kernel's own lines at `2c7e1be1a`.

| boot | jobs | `--bound-ms=` | `boot-deadline=` | hard-lockup bound |
`toyos-metal` waits |
|---|---|---|---|---|---|
| `testcases` | 234 | 100 100 (`main`: 60 000) | 200 200 (`main`: 120
000) | 100 100 (`main`: 60 000) | 501 s (`main`: 420) |
| `shared-debug` | 9 (`main`: 7) | 62 100, unchanged | 124 200,
unchanged | 62 100, unchanged | 425 s, unchanged |

100 100 is 60 000 for the rows' jobs, 88 members at 300 and 137 at 100.
No other boot's list, bound or deadline changes.

## The merged list against its bound, as the T14 measured it

`testcases` at `2c7e1be1a`, each part between its own markers, the
kernel's clock counted from its `Boot: complete (1135ms)` line; beside
it the sum of the three boots it was, three readings each (`testcases`
at `9e70cd2e3`, `473efea22`, `accbd79dd`; `shared` and `ccorpus` at
`eff8b20ee`, `d6d008e88`, `49e12f23b`).

| part | merged, `2c7e1be1a` | its parts apart | its share of the bound
|
|---|---|---|---|
| the boot, to its first job | 1 191 ms | 1 190 to 1 196 ms | |
| the rows' ten jobs | 41 924 ms, ending 43 115 ms in | 41 913 to 41 983
ms | 60 000 ms |
| the 225 members | 9 253 ms | 8 735 to 9 298 ms | 40 100 ms |
| the list's last record | 52 352 ms | 51 838 to 52 477 ms | 100 100 ms
|

Read per part, as
`issues/a-shared-members-allowance-sets-the-kernels-deadline-at-several-times-the-work.md`'s
exit reads: the members at about 4.3 times their work, the rows' jobs at
about 1.4 times theirs (`counters_metal` alone 32.7 s), the last record
47.7 s inside its bound, read by nothing. No part of that exit is met
and none is built: the issue stays open. Behind 42 s of rows' jobs the
members took 9 253 ms, inside what they took alone.

The loader at `2c7e1be1a`: ROOT read 9 301 ms, loader 13 069 ms, as the
measured rate predicted (30.5 ms/MiB to read, 7.1 to hash, a ROOT
partition of 305 MiB), under the firmware's 60 s. `shared-debug`: 176 ms
of members over 7 of 7, its last record 1 754 ms of 62 100 ms.

## Why each row can share its boot

Rows' jobs run first, so every row's own jobs see the machine
`testcases` gave them at stage 1: the same config, the same prefix, one
job at a time. What changes is what stands in the log, the page and the
census behind them. By what each judge reads:

| rows | what the judge reads | why 225 programs behind it cannot move
it |
|---|---|---|
| `blackbox_unclaimed_page`, `control_regs`, `ioapic_topology`,
`klogd_hosted`, `smp_roster_and_tsc_trail`, `pmm_accounting`,
`acpi_table_inventory`, `timer_calibration`, `pci_inventory`,
`loader_watchdog_arms`' control arm | the loader's file and kernel
records written before the first job | a kernel record is the kernel's
(`Readback::kernel` drops every program's line), and these are said at
boot |
| `wake_storm_cost`, `syscall_cost` | the job's exit record, and
`syscall_cost`'s own two lines | the first and fifth jobs of the list;
`exit_code` takes the lowest pid of a name, and a row's job is spawned
before any member |
| `audio_idle_suspend`, `hda_tone`, `hda_client_stall`,
`shipped_client_departures` | `job_window`: the log from the job's spawn
to its sessions' end, or to the next spawn | each window closes before
the hold. Two reads are boot-wide: no null sink, said at soundserver's
start, and no `repeated completion for free buffer`. The second gains no
stream: no member opens one on soundserver. `cpal_drop_unreleased` is
its own stream server (its header: "No sound is played and soundserver
is not involved"), and in the three `shared` readbacks soundserver says
nothing after its start |
| `counters` | `counters_metal`'s own lines by their `counters_metal
<phase>:` head; the idle second by stamp; no `acpi: legacy mode again`
in the boot | no member prints that head (none in six readings of the
member boots); the idle second is inside the job, which ends before the
first member starts; legacy mode returns only when the `acpi` claim's
holder goes, and no member holds or kills it (none in six readings) |
| `acpi_server_events`, `acpi_tables_loaded` | lines whose tag is
`acpiserver`, and boot records | a member's line carries the runner's
tag. The last count line is read with `rfind`: a boot that passes the
server's next interval prints a later one, also a count |
| `claim_reuses_its_remapping_entry` | every hand-over and release of
the I219, and every `iommu: irte` record whose source is it, counted:
two and two | no member claims a PCI function: `pci_reclaim` is on
`RUST_SKIP`, and six readings of the member boots carry no `handed over
on slot` |
| `domain_ends_below_the_host_bridges` | every `iommu: domain` record of
the boot | a domain a member's boot made is held to the same windows;
judged over the member boots' readbacks below |
| `crash_report_reads_no_kernel_memory` | three refusals `fault_gates`
stages, and no report line anywhere in the boot that carries a kernel
address's contents | members fault on purpose, and their reports are now
read too: a leak in any of them is the defect. Judged over `shared`'s
readback below |
| `irq_census_conservation` | the stop's census, off the page: every
device delivery on cpu0, every shootdown IPI counted by its issuer | the
census is the whole boot's, members included, and the rule is
conservation, not a count. Judged over the member boots' readbacks below
|
| `tlb_shootdown_waits` (to `shared-debug`) | its exit | every wait it
asserts is a floor (`elapsed >= FLOOR_NANOS`), which no load shortens.
The eleven self-tests run at init and on the first syscall of the boot
(`task_probes`, once), before any job |
| `trace_record_cost` (to `shared-debug`) | its exit and one printed
line | a million records written by one syscall with interrupts closed;
the number is printed and not recorded. On that boot each syscall pays
one atomic swap in `task_probes`; the flood is one syscall |

**The boot-wide judges, run over the member boots a machine has already
answered.** The `shared` and `ccorpus` readbacks of #794's third
reading, relabelled `testcases` and judged at `3055ab0d1` (`--metal
--metal-readback <dir>` with thirteen row names):

| readback | exit | |
|---|---|---|
| `shared`'s 88 members | 0 | 13 passed, `irq_census_conservation` (5
196 shootdowns, every delivery accounted for),
`domain_ends_below_the_host_bridges`, `acpi_tables_loaded` and
`crash_report_reads_no_kernel_memory` among them |
| `ccorpus`' 137 cases | 1 | 12 passed;
`crash_report_reads_no_kernel_memory` red for its premise, since
`fault_gates` did not run on that boot |

And the old readbacks under the new profile (`boot:testcases
boot:shared-debug` over stage 1's `testcases` and #794's
`shared-debug`): exit 1, every `testcases` row and every self-test row
PASS, 224 members and the two moved rows red as never run, which is what
those logs hold. `shared-debug` reads `its members took 176 ms of the
2100 ms they add`, the sum taken by hand above.

**The numbers each boot measures** are the boot's own
(`boot.testcases.complete_ms`, `panel_us`, `panel_max_us`): the kernel's
time to `Boot: complete`, before any job, and the panel's census, which
painted 10 times on `testcases`, `shared` and `ccorpus` alike. No row
riding these two boots records a number of its own.

**No row kept a boot for sharing's sake.** `mkdir_cap` and
`readdir_bound` keep theirs until stage 3c.

## The members that read machine-wide state

The red was a member reading something the merged ROOT changed. Each
other member whose source reads state another program moves, by `rg`
over `tests/toyos-rust-tests/src/bin` for directory walks, roster reads
(`SYS_SYSINFO` entries), the memory header and the log:

| member | what it reads | why the merged ROOT or population cannot move
its verdict |
|---|---|---|
| `std_fs` | `/system/bin`'s listing | asserts it non-empty and its own
binary in it: more entries cannot red it |
| `hierarchy_paths` | `/`'s listing against `ROOT_ENTRIES` | `/` is the
mount table, not ROOT's content; the corpus's `expect/` and binaries
land under `/system` |
| `toybox_file_tools` | its scratch directory for `.part` names; `/home`
for one name | each keyed on names it wrote itself ("other tests write
there in the same boot") |
| `empty_dir_stat` | `/tmp/empty_dir_stat_empty` | its own directory |
| `readdir_bound`, `mkdir_cap` | `/tmp`'s exact count; the VFS directory
cap, left full | the reason both ride boots of their own
(`testcases-readdir`, `testcases-mkdir`), unchanged here |
| `endowment_denied` | the roster in a 256-entry buffer, its own pid in
it; `ps`'s row count above zero | members run one at a time, so the live
set is the services and one member: `ps` counted 26 processes at
`2c7e1be1a`. Past 256 threads it would red "does not contain this
process", loud and misnamed; nothing near that |
| `kill_ends_every_wait` | the roster in 256 entries, one pid's state |
refuses past 256 by name |
| `process_tree`, `abuse_thread_name` | the roster in 1 024 entries,
filtered to names they spawned | keyed on their own names and pids |
| `audio_idle_suspend` | soundserver's threads in a 128-entry roster | a
rows' job: it runs before any member |
| `allocator_stress` | the memory header: used above zero and below
total | a sum of the whole machine against itself |
| `abuse_connect_flood` | the memory header before and after its own 32
connects, the growth under 32 MiB | the window holds its own syscalls;
ROOT is read into memory by the loader before the kernel starts, not in
the window. It is the first member, as on `main`'s `shared` |
| `shm_release_reclaims`, `handle_lifetime` | per-kind object counts |
their own headers: "per kind, and not the machine's free memory" |
| `inbox_log_post` | the log, until its own child's exit record | keyed
on its child's pid |
| `file_mtime` | its own file on `/log` | its own file |

No other member walks a directory it did not make, counts processes or
files, or reads a log line count. This is evidence for this list; a
member a later landing adds is held to the same question by its own
review.

## The log

The T14's `testcases` log at `2c7e1be1a`: 8 123 829 bytes, whole (no
part missing), where the sum of the boots it was predicted 8 095 744 to
8 130 458. Eight parts at logkeeper's 1 MiB a part; a fresh volume keeps
sixteen before `retire` deletes one of this boot's own continuations; a
part that is missing reds the whole boot by name (`bootlog::lost_parts`,
#785), unchanged.

## The T14

**At `2c7e1be1a`** (the orchestrator's reading; worktree clean before
and after, each image's sha256 checked in the command that flashed it):
three boots, each `toyos-metal` exit 0; judged with `--metal
--metal-readback <dir> boot:testcases boot:shared-debug`: exit 1, **266
passed, 1 failed**, `test_rs_endowment_denied` exit 101 as above.
Everything else as the request asked: the armed pairs 200 200 / 100 100
and 124 200 / 62 100 from the kernel's own lines; `acpi_hold` at the end
of the rows' jobs, exit 0; the bounds programs last; no bound fired;
each loader pass after the reset `DONE`; the members' line over 225 of
225. That reading is this change's negative control: the walk as it
stood, on a ROOT with the corpus's links, red.

**At `01fd3aedd`**: staged with `cargo test --test toyos-build --
--metal --metal-readback <dir> boot:testcases boot:shared-debug`, exit
2, which is "staged"; no machine touched. The images of `2c7e1be1a` were
gone already (the runner deletes each after its boot).

| boot | sha256 | bytes |
|---|---|---|
| `testcases` |
`d99c1440e52def650a5f9600cd75fe4283322db709d40f95fb12f181358003cd` | 429
916 160 |
| `shared-debug` |
`82c1d8aee120d02b9acee3af3245ec2f12d4d33abd81b664c1467cdd5299e43c` | 136
314 880 |
| `testcases-watchdog` |
`c296fb0c0b9b37e2617a1b371c442a78598c011436a320510cd5b818783f1ed4` | 119
537 664 |

The hashes stand in `request.txt` as `shasum -a 256` lines, and `shasum
-a 256 -c` over them answers OK for the three. The request names round
2's fourteen with this head's expected numbers, then: `PASS
test_rs_endowment_denied` with its line `applets: 15 links behind
/system/bin/toybox, 14 declared over-grants and no undeclared one; 137
links to a binary no row names` and the lines after it; and 267 passed.
Sent back by any FAIL or a count other than 267, an applets line with
other numbers, a bound or a `WEDGED`, a missing part, the hold at or
past 54 000 ms, another order, or a members' line over fewer than 225.

**A mutation boot, optional, staged beside it**:
`m4-a-second-binary-with-a-row-behind-a-link` (the patch is in the
round-3 comment) adds `bin/zz_second_multicall -> /system/bin/symbolize`
to `tests/testcases/system.toml`, `symbolize` having a row. Staged from
a never-pushed commit of it on `01fd3aedd` with `--metal
--metal-readback <dir> endowment_denied 134_double_to_signed`, exit 2,
the tree restored to `01fd3aedd` and clean after; one image of 134 217
728 bytes, sha256
`b6787c435359a87d68264f014fef286583667a9a73d8e93c8f91752b6db74d71`, its
config carrying both the C case's link to `test_rs_ccheck` and the
mutation's. Expected: exit 1, `test_rs_endowment_denied` red naming
`/system/bin/zz_second_multicall`: a second binary with a row behind a
link still reds. **No machine has run it**, and only a machine can: see
below.

## Where the tree differs from the design's text

- The design counts 24 boots to 21. `main` is at 23 (#794 was 25 to 23),
so this stage is 23 to 20.
- The design puts a boot's registered jobs first "in today's order" and
names no last job. Stage 3a built the last job behind the members; this
stage puts it back among the rows', on the measurement above.
- The design's merged boot arms "about 198 s"; the tree's formula gives
200 200 ms.
- Nothing of stage 4b is built or prepared.

## Gates, at `01fd3aedd`

Logs are kept beside the orchestrator's scratch as `r3-*`.

| command | exit |
|---|---|
| `cargo run -- --clippy` | 0, 24 invocations clean |
| `cargo test --test toyos-checks` | 0, 39 passed |
| `cargo test -p toyos-build --lib` | 0 |
| `cargo test --test toyos-build -- --metal --list` | 0, 61
registrations and 232 members over 20 boots |
| `cargo run -- --ci host` | 0, 78 steps green |
| `cargo run -- --build-only` | 0 |
| `cargo test --test toyos-build -- test_rs_endowment_denied` | 1, `No
test matches filter "test_rs_endowment_denied"` |
| `cargo test --test toyos-build -- --metal --metal-readback <dir>
boot:testcases boot:shared-debug` | 2, "staged": three images |
| the same with `endowment_denied 134_double_to_signed`, under `m4` | 2,
"staged": one image |

Host load when the gates started: 18.65 / 29.42 / 32.46.

**No guest runs `endowment_denied`, so the T14 is its only oracle.** The
guest suite's names are `MACHINE_TESTS` and `SCREEN_TESTS`; the
discovered Rust binaries and the C corpus are shared members, run only
by `--metal` and the interactive `--debug`, and the filter above is
refused for that reason. No QEMU boot carries a ROOT with the corpus's
links either: the guest stages C cases as `test_c_<case>` and no
comparator. The rest of the change is reached only through `--metal` and
`toyos-checks`; `guest / suite` is a required check and runs at the
landing head.

## High-risk checks

The harness's batching decides what the kernel's deadline is armed with
and which program's exit a verdict is read from; `endowment_denied` is a
security claim about endowments.

- **Negative controls**: round 2's three mutations of
`tests/common/metal.rs`, each red (101) in
`metal_rows_run_before_members_under_a_bound_the_members_widen` at
`tests/checks/metal.rs` 479, 480 and 499 (the round-2 comment);
`metal.rs` has not changed since. For `endowment_denied`: the T14's
reading of `2c7e1be1a`, the walk before the change on a ROOT with the
corpus's links, red; and `m4`, staged, owed a machine.
- **Independent oracle**: the T14, owed at this head. The supervisor's
`declared` and the build's `unnamed_program` are the rule the narrowed
walk follows, read, not run.

## The host check, and what it sees that reading cannot

`rows_run_before_members_under_a_bound_the_members_widen` is changed,
not added: the last job's place in a list rows and members share, a
bound summed over members of two allowances, and the refusal of two jobs
under one recorded name. No guest test is added or cut;
`endowment_denied`'s walk is narrowed, its claim kept. No dependency is
added. No program is changed.

## Size

`git diff --shortstat origin/main...01fd3ae`: 9 files, +266 −151.
`issues/`: 4 files, +97 −25, one new. `tests/checks/`: +29 −18. The
harness and the record: +128 −106 as before. `endowment_denied.rs`: +12
−2.

## Unsure of

- Whether the 266 that passed at `2c7e1be1a` pass again: one reading of
a boot of its kind.
- The order `read_dir` lists `/system/bin` in. On `m4`'s boot the walk
reds on the mutation's link whichever it meets first; that it passes
over the C case's link there holds only if it meets that one first. The
main boot's 137 is what shows the pass-over.
- `endowment_denied`'s roster read takes 256 entries and asserts its own
pid among them without first refusing a larger roster by name, as
`kill_ends_every_wait` does. Not this change's, and far from reached (26
processes).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant