Skip to content

S1 of the xHCI move into usbd: a FAT entry that grows its file is written a cache flush after its chain and data, held by a module test that stops the disk inside a flush epoch - #901

Merged
Japabu merged 7 commits into
mainfrom
wt/toyos-usbd-xhci
Oct 13, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Design accepted (comment 6138717771), with round 3's review answered as ruled. The build is staged below. S1 is re-staged as ruled (comment 6138751364's option 1): it is the two module tests the step-10 exit owes, alone, and each, pci@, several block services, the settled listing and lib/blockserve move into S3, where usbd's block@ is their second user. This is the design for step 10 of issues/the-kernel-is-small-interrupts-post-and-threads-wait.md: the whole xHCI driver moves out of the kernel and into /system/bin/usbd. Three other steps go with it:

  • step 6.4: xHCI's thread is usbd's;
  • step 4.4's tail: SYS_PARTITION_* and the supervisor's partition claims;
  • step 9's block half: the block layer, GPT, and the chain of BudgetExpired, DEADMAN and between_attempts.

The branch is S1, built, at c7201d375, with origin/main merged at bc8429e71 (section "S1, as built"). Round 2's review is answered point by point in the round-3 mapping comment, and round 1's in the round-2 one.

What this was read from: main at 858da4d42, which carries #893's sweep, #900's track text and #902's clean-room rule; the owner's input ruling (comment 6137234651); the rulings on round 1's questions (comment 6137620475); the rulings on round 2 (comment 6138559523); #899's accepted design (its round 6, accepted in comment 6138254697), for the input port; #904, for the track's input text; S0's T14 reading (below); and the specifications below, fetched from their publishers and pinned by hash in specs.txt. A page cited as "p." is the PDF's own page index in that file.

The rulings this design follows:

The rulings on round 2:

The rulings on round 3 (comment 6138717771, on the review):

S1, as built

Head c7201d375, on origin/main at bc8429e71:

  • 92c22a2c6: the FAT fix and its test;
  • 4238b8b7b and 8eb4cf09f: merges of origin/main;
  • 671e5c736: the reopen test, since taken out;
  • 7978cdecf: answer and its test out, and the issues as ruled;
  • 17ea52983: the FAT test under flush epochs, judged by toyos-fat32-check;
  • c7201d375: the FAT test's stop with every epoch on the disk held to a clean check and the append's bytes.

17ea52983 answered the S1 build review as ruled; c7201d375 answers round 2's (section "The final stop"). Net against origin/main (git diff --shortstat origin/main...HEAD: 7 files, +203 −45):

  • production +20 −14: diskserver +11 −13, fileserver +9 −1;
  • tests +129 −0 in fat.rs, and fileserver's dev-dependency (+5, and one Cargo.lock line);
  • issues +48 −31: one narrowed, one filed, and the track's two pointers moved.

The FAT append-and-sync test, and the flush it found missing

The test. an_append_stopped_with_any_writes_of_a_flush_on_the_disk_reads_back_the_last_sync is in system/fileserver/src/fat.rs.

  • It runs on the fatgen103 fixture that toyos-fat32-check's tests build byte by byte, not on a volume the driver wrote.
  • The server writes 3,000 bytes, syncs, appends three blocks, and syncs again. The disk under it, Traced, keeps every block it is written in its flush epoch: a flush opens the next epoch.
  • A stop is every epoch before one whole on the disk, plus any subset of that one epoch's writes. That is the disk's own model: it keeps no order between flushes, and has all of an epoch before any of the next. This append and sync give 70 stops.
  • At every stop, the image is judged first by toyos-fat32-check (now a dev-dependency), which is written from fatgen103 and shares no code with toyos-fat32. Then the driver mounts the image, and the file must read whole as either the 3,000 bytes the last sync held or the 15,288 the append made.
  • The complaints a stop may leave are allowed by name, and every other complaint is red:
    • LostChain, FsInfoFreeCount and FatMirror, as the review named them.
    • ChainTooLong, only on /a.txt at the last sync's 3,000 bytes, and only before the sync is whole on the disk. This is the window the module header names between a growing file's chain and its entry: an append's sync writes the chain and the data in one flush epoch and the entry in the next, so a stop between the two leaves the chain grown under the old entry. toyos-fat32's own a_stop_at_any_write_leaves_only_the_named_windows admits it as "the handle's own window a growth leaves until flush_meta". Ordering the chain before the entry chooses this window; the other order leaves ChainTooShort, which reads bytes that were never written. With the three-name list alone, the fixed code is red at its first stop past the chain: stopped in flush epoch 2 of 5 with writes 0b1 of its 6: /a.txt: DIR_FileSize is 3000 bytes, which needs 6 clusters, and the chain holds 30 (exit 101, log, patch, at c7201d375).
  • Where the allowance stops. The stop with every flush epoch on the disk, after the append's entry epoch and the empty one its flush opens, is the sync whole. At it no complaint is allowed, not the three names either: check must return nothing, and the file must read as the 15,288 bytes the append made, not the 3,000 the last sync held. A sync that returned Ok and left the old size on the disk is red there.

Red before the fix, measured at the merge base in the last round with the prefix version of the test: block write 9 of 13 (log). Under the epoch model, the whole fix reverted is red too (fat-m1 below).

  • The cause. Cache::flush writes dirty blocks lowest first. A sync put the grown entry, the chain and the data into one flush. The root directory's cluster lies below the clusters the append allocated, so the disk took the entry before the data it names.
  • A device with a volatile write cache also keeps no order between writes until a flush. That holds whatever order the cache picks, and the epoch model is what holds it now.

The fix, in one site (FatVolume::level). Before the entry of a file whose entry and handle differ (File::needs_reconcile) is written into the cache, the cache is flushed. That flush is Cache::flush, which ends in the disk's own flush. The module header says the two writes are a cache flush apart.

  • It was built before it was accepted. The design round staged S1 as the two tests alone. The orchestrator accepts the flush after the fact, and records building it first as out of order (ruling 3).
  • Roast.
    • A sync now costs one device flush per growing open file, plus the sync's own. A last close of a grown file costs one. FAT serves only the LOG and BOOT roles. The cost on the T14 is 14,824 µs on usbwrite's timed write and sync (below).
    • The flush runs for a shrink too, which needs_reconcile also names. That leaves a shrink's order as it was: a probe of a truncate and its sync, stopped the same way, reads Io at stops 10 to 12 of 13 (probe, log). The entry still names clusters the stop had already freed. issues/a-shrink-frees-clusters-before-the-entry-stops-naming-them.md records this, and S1 does not fix it.
    • Lost clusters that a stop leaves between the chain and the entry are issues/nothing-reclaims-what-a-stop-leaves-on-a-fat-volume.md's.

Oracle. fatgen103, through toyos-fat32-check on each stop's image, which is the tree's outside judge for the volumes it writes, and through the checker's fixture. Why a host test: the stop is a disk model, and no guest reaches a stop inside a flush epoch.

The reopen: the server's half is a type, and the client's half stays open

answer and its test are deleted. Opened::over already makes a server's ends of the session page and its answer together. The compile_fail doctest at lib/blockring/src/wire.rs refuses building one from its fields. So the order the test held is a type's, and a test of it would test what a type refuses.

  • handshake now calls Opened::over inline at the MSG_OPENED send, and admit takes the ends and the answer.
  • Service::open keeps the partition's length in Opening in their place, so it no longer makes ends before an answer exists.
  • The client's decode is the one other way to make an Opened. It is left as it is. Closing it by type would change toyos-blockring's wire API, which is outside this fence, and the ruling asks for it only "if [it] must close".

issues/nothing-reopens-a-diskserver-session-over-a-used-page.md stays open, narrowed to the client's half.

  • Its exit is now a client host test: a reopen over a page a killed server left its answer on ends the session, and the caller's call is Io, by drain's Tag refusal. The test must be red with that refusal removed.
  • The client-glue issue this branch had filed is folded into it, so there is no sibling.

The submission-ring hang is filed as its own defect, issues/a-block-session-waits-forever-on-a-server-past-its-submission-ring.md, owned by the orchestrator.

  • Session::pump meets the producer's HeadPastTail and only stops pushing.
  • drain reads only the completion ring, so wait blocks in poller.wait(1, u64::MAX, ..) until the server hangs up.
  • Its exit: the violation ends the session, held by a host test that is red with that end removed.

The kernel-is-small track's two pointers (step 3's exit and step 10's storage clause) name wire::Opened::over for the server's order and the reopen issue for the client's half.

The final stop, measured at c7201d375

Round 2's review found the allowance held at every stop, the one after the last flush included, so the test could not fail on its own claim. c7201d375 holds that stop to a clean check and out == whole. Each command is cargo test -p fileserver --lib an_append_stopped, run by run.sh and run2.sh, which apply a patch, run, restore, and log RESTORED_CLEAN=yes.

run test exit log
(a) the tightened test, no patch c7201d375 0: the window closes inside the sync a-tightened.log
(b) the review's record_meta patch: the append's sync rewrites the entry with its old size the test as it was at 17ea52983 0: the old allowance hid the loss b-at-17ea52983.log
(b) the same patch c7201d375 101: stopped in flush epoch 3 of 5 with writes 0b11 of its 2: /a.txt: DIR_FileSize is 3000 bytes, which needs 6 clusters, and the chain holds 30 b-tightened.log
the three-name list, ChainTooLong allowed nowhere c7201d375 101, at epoch 2 of 5 with writes 0b1 of its 6 three-names.log
negative control: the fileserver's flush removed, the whole FAT production change reverted c7201d375 101: epoch 1 of 3 with writes 0b1000 of its 8: /a.txt: DIR_FileSize is 15288 bytes, which needs 30 clusters, and the chain holds 6 negative-control.log

Under (b) at c7201d375, the red stop is 0b11 of its 2 in epoch 3: the append's entry epoch, whole, which is the image with every epoch on the disk.

Mutations at 17ea52983

Each patch is applied as a checked patch, run with cargo test -p fileserver --lib an_append_stopped, and restored clean by run.sh. Every log ends RESTORED_CLEAN=yes.

patch what it does exit
fat-m1-no-barrier Negative control: the flush removed, which reverts the whole FAT production change 101, the checker: epoch 1 of 3 with writes 0b1000 of its 8: /a.txt: DIR_FileSize is 15288 bytes, which needs 30 clusters, and the chain holds 6 (log)
fat-m2-entry-before-chain The FAT exit's mutation: the flush removed, and the cache writing its highest block first 101, the checker's ChainTooShort at writes 0b10000 of 8 (log)
fat-m3-cache-highest-first the cache writing its highest block first, with the flush kept 0 (log): the order rests on the flush, not on the cache's order
fat-m4-no-disk-flush New: Cache::flush without the disk's flush, so no epoch ever closes 101: a stop that holds only part of the first sync has no a.txt (log)

fat-m4 is what the old prefix test could not see without cache.rs's flush-count test (the review's NOTE 2).

Gates

c7201d375 changes only the FAT test (git diff --stat 17ea52983..c7201d375: system/fileserver/src/fat.rs, +7 −3, all in mod tests), so the guest suite's run at 17ea52983 is of this head's production code and image contents, and no guest test reaches the change.

gate head exit log
cargo run -- --ci host c7201d375 0: "76 step(s), all green". system/fileserver's step ran the FAT test ci-host.log
cargo run -- --build-only c7201d375 0 build-only.log
cargo test --test toyos-build (the whole guest suite: every boot's /log is the FAT file server's, and every boot that starts diskserver opens its sessions through handshake) 17ea52983 0: 53 passed of 53 guest.log
the four mutations above 17ea52983 101, 101, 0, 101 above

The extra flush's cost on the T14: read

The row is usb_reset_hands_devices_back.

  • Its metaldevicecase boot (tests/images/metaldevicecase.toml) runs usbwrite first.
  • That job creates /log/metal-write.bin, writes its bytes in 64 KiB chunks, and sync_alls it. That is a sync of a grown file on the stick's FAT volume, with the sync inside the timed span (tests/guest/metalprobe/src/usb.rs).
  • It exits with the span in microseconds, as exit: usbwrite pid=N code=<µs> in the kernel log.

The two arms, staged in the last round (S1 stage, base stage):

  • S1 is 671e5c736.
  • The base is 8e39da48e, which is 671e5c736 with fat-m1-no-barrier applied: the whole FAT change reverted.
  • 671e5c736's FAT production code is this head's. This round changes only diskserver's open and the FAT test.
  • The orchestrator ran both on the T14, and both exited 0.

Each log below is the judge's log, the readback's verdict.txt, and kernel.log lines 347–357, from ===TEST_START usbwrite=== to ===TEST_END usbwrite. Each is masked with the orchestrator's mask, and the mask's own check counts 0 in it.

arm head judge usbwrite span log
S1 671e5c736 PASS usb_reset_hands_devices_back, "1 passed, 0 failed, 2 boot(s)", "0 past its record" 70,480 µs (exit: usbwrite pid=9 code=70480) t14-s1.log
base 8e39da48e the same 55,656 µs (code=55656) t14-base.log

The cost is 14,824 µs for one grown-file sync on the stick: one device flush more. That is one reading per arm. It is reported, and no bound judges it.

S0: what the T14 read

Read. The orchestrator ran usbd_s0_readout on the T14 from the scout at b5c3b14f8: judge exit 0, 1 passed, 0 failed, 1 boot. Its pci-readout lines are published masked on pr-901 as s0-t14-pci-readout.log. Each line there carries its line number in the readback's kernel log: 00:0d.0 is lines 105 to 115, and 00:14.0 is lines 116 to 126. The scout is deleted, locally and on origin.

00:14.0, 8086:a0ed, the boot controller:

  • BAR 0 is mem64 and not prefetchable, and advertises 64 KiB.
  • Status is 0x0290, so the Capabilities List bit is set. The pointer at 0x34 is 0x70.
  • The walk is 0x70 id 0x01 (Power Management) → 0x80 id 0x05 (MSI) → 0x90 id 0x09 (vendor-specific) → 0xb0 id 0x09 → next=0x00: "walk reached the terminator after 4 link(s)".
  • MSI at 0x80: control 0x0086, 64-bit, 8 vectors, no per-vector mask, not enabled.
  • msix none. There is no PCI Express capability, so no Function Level Reset, and no Advanced Features capability.
  • Its path ends on root bus 0x00, with no bridge.

00:0d.0, 8086:9a13, the Type-C controller, has the same shape link for link. usbd_drives_the_type_c_controller already claims it.

What it decides:

  • A claim on 00:14.0 never reaches CapsTruncated (kernel/src/pcidev/mod.rs:830). It takes the Absent arm and arms MSI (:831-833). The scout walked with the same caps::CapWalk that capability() uses.
  • The T14 needs nothing from P1's table-page half. S5 still waits on P1, whole, for QEMU's nec-usb-xhci and for the AMD bench, and on AMD-Vi stages 5 and 6, as ruled.
  • No vector can be masked on either T14 controller, which decides the generic reset stop (section 6).
  • A restart of 00:14.0's usbd resets nothing through PCI unless PM's No_Soft_Reset is clear, and S0 did not read that bit (section 5).

Where it starts

1. What usbd is

  • One usbd per xHCI function, which is the stage-0 ruling. The supervisor starts one for every function its row's selectors match, and starts it again when it ends other than cleanly (ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's restart rule). usbd never exits cleanly.

  • One thread waits on one poller (stage 1's rule). It runs straight-line async fns:

    • one task per root-hub port;
    • one task per bound function (a disk, a keyboard, a pointer);
    • one task per block session.

    A task awaits one of two things: the completion event of the TRB it submitted, matched by that TRB's address, or a deadline. Nothing spins after bring-up.

  • A deadline is never a cancellation. An operation the controller still owns is a value its task holds until the completion or the abort hands it back. Dropping one is a panic by name, because a TRB the controller holds cannot be forgotten. A deadline is an outcome the operation returns (Silent), and the code that follows it is the specification's recovery:

    • for a command, Command Abort (xHCI 1.2 §4.6.1.2);
    • for a transfer, Stop Endpoint and then Set TR Dequeue Pointer (§4.6.9, §4.6.10).
  • The driver is a crate on the driver boundary (issues/a-driver-is-tested-on-the-host-and-its-real-implementation-is-one-instruction-deep.md), shaped as toyos-i219 is:

    • toyos-device-memory's Registers and DmaBuffers, plus the crate's own Clock and Interrupts, are generic parameters;
    • usbd's binary is that boundary one instruction deep, through toyos-pci-claim;
    • the order of the two barriers (publish before a doorbell, observe after a cycle bit) is the trait's. That is where the ARM track's owed xHCI ordering check lives.
  • toyos-xhci moves into system/usbd/xhci/ at the cutover, because usbd is then its one user.

    • It keeps its decoders of controller and device words: descriptor, xecp, protocol, portsc, ring, configure and identity.
    • It keeps the port decision, port::PortState.
    • These exist so the kernel could step them inside a pass or spin them under a lock, and become usbd's straight-line tasks: enumerate, recovery, reset_recovery, ladder, call, job, scan, late and bot's phase machine.
    • sim/ becomes stub.rs, an xHC written from xHCI 1.2 clause by clause, as the toyos-i219 stub is written. Every latitude the specification allows is a Permits entry that is on by default, and every run takes a seed. The three sim tests the step-10 exit names keep their names in it: a_controller_that_never_answers_still_releases_the_port, a_recovery_the_controller_refuses_lets_the_device_go and no_sequence_breaks_an_invariant.
  • Two crates are split out:

    • SCSI becomes lib/scsi (toyos-scsi, no_std, with no dependency on xHCI). That is issues/scsi-is-decoded-inside-the-xhci-crate.md's exit.
    • kernel/usbhid becomes lib/usbhid, because the kernel stops using it.

2. The boot path

  1. The loader reads ROOT through firmware's block I/O, checks its signature and hands it over in memory (stage 1, done). The kernel issues 0 storage commands before the supervisor.
  2. The kernel runs no storage phase. xhci::init, gpt::probe_usb_disks and rootfs::hold_source go. Boot: complete loses what the scan costs today: 484 ms on the T14 and 406 ms on the AMD bench.
    • Until /log mounts, the kernel's records are on its ring, the console, the panel and the black box.
    • A boot that dies before /log mounts is read from the black box, which the next loader prints, as a panic is today.
  3. The supervisor starts the storage rows first, as it does now: diskserver and every usbd (both are block services), then the data, log and boot file servers, then the rest.
  4. usbd brings up its controller:
    1. the handoff (xHCI §4.22.1), HCRST, the grant, the rings, Run;
    2. each port's task (section 5);
    3. for the stick: the Bulk-Only bring-up and its GPT read through toyos-gpt;
    4. its partitions served on the instance's block@<location> port.
  5. The LOG file server holds one connector to each block service, each minted for its partition's GUID alone (Scope::Unique, writing). It lists each service and opens its partition where it is served.
    • usbd answers a listing only once its bus has settled. Settled means every port connected at bring-up was enumerated and either bound or refused by name, and every disk's table was read or refused.
    • The wait is bounded by the deadlines those steps already carry, so a file server never mistakes "not yet" for "absent".
  6. logkeeper writes /log through that file server.

What changes for each volume:

  • ROOT. Every block service refuses ROOT's partition to every session (--running), as diskserver already does.
  • BOOT. Its server holds a read-only grant, and usbd answers its writes ReadOnly. That closes issues/the-boot-volumes-server-holds-a-claim-that-writes.md.
  • DATA on a stick is found by type, through the same listing.
  • The slots that update writes are read and granted through the block service that lists the slot table. On the T14 that is its usbd.
  • Deleted: fileserver's Claimed disk, toyos::PartitionDev, DeviceType::Partition, the part: request and SYS_PARTITION_READ/WRITE. The supervisor mints no partition claim.

3. The keyboard and the pointer

Every HID interface is bound by its report descriptor, in report protocol. There is one decode path.

  • The report descriptor decides every field (HID 1.11 §6.2.2). A parser in toyos-usbhid reads it into the interface's top-level application collections.
    • It binds a keyboard's Keyboard/Keypad-page fields, arrays and variables alike, and a pointer's buttons, X, Y and the Generic Desktop Wheel (0x38, HID Usage Tables 1.5 p.33).
    • It refuses by name, per top-level application collection, what it cannot read. A report belongs to one: "Each top level collection must be an application collection and reports may not span more than one top level collection" (§8.4, p.67). So a refused collection costs only its own reports, which are the Report IDs its fields carry (§6.2.2.7). A keyboard whose interface also carries a Consumer Control collection keeps its keys, and its Consumer Control reports are counted by name and dropped.
    • A Report ID that spans two top-level collections refuses the whole interface by name. §8.4 forbids it, and a device can still declare it; binding one collection's half of a split report would read the other's fields as absent.
    • Each collection it binds is one device to the owner, with its own source (below).
    • That is issues/a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet.md's exit.
  • A boot-subclass interface is told SET_PROTOCOL(Report). "When initialized, all devices default to report protocol. However the host should not make any assumptions about the device's state and should set the desired protocol whenever initializing a device" (HID 1.11 §7.2.6, p.64). The request is "required only for boot devices" (§7.2, p.61), so only those are sent it. Boot protocol is never asked for, and Appendix B's fixed layouts are not decoded.
  • A keyboard interface is told SET_IDLE(0) (§7.2.4, p.63), so a report comes only on a change. That request is optional outside the boot subclass (§7.2's table). A device that stalls it keeps reporting at its polling rate, and an unchanged report is no transition.

A key field is read as HID 1.11 and HUT 1.5 define it. There are three rules, and each has its own §8 test.

  1. An array field reports indexes, not usages (§6.2.2.5, p.40).
    • An array "returns an index in each field that corresponds to the pressed button", and "Logical Minimum specifies the lowest index value returned by the array and Logical Maximum specifies the largest".
    • The range check is on the index. "An out-of range value in and array field is considered no controls asserted" (p.40). So a value outside Logical Minimum to Logical Maximum asserts nothing.
    • The usage is the field's usage list at that index: Usage Minimum + (index − Logical Minimum), where Usage Minimum and Usage Maximum give the list (§6.2.2.8, p.50), or the index's own entry where Usage tags list it one by one. The usage, not the index, is what is delivered.
    • An index whose entry lies past the end of the field's usage list names no key. It is counted as usb.hid.unlisted and not delivered.
    • Inside the range, ErrorRollOver (usage 0x01) is Appendix C's phantom state. Its variable fields are believed, its array fields are not, and the keys held before stay held. POSTFail (0x02) and ErrorUndefined (0x03) refuse the report.
  2. A Keyboard/Keypad-page variable field is a key bitmap, and it is a key report too (§8.3, p.66: modifiers "should be reported as bitmap data"; p.67: keys that are not mutually exclusive "must be represented either as a bitmap or as multiple arrays").
    • Each control's usage is its position in the usage list, from Usage Minimum.
    • A control is held when its value is 1, and released when it is 0 (§5.9, p.30: "0 = off, or False, and 1 = on").
    • A value outside its Logical Minimum to Maximum leaves that control as it was: "the current value for the respective control will not be modified" (§5.10, p.30).
    • The modifier byte is this rule's instance (usages E0 to E7), not a case of its own. A keyboard that reports every key as a bitmap is bound, not refused.
    • A bitmap control at 00 to 03 is never delivered as a key. HUT 1.5 p.96, note 1, says 01 to 03 are "Sent as a member of the keyboard array. Not a physical key", and 00 is reserved. A bitmap that starts at Usage Minimum 0 has such controls; their bits are ignored, so ErrorRollOver never reaches the owner as a press.
  3. A usage the HID Usage Tables reserve names no key. HUT 1.5's Keyboard/Keypad table reserves 00 (p.90), A5-AF (p.94), DE-DF (p.95) and E8-FFFF (p.96).
    • 00 is an empty slot: an index that reaches it asserts nothing, as an idle array reports in every field (§6.2.2.5's remarks, p.42), and is not counted.
    • A usage in the other three ranges, reached by an array's index or by a bitmap's position, is counted as usb.hid.reserved and not delivered.

The wheel is wherever the descriptor puts it. Appendix B.2 leaves byte 3 onwards "Device specific (optional)" (p.71). Under boot protocol every boot mouse would lose its wheel, which is why boot protocol is never asked for.

Absolute motion is scaled into #899's range. #899 refuses an absolute position outside 0..=32767.

  • usbd maps an absolute X or Y from its field's Logical Minimum to Logical Maximum onto it: (value − Logical Minimum) × 32767 / (Logical Maximum − Logical Minimum), in 64-bit arithmetic, rounded down.
  • A value outside the field's logical range leaves that axis's position as it was (§5.10, p.30).
  • A field whose Logical Maximum is not above its Logical Minimum is refused by name at parse, with its collection.

Every field is bounded to #899's record before it is sent. #899's owner drops a source whose frame does not decode, and that source is usbd's whole connection, so one device's out-of-record value would release every device on its controller.

  • Buttons: the Button page's usages 1 to 5 are the record's five bits. A button usage past 5 is counted as usb.hid.unsent_button and not delivered; the device's other fields still are.
  • Wheel and relative motion: a field wider than the record's scroll, dx or dy saturates to that field's range, as the summed motion already does.
  • Absolute position: scaled as above, so always inside 0..=32767.

Transitions go to the owner of input, over the input port #899 builds. The owner is the program that owns the screen until #868's inputserver lands: the compositor in images/system.toml and tests/images/metalcase.toml, and /system/bin/console in images/console.toml.

The panic console never depends on usbd. It takes no input (stage 7.1). After the cutover, the kernel's reset and panic paths touch no USB register (section 6).

4. pcidev: P1 serves usbd's claim

P1 of the Wi-Fi track (issues/there-is-no-wifi.md, track order item 4) is the route, whole, as #900 ruled. It keeps a BAR where firmware put it, hands it over in 4 KiB pages, and maps the MSI-X table's page with the rest, under the owner's Q2 ruling. This design adds nothing to P1. It depends on three things P1 lands:

  • nec-usb-xhci, QEMU's boot controller, keeps its table at bar0+0x3000. Its claim is refused NoMappableBar until P1.
  • The spare is qemu-xhci with its MSI-X left on, which the track names as the sign that P1 serves usbd: usbd_drives_the_spare passes on it.
  • The AMD bench's 1022:1639 keep their tables in BAR 0 behind bridge 00:08.1 (Where it starts). Both P1 halves are needed there: the table's page, and the in-place placement inside the bridge's forwarded range.

Round 1's S1 built P1's table-page half at 2 MiB windows. That is gone:

  • the table's BAR would have to move;
  • bring_up arms MSI-X before place_bars (kernel/src/pcidev/mod.rs:826-839);
  • arm_claimed_msix keeps an Mmio at the entry's address before the move, held in Armed::Msix (kernel/src/drivers/pci.rs:482-486).

What P1 owes for source verification. Round 1 proposed reading QEMU's VT-d source. Under #902 another implementation's source is read only clean-room. The cheaper reading is a mutation: clear VERIFY_SOURCE_ID (kernel/src/arch/x86_64/vtd/interrupt.rs:62), and the guest test that rewrites a held table entry towards another claim's handle must go red. That test and its mutation are P1's, not this design's.

The location selector stays here (S3). pci@<bus>:<dev>.<fn> is a new DeviceRequest. The vendor:device spelling cannot name either of the AMD bench's two 1022:1639 functions: it answers Ambiguous.

5. Recovery and hot-plug, written once

Boot discovery is hot-plug. Each port's task starts at bring-up. A device present then and one plugged in an hour later run the same code. The task follows the port's protocol, which the Supported Protocol capability names (xHCI §7.2):

  1. Await a connect change.
  2. A USB 2 port (xHCI §4.19.1.1):
    • Debounce for 100 ms. TATTDB is "a debounce interval with a minimum duration of 100 ms that is provided by the USB System Software" (USB 2.0 §7.1.7.3, p.178). It is a fixed delay the specification mandates with no notification for it, cited at its site.
    • Reset, and await Port Reset Change.
    • Wait 10 ms. TRSTRCY: "The USB System Software guarantees a minimum of 10 ms for reset recovery" (USB 2.0 §7.1.7.3, p.179; §7.1.7.5, p.181; §9.2.6.2, p.274). xHCI leaves that timing to software: "Software shall be responsible for timing the Reset 'recovery interval' required by USB" (xHCI 1.2 §4.6.8, p.131).
  3. A USB 3 port (xHCI §4.19.1.2):
    • Await link training. The port moves from Polling to Enabled by itself, once its configuration exchange succeeds as a downstream port (§4.19.1.2.4.2, p.308). Software neither debounces nor resets it to enable it; USB 3.2 defines no debounce interval.
    • Wait nothing after a reset. "After a port is successfully reset or resumed, the USB system software is allowed to access the device attached to the port immediately and it is expected to respond to data transfers" (USB 3.2 §9.2.6.2, p.358).
    • A warm reset (WPR) out of SS.Inactive (§4.19.1.2), awaited on Port Reset Change, with the same zero recovery.
    • Every PORTSC word read is handed to port::PortState.
  4. Enumerate in xHCI §4.3's order: Enable Slot; Address Device; at full speed, the device descriptor's first 8 bytes and Evaluate Context; the device descriptor; the configuration; Set Configuration and Configure Endpoint.
  5. Bind the classes.
  6. Await the disconnect. On it, the function tasks end with Gone, the endpoints stop and the slot is disabled.

An endpoint's recovery is one function, chosen by the endpoint's state as its events report it (§4.8.3), as toyos-xhci::recovery decides today:

  • a halted endpoint gets Reset Endpoint (§4.6.8) and CLEAR_FEATURE(ENDPOINT_HALT) (USB 2.0 §9.4.5);
  • a running one gets Stop Endpoint;
  • an errored one gets Set TR Dequeue Pointer.

A disk's recovery is the ladder, as straight-line code:

  1. Bulk-Only Reset Recovery (BOT 1.0 §5.3.4), verified by a TEST UNIT READY under its own tag.
  2. A port reset and enumeration again, under the same disk, with the recovery its port's protocol gives (above).
  3. Offline, and the last thing it is sent is a reset.

A disk whose device leaves is held for identity::RETURN_WINDOW; for example, a reset can move a SuperSpeed stick to its companion port. Its sessions wait meanwhile.

  • The same device comes back, as identity::same says: its sessions resume, and toyos-blockring's client issues again the writes no flush covered.
  • A different device comes back, or none within the window: every session on the disk ends Gone, and nothing is issued again. Writes issued again to a different device would be data loss on that device.

A command the controller does not answer is aborted (§4.6.1.2).

  • An abort it does not answer means the controller is gone, and usbd ends by name.
  • The supervisor's restart is then the controller's recovery, the same path a crash takes: the claim is released, a new one minted, and HCRST runs before the first grant lets the function master again (pcidev's rule).
  • On the T14 that release resets nothing through PCI. S0 reads no PCI Express capability on 00:14.0, so no Function Level Reset, and no Advanced Features capability, as on 00:0d.0. PM's No_Soft_Reset bit is still unread on 00:14.0; on 00:0d.0, whose shape is the same, usbd_drives_the_type_c_controller reads the release as reset by nothing (Express: no capability; AF: no capability; PM: No_Soft_Reset set).
    • So a restart of 00:14.0's usbd takes the inherited-residue release that row reads on 00:0d.0: the dead holder's device addresses stay the slot's residue (kernel/src/pcidev/mod.rs:70-83).
    • HCRST is the controller's only reset. The new usbd writes it before its first grant, and lays its grants out as its predecessor did, which pcidev's rule rests on (:85-90).
    • usbd_serves_the_boot_stick prints the release's line for 00:14.0 (section 9), which reads the No_Soft_Reset bit.

Where a specification bounds a wait, the wait is that bound: a standard request with no data stage completes "within 50 ms of receipt" (USB 3.2 §9.2.6.4, p.358), and USB "sets an upper limit of 5 seconds for any command to be processed" (§9.2.6.1). Where none does, the bound is a policy stated at its site. BOT and SBC give a command no time limit, and the T14's stick has answered later than 2 s (issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md). The proposed bound is 10 s, on the reasoning behind diskserver's COMMAND_SILENCE, and it is measured on both benches' sticks.

A Bulk-Only command is a CBW, a data phase and a CSW (§5.1, §5.2).

  • The CSW is held to "valid" and "meaningful" (§6.3), and §6.7's thirteen cases are decoded.
  • No data TRB spans a 64 KiB boundary (xHCI §6.4.1).
  • The data moves straight between the stick and the client's region, which the kernel maps into the controller's domain for the session (SYS_DEVICE_DMA_MAP), as diskserver does.

A flush is decided by the Caching mode page's WCE bit (SBC-3 §6.4.5) where the device answers MODE SENSE for it, and by SYNCHRONIZE CACHE's own answer where it does not.

UAS (USB-IF UASP 1.0, T10 UAS-2) is not built. It needs streams (xHCI §4.12), and no device on either bench offers it. SCSI stays apart from its transport, and the build files UAS as a feature.

6. Reboot, shutdown, panic

  • An orderly stop: the supervisor has logkeeper flush and the file servers sync (a flush reaches the stick through usbd), and then asks the kernel to stop. The kernel's quiesce loses flush_disks and seal_shut.
  • The boot deadline, the hard-lockup detector and a panic: the kernel writes the reset register having touched no USB register. The platform reset cuts any Bulk-Only command in flight, and the next host's firmware resets the port before it reads the device (USB 2.0 §7.1.7.5). The rewritten usbload row (section 9) measures whether the stick comes back.
  • The reset paths' stop. stop::before_reset is the one place the kernel stops a function's bus mastering before the reset register: the xHCI's, which the kernel drives. A function a process claims is left armed today. Once the xHCI is claimed too, a generic stop replaces it, lock-free, for every bound function. That is the second answer of issues/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md. What it writes, by the function's capabilities:
    • every function: Bus Master Enable cleared in the Command register (PCIe Base §7.5.1.1). That also stops its MSI and MSI-X, since a message is a memory write the function masters;
    • a function armed on MSI: MSI Enable cleared (§7.7.1). S0 reads per-vector-mask=n on both T14 controllers, so there is no vector to mask, and Enable is the one bit every MSI capability has;
    • a function armed on MSI-X: Function Mask set (§7.7.2).

7. The supervisor and the manifest

  • One instance per function. A row may start one instance per PCI function that any of its selectors matches: each = ["pci-class:0c0330"], or ids, each = ["pci:1033:0194"]. Each instance is endowed:

    • a claim on its function by location, pci@<bus>:<dev>.<fn>;
    • its own acceptor for each name it serves (block@00:14.0, usb@00:14.0);
    • what its row receives, such as input.

    A row that receives a name an each row serves gets one connector per instance.

  • Every row that serves block is a block service. Each is told --running. A file server is endowed one connector per block service, minted for its role's grant.

  • The generic half of diskserver becomes a crate, lib/blockserve, that both block services stand on. That half is: accepting, grants, holds, the listing, sessions with wire::Opened::over, region lending and the partition table's read. It moves with its tests. The device beneath it is NVMe, or a Bulk-Only disk.

  • The rows:

    • images/system.toml's [programs.usbd] gets each = ["pci-class:0c0330"], serves = ["block", "usb"], receives = ["input"], service and restart. images/console.toml's row is the same.
    • The test images' usbd row names only each machine's boot controllers: each = ["pci:1033:0194", "pci:8086:a0ed", "pci:1022:1639"]. QEMU's spare (1b36:000d) and the T14's Type-C controller (8086:9a13) stay free, for usbd_spare to claim, as xhci-leave= leaves them today.
    • inspect receives usb, and toyos_inspect::USB joins OWNERS.

What is deleted

kernel/src at 858da4d42 is 64,063 lines.

whole files (wc -l) lines
drivers/xhci/: mod 1,727, wait/msc 1,772, device 913, stop 716, wait/boot 546, wait/mod 455, legacy 246, hid 150 6,525
gpt.rs. The loader-named GUIDs the supervisor reads, about 40 lines, move rather than go. 552
block.rs. BlockError and BlockResult, about 15 lines, stay with the page cache until step 9's VFS half. 504
drivers/usb_storage.rs 108
usb_gate.rs 91
arch/x86_64/idt/xhci.rs 17
keyboard.rs 114
mouse.rs 190
input_merge_test.rs 155
total 8,256

#899 lands first (track order item 1) and takes about 205 of the last three rows' lines. S5 measures what is left at its own base.

The kernel's input goes whole. Once ps2server delivers to the owner, the kernel xHCI's HID is the last source of the kernel's keyboard and mouse streams, so S5 deletes:

  • keyboard.rs, mouse.rs and input_merge_test.rs, with main.rs's two modules and its call;
  • the keyboard and mouse device classes: DeviceType::Keyboard (3) and Mouse (7), their DeviceRequest names, FileType::Keyboard and Mouse, device.rs's claim arms, syscall/device.rs's arms (:173-174, :462-463), and in object/ops.rs the read-watch arms (:286-287), the event reads (:421-445), the readiness arms (:830-831), the stat arms (:684-685) and the arms at :358-363, :763-764 and :905-906;
  • syscall/io.rs's ReadBlock::Keyboard, its wait, and read_block_device's keyboard arm;
  • KObjectRef::Console's keyboard watch (object/ops.rs:284) and ReadBlock::Console's wait on keyboard::WATCH. Nothing posts that watch for a serial byte today. A console's blocking read parks on its own re-poll deadline alone. Its poller watch has no source and is refused NotSupported at registration (inbox::arm), where today it registers a watch serial input never completes (issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md, whose exit stays open);
  • outside the kernel: toyos::Keyboard and Mouse with their endowment arms; sdk/std's and sdk/libc's FileType::Keyboard arms; lib/manifest's and lib/inspect's two classes; the keyboard and mouse devices of the compositor's and the console's rows in images/system.toml, images/console.toml, tests/images/metalcase.toml and tests/images/panelcase.toml. endowment_denied names the framebuffer class where it names the keyboard's. toyos_abi::input's records, which the kernel no longer speaks, move to toyos-input.

What the console image's shell reads instead: nothing new. The shell reads its surface from /system/bin/console, as today. The console reads key transitions from the connections on its input acceptor, ps2server's and every usbd's, in place of the kernel's keyboard claim.

Parts of files: about 900 lines, by reading. The build measures them.

  • The input pieces above.
  • The partition claims and their syscalls: device.rs, object/device.rs, object/ops.rs's until_answered, partition_fsync and partition_word, syscall/device.rs's sys_partition_transfer, and the dispatch arm.
  • rootfs::hold_source.
  • sync::OwedLock, its fence and its feature. Its one user is XHCI.
  • The xHCI and USB actuators: xhci-leave=, xhci-xecp-selftest, xhci-descriptor-selftest, usb-transport-break and usb-reset-under-load.
  • quiesce's block budget, machine.rs's USB calls, and acpi.rs and power.rs's stop.
  • drain_irqs's and the idle loop's xHCI checks.
  • IrqSource::Xhci, with its census and trace arms, and AArch64's DriverIrq::Xhci.
  • The inventory's USB, block and partition records.
  • main's storage phase.

Added in the kernel: about 70 lines, estimated. The location selector is about 30, and the generic reset stop about 40.

Net: about −9,000 lines of kernel/src, roughly 14%.

  • The kernel's dependencies on toyos-xhci, toyos-usbhid, toyos-gpt and toyos-blockhold go, and so does kernel/loom/tests/owed_lock.rs (64 lines).
  • That meets step 6.4's exit: Xhci, poll_if_pending, port_work_pending and OwedLock appear nowhere.
  • irq_ring is gone: ps2server took I8042, and this takes Xhci.
  • That meets step 9's block exit: git grep -n -e BudgetExpired -e DEADMAN -e between_attempts -e until_answered -- kernel finds nothing.

Outside the kernel:

  • In toyos-xhci, call (546), late (120) and scan (116) are deleted outright.
  • enumerate (427), recovery (322), reset_recovery (626), ladder (481), job (474) and bot's phase machine (bot.rs is 741) become usbd's straight-line tasks.
  • sim/src/driver.rs (701) is replaced by the stub.
  • Also gone: fileserver's and diskserver's Claimed, the supervisor's kernel-partition half, toyos::PartitionDev, SYS_PARTITION_*, DeviceType::Partition, and Record::Usb, Block and Partition.

Staging

Round 1's S3 to S7 are S1 to S5 here. Its S1 and S2 are gone, and its S8 folds into S5.

pull request depends on exit
S0 Scout: measurement only, never landed. The usbd_s0_readout row on the T14, from b5c3b14f8 (section "S0"). Done: the walk reached its terminator, and the scout is deleted. — met
S1 The FAT module test the step-10 exit owes, on today's fileserver: system/fileserver/src/fat.rs's append and sync, stopped inside a flush epoch and judged by toyos-fat32-check, with the flush it found missing. diskserver's open answers through Opened::over inline. The reopen's client half stays issues/nothing-reopens-a-diskserver-session-over-a-used-page.md's, as ruled (comment 6139586864). — the FAT test red on a sync that writes a growing file's directory entry before its chain
S2 usbd's core: the executor, the controller, event dispatch, Command Abort, the port task, enumeration, and the stub with the three named sim tests. It runs on the default qemu-xhci spare and the T14's Type-C controller, which xhci-leave= leaves. P1 host tests; usbd_drives_the_spare on qemu-xhci with MSI-X on; usbd_drives_the_type_c_controller
S3 Mass storage: toyos-scsi split out with the sense fix; the Bulk-Only disk, the ladder, identity and flush; block@ on the spare. And what has its second user here, moved from round 3's S1: each with its selectors, the pci@ location selector, several block services, lib/blockserve taken out of diskserver, fileserver over several block services, and a listing answered once settled S1, S2 host tests; issues/sense-data-is-read-as-fixed-format-whatever-its-response-code-says.md's exit; diskserver's guests unchanged and green; the manifest and blockserve host tests
S4 HID: lib/usbhid with the report-descriptor parser, SET_PROTOCOL(Report) and SET_IDLE; the input extension in ps2server, toyos-input and the owners; usbd's connection and its full-connection rule. The stub test the step-10 exit owes: a stick whose bulk endpoint never answers leaves a keyboard on the same controller delivering every report. S2, S3, #899 host tests; the tablet issue's exit; the stub test red on a usbd that waits on the stick before it reads the keyboard's ring
S5 Cutover: everything in "What is deleted"; the rows in section 7; the generic reset stop; tests rewired and cut, each named; the metal rows in section 9 on both benches; lib/xhci moved into system/usbd; FLASHABLE without xhci-leave= P1, whole; AMD-Vi stages 5 and 6; #904, the track's input text; S1 to S4 the track's step-10 exit, below; step 6.4's and step 9's block half's

S5's exit is the track's step-10 exit as it stands once #904 lands:

On ARM, usbd claims virt's xHCI once #869 lands. That stays the ARM track's.

Until S5, the kernel's driver and usbd's run side by side, as since #827. The kernel's copies of the sequences are a compromise owned by S5, and their exit is the kernel's driver deleted. The track already records such a compromise for the descriptor decoder and the extended-capability walk.

8. Checks under the testing ruling

Units and modules, on the host. These carry the weight.

  • usbd's driver against the stub, seeded:
    • a device present at bring-up and one plugged in later run the same path, and their traces are equal;
    • a USB 2 port waits TATTDB and TRSTRCY, and a USB 3 port waits neither, read from the stub's recorded clock;
    • a port that drops mid-enumeration frees its slot;
    • an unanswered command is aborted, and its waiter is answered Aborted;
    • an unanswered abort ends the driver by name;
    • a stick whose bulk endpoint never answers leaves a keyboard on the same controller delivering every report. It counts reports and asserts no clock. Its mutation: the event dispatch awaits the stick's Bulk-Only command before it routes a transfer event for another endpoint. The test is red under it.
    • each of Bulk-Only's thirteen cases is answered as §6.7 says;
    • a stick that comes back on its companion port keeps its sessions; a different stick there ends them Gone, with nothing issued again;
    • no TRB spans 64 KiB;
    • the cycle bit is published after the TRB's body, read from the stub's recorded access order. This is the ARM track's owed check;
    • a listing waits for the bus to settle;
    • a restarted driver resets the controller before its first grant, with the stub keeping the dead holder's rings live;
    • at a full input connection no record waits in usbd; a release that falls while it is full reaches the owner once it drains; a press and release inside it are counted coalesced;
    • the connection follows the input devices: a controller with only a stick opens no input connection; the first HID bind opens one; the last device's gone is sent and the connection dropped. Red when usbd opens its connection at bring-up, and red when it keeps the connection after the last gone.
    • a bound device reopens its connection on its next report: the owner drops the connection while a key is held; the next report opens a new one, and the owner receives the held key as current state. A second drop is counted usb.input.unowned, and the first report a second after it opens again, while a report inside that second opens nothing. Red under the HID-bind-only trigger, and red when every report opens.
  • toyos-scsi: SPC-4's sense formats (the sense issue's exit). The INQUIRY and READ CAPACITY refusals move with their tests.
  • toyos-usbhid: the report-descriptor parser, on QEMU's tablet descriptor, its keyboard's, and another layout, with refusals by name and a no-panic fuzz. a_rollover_report_believes_its_modifiers_and_not_its_slots holds rollover. Each of section 3's key rules has its own test, each red under its own mutation:
    • an array whose Usage Minimum is not its Logical Minimum: a descriptor with Logical Minimum 1, Logical Maximum 0x62, Usage Minimum 0x04 and Usage Maximum 0x65. Index 1 delivers usage 0x04 (a), index 0x62 delivers usage 0x65, index 0 asserts nothing, and index 0x63 asserts nothing. Red when the usage is read as the raw index, and red when the range check is made on the usage instead of the index;
    • a bitmap keyboard: a descriptor whose keys are a Keyboard/Keypad-page variable field, one bit per usage from 0x04 to 0x65, beside the modifier byte. Setting and clearing a bit delivers that usage's press and release. Red when a key variable field is refused or skipped;
    • a bitmap's controls at 00 to 03: a bitmap from Usage Minimum 0, with bits 0 to 3 set, delivers no press. Red when they are delivered as keys;
    • a usage in each reserved range: 0xA5, 0xAF, 0xDE, 0xDF and 0xE8, each reached through an array index and through a bitmap position. Each is counted reserved and not delivered, while 0xA4, 0xB0, 0xDD and 0xE7 beside them are; an idle array's 0x00 slots assert nothing and count nothing. Red when any one of the three ranges is taken out of the check;
    • refusal per collection: an interface with a keyboard collection and a Consumer Control collection under two Report IDs. The keyboard's reports are delivered, and the other ID's are counted and dropped. Red when one refused collection refuses the interface;
    • a Report ID spanning two top-level collections: the interface is refused by name. Red when the per-collection refusal binds one half;
    • an 8-button pointer: a descriptor with Button Usage Minimum 1 and Usage Maximum 8. Pressing button 6 delivers nothing and counts unsent_button, while button 1 in the same report is delivered. Red when bits past 5 are passed through;
    • absolute scaling: an axis with Logical Minimum −100 and Logical Maximum 100 maps −100 to 0, 100 to 32767, and 101 leaves the position unchanged. Red when the raw value is passed through.
  • toyos-input (ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's crate): a gone naming one source releases what that device held and nothing another source on the same connection holds; a connection's end releases every device it carried; ps2server's pointer at source 0 and keyboard at source 1 are two devices. This is the owner's test that a key held at usbd's death is released, red with Merge's release on a connection's end removed, and red with gone releasing its whole connection.
  • lib/blockserve: grants, holds, --running, the boot volume's read-only grant, moved from diskserver with their tests (S3).
  • system/fileserver/src/fat.rs: a disk stopped after each block write in turn of an append and its sync mounts at every stop and reads back all the last whole sync held.
  • lib/manifest: each's parsing and its refusals.

The kernel's claims, as the kernel's integration.

  • usbd_drives_the_spare (QEMU) stays, on the default qemu-xhci from S2. Its drive arm hands the claim to usbd and kills it, and its fault arm claims the function again: a dead holder's claim handed over again, which is the exit's kernel part.
    • At S5, xhci-leave= goes. The spare is free because the test image's usbd row does not name it (section 7). The control arm becomes a claim on the boot controller, which the image's usbd holds: refused as held.
    • Why QEMU: the hand-over, the interrupt through the claim and the unit refusing a fetch past the grants have no host build, and the T14's unit is not asked to fault.
  • usbd_drives_the_type_c_controller (T14) stays, on 00:0d.0, the same at S5.
  • No new guest test. After S5, every guest boot's stick, and every desktop test's usb-kbd and usb-tablet, go through usbd and the owner's input port. The suite is the smoke test.

Cut at S5, each with what holds it:

  • usb_keyboard_rollover (QEMU): toyos-usbhid's rollover test.
  • The metal rows xhci_xecp_walk and xhci_descriptor_walk: their actuators go, and xecp's and descriptor's host tests hold the decoders.
  • usb_stick_left: its actuator goes, and the ladder's breaks are the stub's.
  • usb_reset_hands_devices_back: its judge reads stop.rs's account, which goes. The rewritten usbload row holds the stick's survival of a reset.
  • The kernel's input_merge_test: the merge is toyos-input's.

partition_grant was cut by #893, so it is not in this list.

Negative control, per stage: the whole stage reverted onto the base its green arm ran on, with its new checks red.

Independent oracles:

  • the specifications, written into the stub and the parser clause by clause;
  • QEMU's xHCI and HID devices, an implementation that is not ours, in every guest boot;
  • the T14's and the AMD bench's controllers and sticks, in the metal rows.

9. Metal rows

On the T14 and the AMD bench, at S5:

  • Every row. The harness reads /log off the stick, so every row passes through usbd's storage path.
  • usbd_serves_the_boot_stick holds the kernel's part only, as usbd_drives_the_type_c_controller does for 00:0d.0: the boot controller's claim handed over (00:14.0; 03:00.3 and 03:00.4), its message in remappable format through its remapping entry, and the first message on its slot raised by the No-Op. Its hand-over prints the release's line for the boot controller, which reads 00:14.0's PM No_Soft_Reset bit (section 5). Boot: complete and the time /log mounted are reported against the base's, and judged by nothing. The suite's boots through usbd are the smoke test of its naming and serving.
  • stick_survives_a_reset_under_load rewrites usb_reset_records_the_phase_it_cut's usbload boot.
    • A job writes the stick continuously through usbd, and the boot deadline resets the machine with no USB register touched.
    • It is judged on boot.usbload.stick_secs against its ceiling.
    • It is the kernel's: what it judges is the kernel's reset path, which after S5 cuts a Bulk-Only command with no stop, and the stick's answer to it on the next host, which only metal reaches. usbd is the load.
  • mask_windows is read under a stick-write load: each CPU's longest interrupts-off and preemption-off windows, against the base, by the track's rule. Stage 5's exit asks for no window longer than a register access.
  • tone_beside_a_stick_write, a new row: issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md's exit, on the T14. A job writes a mebibyte to /log while hda_tone's tone plays, and soundserver's underruns=0 over the tone's window is the verdict.
    • It is the kernel's: the claim is that one claim's interrupts and its holder's threads never starve another holder's deadline. That is the kernel's interrupt routing, scheduling and windows. The issue's evidence is the kernel's own: 15,564 xHCI interrupts on the CPU that took the sound device's, and a kernel transport break mid-tone.
    • soundserver and usbd are unchanged stimuli, and underruns is soundserver's count of its thread being run too late. Neither program's behaviour is judged.

The specifications it follows

Each is pinned by hash in specs.txt.

  • xHCI 1.2. Intel's own specification, and the one both benches' controllers implement. §4.3, §4.6.1.2, §4.6.8 to §4.6.10, §4.8.3, §4.12, §4.19.1.1, §4.19.1.2, §4.22.1, §6.4.1, §7.2.
  • USB 2.0. §7.1.7.3, §7.1.7.5, §9.2.6.2, §9.4.5.
  • USB 3.2 Revision 1.1. §9.2.6.1, §9.2.6.2, §9.2.6.4.
  • USB Mass Storage Class Bulk-Only Transport 1.0. §3.1, §3.2, §5.1 to §5.3.4, §6.3, §6.7.
  • UASP 1.0 and T10 UAS-2, for what is not built.
  • SPC-4 (sense data, INQUIRY) and SBC-3 (READ CAPACITY, the Caching mode page, SYNCHRONIZE CACHE).
  • HID 1.11 (§5.9, §5.10, §6.2.2, §6.2.2.5, §6.2.2.7, §6.2.2.8, §7.2, §7.2.4, §7.2.6, §8.3, §8.4, Appendices B and C) and the HID Usage Tables 1.5 (the Generic Desktop and Keyboard/Keypad pages, the latter's reserved ranges on pp.90, 94, 95 and 96).
  • PCIe Base 6.0 §6.1.4, §7.5.1.1, §7.7.1 and §7.7.2, and the PCI Code and ID Assignment Specification.
  • VT-d Rev 4.0 §5.1.4, §9.10.
  • GICv3's ITS.
  • Vendor documents. Intel's 500 Series PCH datasheet volume 2 for 00:14.0, the 11th Generation Core datasheet volume 2 for 00:0d.0, and AMD's Family 17h Model 60h Processor Programming Reference for 1022:1639. A stub Permits entry specific to a controller comes from these.

The five sites that cite another operating system, decided from the specifications

The build deletes each citation and each decision that rested on it:

  • lib/xhci/src/ladder.rs:229: RESET_RECOVERY_NS, 50 ms, rested on another implementation. Now by protocol: a USB 2 port waits TRSTRCY, 10 ms (USB 2.0 §7.1.7.5, §9.2.6.2), and a USB 3 port waits nothing (USB 3.2 §9.2.6.2). issues/the-tree-names-linux-as-an-authority.md's sweep (wt/toyos-linux-sweep) covers lib/xhci too. Whichever lands second carries the other's decision, so the value is decided once.
  • lib/xhci/src/port.rs:20: the debounce is USB 2.0 §7.1.7.3's TATTDB, 100 ms, and it applies to a USB 2 port alone. The name beside it goes.
  • lib/xhci/src/enumerate.rs:25: reading 8 bytes at full speed is xHCI §4.3.4 and USB 2.0 §9.6.1. The name beside it goes, and the module becomes straight-line code.
  • kernel/usbhid/src/keyboard.rs:21 and :27: rollover is HID 1.11 Appendix C. Now: an array field's value is an index, range-checked against Logical Minimum to Maximum and read through the field's usage list (§6.2.2.5); a key variable field is a bitmap of usages; a usage HUT 1.5 reserves (00, A5-AF, DE-DF, E8-FFFF) names no key (section 3).
  • kernel/usbhid/src/pointer.rs:17: now the wheel is the Generic Desktop Wheel usage, wherever the report descriptor puts it. Byte 3 is not read as anything by position.

This design rests on no other implementation.

Roast

  1. An async executor inside a driver hides its state machines, and ToyOS writes the executor itself.
    • The state machines exist today, written by hand: Enumeration, Recovery, Quiescing, Ladder, AfterBreak. They exist so that two drivers, one blocking and one stepped, could share an order. With one driver, the compiler writes them from code read top to bottom.
    • The real hazard is cancellation, and section 1 removes it: an operation is never dropped.
    • The executor is one thread over one poller, about 200 lines, with a virtual clock on the host. That is what makes the host tests deterministic.
  2. HID and mass storage in one process: a bug in the storage decoder kills the keyboard.
    • Class drivers in separate processes would gain no DMA isolation. The controller is one requester, so every class's data is that one function's DMA in one domain (VT-d translates by source-id).
    • They would gain only isolation of software faults, at the price of a transfer protocol and every buffer lent across a second process.
    • Every decoder is a pure crate that refuses by name and is fuzzed. A usbd death costs one enumeration, and the owner releases the keys usbd's devices held.
    • This is a weakness, still true.
  3. A full input connection loses a whole keystroke. A press and its release that both fall while the owner is not reading are never delivered. Queueing them would put a slow owner's backlog in usbd's memory, and blocking would stall the stick's ring behind the screen. They are counted by name; no key is ever left held.
  4. Report protocol for every device trusts each descriptor. A device whose descriptor lies about its fields is refused or misread, where boot protocol's fixed layout would have read it. The specification makes report protocol the default and the descriptor the definition (§7.2.6, §6.2.2), and the parser refuses what it cannot read by name.
  5. No USB 3 recovery wait. USB 3.2 lets software access the device at once after a reset. A device that needs longer is measured on the benches and filed, not given a borrowed constant.
  6. Deleting the kernel's port-reset stop.
    • The stick's survival of resets was measured only with that stop in place (issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md).
    • The usbload row is kept, rewritten, so a red reads there.
    • A red is filed. Its remedy cannot be xHCI code in the kernel.
  7. The cutover waits on two tracks and one text. P1, AMD-Vi stages 5 and 6, and issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904. S0's reading no longer holds it. Until then the kernel keeps its xHCI on both benches, and /log stays the kernel's on the stick.
  8. The supervisor grows each, and several block services.
    • The AMD bench's twin 1022:1639 controllers cannot be named by id.
    • The test images name their boot controllers by id so the spare stays the job's. A machine whose boot controller is not in that list boots its test image with no stick served, and says so.
    • The same mechanism later fixes diskserver's QEMU-only row. That change is outside this fence.
  9. port::PortState stays a state machine.
    • It is a decision about a register word, not a second driver.
    • Its explore and invariant tests are the strongest the crate has, and the T14's SuperSpeed wedge lives there.
    • The straight-line port task consults it.
  10. /log mounts later in the boot than the kernel's disk is ready today.
    • The kernel's own boot gets the 484 ms back.
    • Until /log mounts, records wait in the rings. S5 counts the ones the rings refused, and reports that count and the time /log mounted.
  11. Rewriting enumeration, recovery and the ladder risks code that works and is tested.
    • The decisions and their citations carry over, and their tests move to the stub.
    • The rows on both benches are the check.
  12. xhci-leave= lives through S2 to S4, with two drivers side by side. That is the compromise usbd drives one xHCI controller the kernel was told to leave alone (xhci-leave=), naming every device over inspect #827 already records, and S5 ends it.
  13. A console's poller watch is refused once the keyboard watch goes. Today it registers on a watch serial input never posts. The refusal is the honest answer until issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md gives the serial line a watch.
    • It changes what libc's poll answers. poll reads every completed token as ready, a refusal included (sdk/libc/src/posix_io.rs:629-638), and Poller::wait does not filter refusals. So after S5, poll on a console fd answers POLLIN at once, where today it waits. A caller that polls a console in a loop spins.
    • This is a weakness, still true. S5 files it beside the console-watch issue, since that issue's exit, a watch on the serial line, is what removes it.
  14. Two new metal rows judge with a userland program as the load. tone_beside_a_stick_write and stick_survives_a_reset_under_load each name what of theirs is the kernel's (section 9). Without them, the tone issue's exit and the reset stop's deletion would be judged by no machine.
  15. The fifth input connection is still refused. ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 takes at most four connections to the owner. With usbd connecting only while it has an input device bound, a machine is refused only when it has an i8042 and input devices on four xHCI controllers at once. The refused controller's devices deliver nothing while the owner is full, and each report retries at most once per second (section 3), so a full owner costs it one open and one drop per second per refused controller. Raising the cap is ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's owner side, outside this fence. This is a weakness, still true.
  16. A device's source is a u16 that one connection never reuses. A connection that binds 65,536 devices is dropped and opened again. That costs one resend of every device's state, and keeps a late record from ever reaching a later device.

What I am unsure of

  • ChainTooLong past the review's list. The FAT test allows it only on /a.txt at the last sync's size, and only before the sync is whole on the disk: the window between chain and entry that toyos-fat32's own stop test names ("S1, as built"). Round 2's review ruled the window a property of the design, and (a) measures it closed inside the sync.
  • The T14 cost is one reading per arm, 14,824 µs apart, from one session.
  • 00:14.0's PM No_Soft_Reset bit is unread. 00:0d.0, of the same shape, has it set. If 00:14.0's is clear, a restart's release resets the function and keeps no residue; the design holds either way, since usbd writes HCRST before its first grant.
  • Whether a held input connection can take a reopen after its owner is started again on the same port. ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's owner side is not built.
  • The 10 s disk bound is a policy, not a specification. The benches' sticks set its evidence.
  • The line counts for parts of files are estimates by reading. The whole-file counts are wc -l at 858da4d42.

Issues this closes or moves

Closed at their stage:

  • scsi-is-decoded-inside-the-xhci-crate
  • sense-data-is-read-as-fixed-format-whatever-its-response-code-says
  • a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet
  • the-boot-volumes-server-holds-a-claim-that-writes
  • a-machine-that-boots-off-nvme-looks-1500-ms-for-a-usb-boot-disk (its wait is deleted)
  • xhci-waits-are-spins
  • a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass
  • hotplug-blocks-a-scheduler-pass
  • scheduler-pass-blocks-in-xhci
  • storage-still-comes-up-before-the-supervisor-runs
  • a-megabyte-written-to-the-stick-starves-a-tone-beside-it (its row)
  • nothing-reopens-a-diskserver-session-over-a-used-page: stays open, narrowed at S1 to the client's half; S1 files a-block-session-waits-forever-on-a-server-past-its-submission-ring

Moved:

  • a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed: its register half.
  • the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-off-for-3-8-ms: a claim by location reads one function.
  • a-file-server-waits-30-s-on-a-block-port-nobody-serves-and-says-nothing: S3's bounded listing.
  • every-driver-is-still-in-the-kernel: its USB HID bullet.

a-controller-whose-msix-table-is-in-bar-0-cannot-be-driven-from-userland is P1's. The build accounts for every other USB issue at the stage that touches it.

The gates S1 claims are in "S1, as built". The other commands whose exits this body reports are the scout's QEMU run (exit 0) and the orchestrator's T14 run of usbd_s0_readout (exit 0).

🤖 Generated with Claude Code

https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Ruling on the feed question (open question 2): userland owns the merge, as ruled on #899: #899 (newest comment). usbd delivers key transitions over IPC to the userland owner of input, the inputserver #868 proposes or the compositor until it lands. There is no kernel feed claim.

@Japabu

Japabu commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Design review, round 1: head 9e78d5436

This round judges the design against origin/main at 858da4d42. That tree carries #900's track text, #902's clean-room rule and #893's sweep. It also carries the owner's ruling on Q2 (issuecomment-6137234651: userland owns the merge, and there is no kernel feed claim). The body was written at 6c4d2b3f4, before any of these.

git log origin/main..9e78d5436 is one empty commit, and git diff --shortstat is empty: 0 lines of production code, 0 of tests. The body's whole-file counts check out at 6c4d2b3f4: the eight xHCI files total 6,525 lines, and kernel/src is 63,836. The body's five Linux sites are the only Linux citations in the code it keeps or moves. lib/gpt/tests/oracle.rs names a partition type, which #902 permits.

BLOCKER

  1. PR body §3, S2, §8's feed_claims_merge_and_release and "the kernel's pure input module", §7's devices = ["keyboard-feed", "pointer-feed"]: the design builds what the owner ruled out.

  2. PR body, "What is deleted": the deletion that ruling makes possible is not named (Growth).

    • Once ps2server delivers to userland (track order item 1), the kernel xHCI's HID is the merge's last source.
    • So S7 can delete these and does not say so:
      • kernel/src/keyboard.rs (114 lines on main) and mouse.rs (190);
      • input_merge_test.rs;
      • the keyboard and mouse device classes, with their read and poll arms in object/ops.rs:284-287, :424-437 and :830-831;
      • syscall/io.rs's keyboard waits;
      • KObjectRef::Console's keyboard watch.
    • The design must name each of these. It must also say what the console image's shell reads instead.
  3. PR body §4 and S1: building P1's table-page half at 2 MiB is the route The small-kernel track says what is done, what is left, and in which order #900 ruled against, and it does not work as described.

    • The small-kernel track says what is done, what is left, and in which order #900's order item 4 runs P1 whole before step 10, and the orchestrator ruled there that P1 serves usbd's claim.
    • At 2 MiB the table's BAR has to move. But bring_up arms MSI-X before place_bars (kernel/src/pcidev/mod.rs:826-839), and arm_claimed_msix keeps an Mmio at the entry's pre-move address (kernel/src/drivers/pci.rs:482-486, held in Armed::Msix). That needs a reordered bring_up, not "about 20 lines off".
    • S1's own exit takes two bullets of P1's exit: msix-exclusive-bar=on gone, and the BAR-0 issue's exit rewritten.
    • S1 goes. S4 depends on P1.
  4. PR body S8, §4's "it is built here first because … the AMD bench needs it", and Q1: the AMD bench's dependency is understated.

  5. PR body S0: the measurement the design rests on is still unmade.

    • Whether 00:14.0's capability walk reaches its terminator decides whether the T14 cutover can happen at all. It needs one orchestrator boot with pci-readout.
    • The kernel's driver arms MSI on a truncated walk without logging it (enable_msix returns None silently on NoTable, kernel/src/drivers/pci.rs:290). So "neither printed an MSI-X not armed line" says nothing about the walk.
    • S0 is read before acceptance, not in parallel with S1 to S6.
  6. PR body §9, stick_survives_usbd_killed_mid_batch, and S7's exit, "the track's step-10 exit on the T14": the row is refused, and the exit it cites is gone.

  7. PR body, Staging and §8: the module tests the step-10 exit now owes have no stage.

    • system/fileserver/src/fat.rs's append-and-sync test, red on a sync that writes the directory entry before the chain.
    • usbd's session reopen test from issues/nothing-reopens-a-diskserver-session-over-a-used-page.md, red under the mutation that moves the stores after the answer.
    • The sim test for a keyboard on a stick's controller, red on a usbd that waits on the stick before it reads the keyboard's ring. §8's stub test has to say that it is red under that mutation.
  8. PR body §8, the cut list for S7 (usbd_drives_the_spare and usbd_drives_the_type_c_controller): after S7 nothing holds the exit's kernel part, "usbd's claims handed over again at its death".

    • The job that replaces them checks a DMA fault past a grant. That is a different claim.
    • The spare row has to be kept on the default qemu-xhci, as the track names it, or a kernel test of re-handover at a holder's death has to replace it.
  9. PR body §3 ("Boot-subclass interfaces … SET_PROTOCOL(boot)" beside the report-descriptor parser): two decode paths for one interface, a second code path.

    • HID 1.11 §7.2.6 leaves a device in report protocol unless the host asks otherwise, and the parser is built anyway.
    • With every interface bound by its descriptor, SET_PROTOCOL and the Appendix B decoders go.
    • It also settles two of the Linux sites from the specification:
      • the wheel is wherever the descriptor puts it (pointer.rs:17). Under boot protocol, byte 3 is undefined, so every boot mouse would lose its wheel;
      • a usage above the field's Logical Maximum is out of range and asserts nothing, HID 1.11 §6.2.2.7 (keyboard.rs:27). The body says only that the move "decides it", and never states the decision.
  10. PR body §5, "If the device does not come back the same, … toyos-blockring's client issues those writes again": writes issued again to a different device are data loss on that device.

    • A device that is not the same ends its sessions Gone, and nothing is issued again.
    • Writes are issued again only after the same device comes back, as identity::same says.

NOTE

  1. PR body §5, step 4, and the ladder.rs:229 site: the 10 ms is cited only for USB 2.0. SuperSpeed ports take theirs from USB 3.2 §9.2.6.2's recovery interval. issues/the-tree-names-linux-as-an-authority.md's source sweep (wt/toyos-linux-sweep) covers lib/xhci too. Whichever lands second carries the other's decision, so the value is decided once.
  2. PR body §8, "reading QEMU 11.1.1's hw/i386/intel_iommu.c": under Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902 that source is read only clean-room. It also moves to P1 with the table page. A mutation that clears VERIFY_SOURCE_ID (vtd/interrupt.rs:62) and turns the guest red is the cheaper reading.
  3. PR body §8, "partition_grant … cut at S7": Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893 already cut it.
  4. PR body §9, tone_beside_a_stick_write: no such row exists. It is new. The body must say why it is the kernel's (scheduling and windows) and not integration of soundserver and usbd.
  5. PR body §9, usbd_serves_the_boot_stick: it judges usbd's naming and serving. It should hold the kernel's part only: the claim, the message through its remapping entry, and the No-Op by interrupt. The suite's boots through usbd are the smoke test.
  6. issues/the-kernel-is-small-interrupts-post-and-threads-wait.md, step 10 ("HID to the keyboard claim"), and stage 7 step 2 ("feeding the kernel's keyboard and mouse streams"): both contradict issuecomment-6137234651. The orchestrator corrects them; this branch does not.

The five questions

  • Q1. Neither waiting nor running without /log is the whole choice. After S7 the AMD bench needs AMD-Vi stages 5 and 6 and P1's in-place placement (BLOCKER 4). Keeping the kernel's xHCI for that bench would be a second path, which is refused.
  • Q2. Ruled: userland owns the merge (BLOCKERs 1 and 2).
  • Q3. No.
  • Q4. Not allowed. Tests refuses it, and The small-kernel track says what is done, what is left, and in which order #900's step-10 exit no longer asks for it. The exit is held by the module tests in BLOCKER 7, with the kernel's re-handover in BLOCKER 8.
  • Q5. No instrument. The track's exit holds "the keyboard keeps working while a stick misbehaves" as a sim test, red on a usbd that waits on the stick before the keyboard's ring. The kernel's half is stage 5's windows reading. A programmable HID device at the benches would only drive usbd, a userland program, through steps.

Growth. Nothing is built: production 0, tests 0. The plan is about −8,150 lines of kernel/src, and BLOCKER 2 adds about 300 more to it. BLOCKER 1 also takes the roughly 150 feed-claim lines out of the 200 the body adds, so what the kernel adds is the generic reset stop (about 40 lines) and the pci@ selector (about 30).

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on round 1's five questions (#901 (comment)), as the review recommends:

The BLOCKERs and NOTEs 1-5 are taken as written. NOTE 6, correcting the small-kernel track's step 10 and stage 7 step 2 text, is mine to brief separately.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 2: round 1's review, mapped

Answers round 1's review under the rulings on its questions and the input ruling. The body is rewritten. Head 98c6ea052: the empty design commit with main at 858da4d42 merged in, and no diff against main.

Round 1's S3 to S7 are S1 to S5 now. Its S1 and S2 are gone, and its S8 folds into S5.

BLOCKERs

  1. Feed claims (§3, S2, §7, §8). Gone, with both device classes and the kernel's pure merge. usbd's row receives = ["input"] and delivers over ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's port to the owner of input (§3). The port is extended once, in S4: a device number in every record, and a per-device gone in place of LOST. A key held at usbd's death is released by the owner's Merge, held by toyos-input's module test (§8). A full connection: usbd writes with try_send and never blocks. At Full it keeps no queue, only what the owner was last told and each device's current state. When the connection drains it sends the difference, so a release is never lost, and a press and release inside the full span are counted usb.input.coalesced (§3, roast 3).
  2. The input deletion (What is deleted). Named whole: keyboard.rs (114), mouse.rs (190), input_merge_test.rs (155); both device classes with every arm in object/ops.rs, device.rs, syscall/device.rs and the SDK, std and libc; syscall/io.rs's keyboard wait; and KObjectRef::Console's keyboard watch, after which a console's poller watch is refused NotSupported rather than registered on a watch serial input never posts (roast 13). The console image's shell reads its surface from the console as today, and the console reads its input acceptor's connections in place of the kernel's claim.
  3. S1 at 2 MiB. Gone (§4). P1, whole, is the route. usbd's core, now S2, depends on P1.
  4. The AMD bench. Bridge 00:08.1's forwarded range and placement::reserve are in "Where it starts". S5 depends on P1, whole, and on AMD-Vi stages 5 and 6, and the AMD rows ride S5 (Q1's ruling).
  5. S0. The scout is built and staged: wt/toyos-usbd-s0 at b5c3b14f8 logs every xHCI function's whole capability walk and how it ended. Its judge ran on QEMU, exit 0 (log). The T14 run is requested with --metal-readback. The body says the design waits on its reading, and the MSI-X not armed sentence is gone.
  6. The crash row. Gone. S5's exit is the track's step-10 exit as main states it, quoted in Staging.
  7. The owed module tests. Staged: lib/blockserve's reopen test and fat.rs's append-and-sync test in S1, each red under its named mutation; the stub's keyboard-beside-a-silent-stick test in S4, red on a dispatch that awaits the stick's command before it routes another endpoint's event (§8).
  8. The kernel's re-handover. usbd_drives_the_spare stays, on the default qemu-xhci from S2, and usbd_drives_the_type_c_controller stays. At S5 the test images' usbd row names only the boot controllers by id, so the spare stays the job's without xhci-leave= (§7, §8).
  9. Two decode paths. One: every interface is bound by its report descriptor in report protocol. A boot-subclass interface is sent SET_PROTOCOL(Report), as HID 1.11 §7.2.6 asks of a host. Appendix B is not decoded. The wheel is the Generic Desktop Wheel usage wherever the descriptor puts it. A slot value outside Logical Minimum to Maximum asserts nothing (§5.10, §6.2.2.7), and a reserved usage is not delivered (§3, the five sites).
  10. Writes issued again to a different device. Fixed (§5). A different device, or none, ends every session Gone and nothing is issued again. Writes are issued again only after identity::same says the same device came back.

NOTEs

  1. The SuperSpeed recovery. Decided from USB 3.2 §9.2.6.2: software may access the device "immediately" after a reset, so a USB 3 port waits nothing, and a USB 2 port waits TRSTRCY's 10 ms. TATTDB is a USB 2 port's alone. The sweep coordination is in the five sites.
  2. QEMU's VT-d source. Not read. The table-page test moves to P1 with its cheaper reading, the VERIFY_SOURCE_ID mutation (§4).
  3. partition_grant. Out of the cut list; Integration tests outside the kernel are cut, and unit and module tests hold what they checked #893 cut it.
  4. tone_beside_a_stick_write. Marked new, with why it is the kernel's (§9): one claim's interrupts and threads never starving another holder's deadline, with soundserver and usbd as unchanged stimuli.
  5. usbd_serves_the_boot_stick. Holds the kernel's part only: the claim, the message through its remapping entry, and the first message on its slot. Naming and serving are the suite's smoke test.
  6. The track's text. Left to the orchestrator, as ruled. The body names the step-10 exit's keyboard clause as having no subject.

The five Linux sites, from the specifications

  • ladder.rs:229: by protocol. USB 2.0 §7.1.7.5 and §9.2.6.2 give 10 ms, and USB 3.2 §9.2.6.2 gives none.
  • port.rs:20: TATTDB, USB 2.0 §7.1.7.3, on a USB 2 port alone.
  • enumerate.rs:25: xHCI §4.3.4 and USB 2.0 §9.6.1.
  • keyboard.rs:21, :27: the descriptor's Logical range (HID 1.11 §5.10, §6.2.2.7), and HUT's reserved range (p.96).
  • pointer.rs:17: the Wheel usage wherever the descriptor puts it (HUT p.33, HID Appendix B.2).

Every specification is pinned by hash in specs.txt.

T14 RUN REQUESTED: the S0 scout's usbd_s0_readout row, staged at b5c3b14f8. Its request.txt is with the orchestrator.

@Japabu Japabu changed the title Design: the whole xHCI moves into usbd, and the kernel's USB, block layer and partition claims are deleted Design: the whole xHCI moves into usbd, its HID reaches the owner of input, and the kernel's USB, input streams, block layer and partition claims are deleted Oct 13, 2026
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

S0 on the T14, run by the orchestrator from agents/901-r2/s0-metal/request.txt at the scout b5c3b14f8: judge cargo test --test toyos-build -- --metal --metal-readback <dir> usbd_s0_readout, exit 0, 1 passed, 0 failed, 1 boot. The readback's kernel log holds every pci-readout line: /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/agents/901-r2/s0-metal/usbd-s0/kernel.log. (The request named the QEMU judge scout_usbd_s0; the metal row is usbd_s0_readout.)

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 2: head 98c6ea052

git log origin/main..98c6ea052 has two commits: the empty design commit 9e78d5436, and the merge of main at 858da4d42. git diff --shortstat origin/main...98c6ea052 is empty, so production is 0 lines and tests are 0. The plan is about −9,000 lines of kernel/src, by the body's own count.

What I read for this round:

I checked the HID citations in the text of the pinned hid1_11.pdf and hut1_5.pdf. Both SHA-256 values match specs.txt.

What S0 decides

00:14.0's walk reaches its terminator. Kernel log lines 116-126, from usbd_s0_readout at b5c3b14f8: judge exit 0, 1 passed, 1 boot, run by the orchestrator. Its shape:

  • Status 0x0290, so the capability list is present. The pointer at 0x34 is 0x70.
  • The walk is 0x70 PM → 0x80 MSI → 0x90 vendor → 0xb0 vendor → next=0x00: "walk reached the terminator after 4 link(s)".
  • msix none.
  • MSI: 64-bit, 8 vectors, no per-vector mask.
  • BAR 0 is a 64 KiB mem64 BAR.

So a claim never reaches CapsTruncated (kernel/src/pcidev/mod.rs:830). It takes the Absent arm and arms MSI (:831-833). The scout walks with the same caps::CapWalk that capability() uses.

00:14.0's shape is link for link the shape of 00:0d.0 (lines 105-115). usbd_drives_the_type_c_controller already claims 00:0d.0 on the T14.

The T14 cutover can happen. The T14 needs nothing from P1's table-page half. S5 still waits on P1, whole, for QEMU's nec-usb-xhci and for the AMD bench, and on AMD-Vi stages 5 and 6, as ruled.

Round 1's BLOCKERs

  1. CLOSED. No feed claim, no device class and no kernel merge. usbd's row receives input, and the full-connection rule is stated with its host test. One new defect in how the extension meets ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 is new BLOCKER 2.

  2. CLOSED. Every deletion is named, and I checked the line references:

    • object/ops.rs:284, :286-287, :358-363, :421-445, :684-685, :763-764, :830-831 and :905-906;
    • syscall/device.rs:173-174 and :462-463.

    The body says what the console image's shell reads instead.

  3. CLOSED. Round 1's S1 is gone. usbd's core depends on P1.

  4. CLOSED. Bridge 00:08.1's range and placement::reserve are stated. S5 depends on P1, whole, and on AMD-Vi stages 5 and 6.

  5. CLOSED by the T14 reading above.

  6. CLOSED. The crash row is gone. S5's exit quotes the track's step-10 exit, and it matches main's text.

  7. CLOSED. The reopen test and the FAT test are in S1, and the stub test is in S4. Each is red under the mutation the exit names.

  8. CLOSED. usbd_drives_the_spare stays on the default qemu-xhci from S2. At S5 the spare stays free because the usbd row does not select it.

  9. CLOSED as one path. Every interface gets report protocol and SET_PROTOCOL(Report), as §7.2 and §7.2.6 say. Those quotes are on the pinned file's pages. How that one path reads a key field is new BLOCKER 1.

  10. CLOSED. A different device ends its sessions Gone, and nothing is issued again.

Round 1's NOTEs 1-5 are CLOSED. NOTE 6 stays the orchestrator's, as ruled.

BLOCKER

  1. PR body §3, the key-field rule, and §8's toyos-usbhid tests: the one decode path does not follow HID 1.11 §6.2.2.5 or HUT 1.5.
    • Indexes and usages are mixed up. An array field "returns an index in each field that corresponds to the pressed button", and "Logical Minimum specifies the lowest index value returned by the array" (§6.2.2.5). The usage is the field's usage list at that index: Usage Minimum + (index − Logical Minimum). The range check is on the index: "an out-of-range value in an array field is considered no controls asserted" (§6.2.2.5). The body applies Logical Minimum to Maximum to the usage instead ("A key usage outside the field's Logical Minimum to Logical Maximum…"; "the keys a keyboard can report are the range its own descriptor declares"). That misreads any keyboard whose Usage Minimum differs from its Logical Minimum.
    • Only key arrays are bound. A Keyboard/Keypad-page variable field, a bitmap, is a key report too. With boot protocol never asked for, a keyboard whose report-protocol descriptor reports its keys as a bitmap is refused, and the keyboard is dead.
    • The reserved range is incomplete. HUT 1.5's Keyboard/Keypad table reserves A5-AF and DE-DF as well as E8-FFFF. The body names only 0xE8 to 0xFFFF.
    • Required: state all three rules, and add §8 tests for each:
      • a descriptor whose Usage Minimum is not its Logical Minimum;
      • a bitmap keyboard;
      • a usage in each reserved range.
  2. PR body §3, "a device number in every record", and "ps2server names its keyboard 0 and its aux device 1": ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's accepted record already names the device, and this design numbers it differently.
  3. PR body §3, "one connection per usbd", and "What I am unsure of", the four-source cap: every xHCI function's usbd takes one of ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's four sources, whether or not an input device is on it.
    • With the i8042 and four xHCI functions, the fifth source is refused, and a USB keyboard on that controller is dead.
    • The body defends the cap by the benches' counts ("three on each bench"). Fit refuses that ground: a design is chosen for the hardware ToyOS targets, not for its test machines.
    • The fix is already half in the design: usbd opens its connection when it binds its first HID interface and drops it with its last device's gone. The body's reopen path already does the opening. Then a source is a controller that has an input device.
    • If that fix is not taken, the cap has to change with ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's owner side, and the body has to say so.

NOTE

  1. PR body, "S0 is read before this design is accepted", "What I am unsure of" first bullet, and S5's "depends on" column (prose): the body still says S0 is "Staged, not yet read". It has been read since 6137990347, and the body should carry the reading. The log is a local path, so publish the masked pci-readout lines to ToyOSOrg/evidence, as ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 did for M1 and M2.
  2. PR body §6, the generic reset stop's "the vector masked": S0 reads per-vector-mask=n on both T14 controllers, so no vector can be masked. The stop has to say what it writes for each capability shape:
    • Bus Master Enable cleared (PCIe Base §7.5.1.1), which also stops MSI, since a message is a memory write;
    • and MSI Enable cleared where there is no per-vector mask.
  3. PR body §5, "the supervisor's restart is then the controller's recovery": S0 shows 00:14.0 has no PCI Express capability (so no FLR) and no Advanced Features capability, like 00:0d.0.
    • So a usbd restart on the boot controller takes the inherited-residue release that usbd_drives_the_type_c_controller reads on 00:0d.0, and HCRST is its only reset. State it.
    • PM's No_Soft_Reset bit is still unread.
  4. PR body §3, absolute motion: ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 refuses an absolute position outside 0..=32767. usbd has to scale the descriptor's Logical Minimum to Maximum into that range, and the body does not say it does.
  5. PR body §3, "It refuses by name what it cannot read": the refusal has to be per Report ID or application collection, not per interface. Otherwise a keyboard whose interface also carries a Consumer Control collection is lost.
  6. PR body, "What is deleted", the console watch refused NotSupported (roast 13): libc's poll reads every completed token as ready, a refusal included (sdk/libc/src/posix_io.rs:629-638; Poller::wait does not filter refusals).
    • After S5, poll on a console fd answers POLLIN at once, where today it waits.
    • Say so in the roast, or file it beside issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md.
  7. PR body, Staging, S5: the exit's third clause has no subject until the orchestrator corrects the track's step-10 text (round 1's NOTE 6). Put that correction in S5's "depends on", so S5 does not close against an exit it cannot meet.

The design is not ready to build: BLOCKERs 1 to 3 each need a stated decision in the body. None of them needs a new measurement.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on round 2 (#901 (comment)). BLOCKER 1 is taken as written. BLOCKER 2: the device number is #899's source, extended to the key record and to gone, with one numbering. BLOCKER 3: take the fix the review names. usbd opens its input connection at its first HID bind and drops it with its last device's gone, so a source is a controller that has an input device. The NOTEs are taken as written; for NOTE 7, the track-text correction is #904, which S5 depends on.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 3: the answer to round 2's review (#901 (comment)), under the rulings in #901 (comment). The body is rewritten in place; nothing is built, and the head is still 98c6ea052.

BLOCKERs

  1. The key field: taken as written. Section 3 now states three rules from HID 1.11 and HUT 1.5, re-read in the pinned files (hashes as in specs.txt).
    • An array reports indexes (§6.2.2.5, p.40). The range check is on the index, and the usage is Usage Minimum + (index − Logical Minimum).
    • A Keyboard/Keypad-page variable field is a key bitmap (§8.3, pp.66-67), and the modifier byte is that rule's instance. A bitmap keyboard is bound.
    • The reserved ranges are 00, A5-AF, DE-DF and E8-FFFF (HUT 1.5 pp.90, 94, 95, 96). 00 is an idle slot and is not counted; the other three are counted reserved.
    • Tests: §8's toyos-usbhid bullet gains one test per rule, each with its own mutation: Logical Minimum 1 against Usage Minimum 0x04, a bitmap keyboard, and a usage in each reserved range.
    • The fifth "site that cites another operating system" (kernel/usbhid/src/keyboard.rs:21, :27) now points at these rules.
  2. The device number is ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's source: taken as ruled. S4 extends that one field to RawKeyEvent and to gone, with one numbering. ps2server keeps its pointer at source 0, as ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's records have it, and names its keyboard source 1. usbd numbers its bound collections from 0 per connection and never reuses one. The owner keys every device by (connection, source), as ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's Pointer already does. gone replaces LOST.
  3. The cap: the named fix, as ruled. usbd opens its input connection at its first HID bind and drops it after its last device's gone. So a connection is a controller with an input device bound. A refused open is named once, its reports are counted unowned, and it is tried again at the next HID bind.

NOTEs

  1. S0 read: fixed. The "S0" section now carries the reading. 00:14.0's walk is 0x70 PM → 0x80 MSI → 0x90 → 0xb0 → terminator; it has MSI with 8 vectors and no per-vector mask, no MSI-X, no Express capability and no AF capability, and the same shape as 00:0d.0. The masked pci-readout lines are on pr-901 at 25d61f0, each with its kernel-log line number; the mask check counts 0, and the mask changed no byte. The S0 staging row reads "met", S5 no longer depends on S0, and the "unsure" bullet is gone. The scout's worktree and branch are deleted, locally and on origin.
  2. The reset stop: fixed. Section 6 now says what is written for each capability shape: Bus Master Enable cleared for every function (PCIe Base §7.5.1.1), MSI Enable cleared for an MSI function (§7.7.1), and Function Mask set for an MSI-X one (§7.7.2).
  3. Restart on 00:14.0: fixed. Section 5 now states it. A restart takes the inherited-residue release that usbd_drives_the_type_c_controller reads on 00:0d.0 (kernel/src/pcidev/mod.rs:70-90), and HCRST is the controller's only reset. PM's No_Soft_Reset bit is unread. usbd_serves_the_boot_stick's hand-over prints the release line that reads it, and the bit is listed under "unsure".
  4. Absolute motion: fixed. It is scaled from Logical Minimum to Maximum onto 0..=32767. An out-of-range value leaves the axis unchanged (§5.10), and a degenerate range is refused at parse. It has its own test.
  5. Refusal per collection: fixed. A collection is refused per top-level application collection, so per the Report IDs it carries (§8.4, p.67), and each bound collection is its own source. A keyboard with a Consumer Control collection keeps its keys. This has its own test.
  6. poll on a console: fixed in roast 13. After S5, poll answers POLLIN at once, by sdk/libc/src/posix_io.rs:629-638. This is stated as a weakness that is still true, and S5 files it beside the console-watch issue.
  7. S5 depends on issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904: fixed. That is the "depends on" column and roast 7. One discrepancy for you: issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904's body at 4068a0cda leaves step 10's exit's third clause standing and says "S1 of the xHCI move into usbd: a FAT entry that grows its file is written a cache flush after its chain and data, held by a module test that stops the disk inside a flush epoch #901 owes step 10's exit". So either issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904 carries the clause, or S5, which takes up the track issue, rewrites it to toyos-input's connection-end test. The body says the choice is yours.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Ruling on the open choice: #904 takes step 10 exit's third clause, so all the track text the input ruling changes lands in one record. S5 depends on #904 and rewrites nothing in the track.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 3: head 98c6ea052

git log origin/main..98c6ea052 is the empty design commit 9e78d5436 and the merge of 858da4d42. git diff --shortstat origin/main...98c6ea052 is empty: production 0 lines, tests 0. The plan is about −9,000 lines of kernel/src, by the body's own count.

What I read for this round:

I fetched hid1_11.pdf and hut1_5.pdf again. Their SHA-256 values match specs.txt, and I checked every HID and HUT quote and page the body gives against them: HID pp.30, 40, 42, 50, 61, 63, 64, 66, 67, 71 and 72-73, and HUT pp.90 and 94-96.

Round 2's BLOCKERs

  1. CLOSED. Section 3 states all three key rules from the pinned text:

    • the index is range-checked, and the usage is Usage Minimum + (index − Logical Minimum) (§6.2.2.5, p.40);
    • a Keyboard/Keypad-page variable field is a bitmap (§8.3, pp.66-67);
    • the reserved ranges are 00, A5-AF, DE-DF and E8-FFFF. HUT 1.5 pp.90, 94, 95 and 96 reserve exactly those, and nothing else on the page.

    §8 has one test per rule. One assertion is still missing; see NOTE 4.

  2. CLOSED. The device number is ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's source, carried in RawKeyEvent and gone with one numbering. ps2server's pointer stays at source 0, as ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 has it. The owner keys every device by (connection, source).

  3. CLOSED as ruled. usbd opens its connection at its first HID bind and drops it after its last gone, and the stub test "the connection follows the input devices" holds that. How it opens again is new BLOCKER 1.

Round 2's NOTEs

  1. CLOSED. S0's reading is in the body. The 25d61f062 lines 116-126 read as the body states: the walk 0x70→0x80→0x90→0xb0→terminator; MSI control=0x0086, per-vector-mask=n, vectors=8; msix none; no Express capability (id 0x10) and no AF capability (id 0x13). Lines 105-115 for 00:0d.0 have the same shape.
  2. CLOSED. Section 6 says what is written for each capability shape.
  3. CLOSED. The restart on 00:14.0 is stated. pcidev/mod.rs:70-83 and :85-90 say what the body cites. The PM bit is listed as unread.
  4. CLOSED. Absolute motion is scaled onto 0..=32767, with a test that is red when the raw value passes through.
  5. CLOSED. Collections are refused per top-level collection (§8.4, p.67), with a test. One parser gap is NOTE 3.
  6. CLOSED. Roast 13 states that poll answers at once, as a weakness that is still true, and S5 files it.
  7. CLOSED in S5's "depends on". The text around it is NOTE 5.

BLOCKER

  1. PR body §3, "A refused open … opens again at its next HID bind", against "The owner gone … opens a new one … at the next report", and roast 15: two rules for one state, and usbd cannot tell which one applies.
    • Opening a connection is queued on the port and never refused by the kernel (sdk/toyos/src/port.rs:7-11). ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's owner refuses a fifth source "by name", by dropping it. So usbd sees a refused open exactly as it sees an owner that went away: its connection ends while an input device is bound.
    • The body gives that one state two triggers to open again: the next HID bind, and the next report. The first one is wrong behaviour. A keyboard that is already bound stays dead after a slot frees, until it is unplugged and plugged in again. That is roast 15's "deliver nothing until its next HID bind", and it is chosen, not forced.
    • Required: one rule for "an input device is bound and there is no connection", with one stub test. It must be red under the HID-bind-only trigger. Example: the owner drops the connection while a key is held; the next report opens a new connection, and the owner receives the current state. A second drop is counted usb.input.unowned, and the report after it tries again.
    • Say what bounds the retries. A report-rate open-and-drop loop against a full owner is the cost, and it belongs in roast 15.
  2. PR body §3, "a pointer's buttons" and "usbd never blocks on the owner", against ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's MouseEvent: one device's report can take down every device on its controller.
    • ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899 refuses "a button outside boot-mouse order's five bits", and "a frame that does not decode drops its source by name". The source is usbd's whole connection.
    • The body binds the Button page with no bound. A mouse whose descriptor declares Usage Minimum 1 to Usage Maximum 8 sends bit 5 on button 6. The owner drops the connection, every key held on every device of that controller is released, and under BLOCKER 1's rule each later report reopens the connection and is dropped again.
    • This is the sibling of round 2's NOTE 4, which the revision fixed for absolute positions only.
    • Required: every field is bounded to the width of ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's record before it is sent:
      • Button usages past 5 are counted by name and not delivered;
      • a wheel field wider than the record's scroll saturates, as the summed motion already does.
    • Test: a toyos-usbhid test with an 8-button descriptor. Pressing button 6 delivers nothing and counts it. The test is red when bits past 5 are passed through.

NOTE

  1. PR body §3, "ps2server's overrun sends gone for source 1" — ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's LOST has a second trigger, "a byte it had to drop", which the mapping leaves without a record. Say that it sends gone for the device the byte came from (AUXB set → source 0), and change ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's stream.rs test to match.
  2. PR body §3, rule 2 — a bitmap that starts at Usage Minimum 0, as boot-like full-rollover descriptors do, has controls at 00-03. HUT 1.5 p.96, note 1, says 01-03 are "Sent as a member of the keyboard array. Not a physical key". Rule 1 handles them in arrays only. Say that a bitmap control at 00-03 is never delivered as a key, so ErrorRollOver never reaches the owner as a press.
  3. PR body §3, "A report belongs to one [collection]" — §8.4 forbids a Report ID that spans two top-level collections, but a device can still declare one. Name the parser's answer, which is to refuse the interface by name, and add it to the refusal tests. Otherwise the per-collection refusal binds one half of a split report.
  4. PR body §8, "an array whose Usage Minimum is not its Logical Minimum" — the listed assertions do not make the mutation "range check made on the usage" red in every case. Under that mutation, index 1 still delivers 0x04 and index 0x63 still asserts nothing. Index 0 maps to 0x03, which refuses the report, and that differs from "asserts nothing" only if a key is held beforehand. Add "index 0x62 delivers 0x65": a usage-side check against 1..0x62 refuses it, so the test is red under that mutation whatever the order.
  5. PR body, Staging (S5's exit, "Which of the two is the orchestrator's call") and "What I am unsure of", third bullet (prose) — 6138651083 ruled that issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904 takes step 10 exit's third clause, and 6138651351 widened issues: the kernel-is-small track's input steps and exits deliver to the owner of input over the input port, and step 10's exit checks the kernel's streams deleted #904's fence to carry it. The body still presents this as open.

The design is not ready to build. Each of the two BLOCKERs needs a stated decision in §3 and one host test, and neither needs a new measurement. Once they are in, nothing I read in the rest of the design stands in the way of S1.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on round 3 (#901 (comment)).

  1. BLOCKER 1, one rule. While an input device is bound and usbd holds no connection, the next report opens one and the owner receives the current state. A drop is counted usb.input.unowned. Reopening is tried at most once per second, so a full owner costs one open per second per controller and not one per report. Roast 15 states it. One stub test holds it, red under the HID-bind-only trigger.
  2. BLOCKER 2: every field is bounded to ps2server: the i8042 driver leaves the kernel, and the owner of input merges every source #899's record before it is sent, as the review writes it, with the 8-button test.
  3. NOTEs 1-5 are taken as written.

With these the design is accepted and goes to build. No further design review: the build review checks them.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

S1 held before building: as staged, it ships machinery that nothing in any image uses until S3 or S5. The head is still 98c6ea052, and nothing is built. The body is updated for round 3 as ruled (comment 6138717771).

What the tree shows at 98c6ea052:

  • each and pci@ have no row to serve. Every image's one block-service row is diskserver's, and it names devices = ["pci:1b36:0010"]: images/system.toml:228-232, images/console.toml:83-87, and all eight tests/images/*case.toml that start it. usbd's only row (tests/images/testcases.toml:64) is empty, because usbd_spare starts it. No row would take each until S5's rows (section 7).
  • "Several block services" has no second service. One row per image serves block, and the supervisor asserts as much (system/supervisor/src/main.rs:879). fileserver's plural path and Grants's list of block services would serve only one entry until usbd serves block@, at S3.
  • "A listing answered once settled" has nothing to wait for. diskserver reads its table before it serves its first connection (system/diskserver/src/main.rs:625 and then :638). So the only block service is settled before it accepts anyone, and the gate would have no state to gate until usbd's bus at S3.
  • lib/blockserve would have one user until S3. reviewer.md's Fit allows a crate of its own only for what two programs share.

Under root CLAUDE.md ("Code is liability") and reviewer.md (Fit, and Growth's "an abstraction with one caller, a parameter with one value"), each of these is code with no user when it lands. S1's own exit says this: "diskserver's guests unchanged; the manifest and blockserve host tests". Only tests would exercise it, and "a diff that ships something for a test alone is a BLOCKER".

What does have a user now: the two module tests the step-10 exit owes. They are the FAT append-and-sync test on system/fileserver/src/fat.rs, and the reopen of a session over the page a killed server left, which today is diskserver's (issues/nothing-reopens-a-diskserver-session-over-a-used-page.md). Both test code that ships today.

Two ways to re-stage it. The choice is the owner's or the orchestrator's; I have built neither:

  1. S1 becomes the two module tests, on today's diskserver and fileserver. each, pci@, the plural block services, the settled listing and lib/blockserve move into S3, where usbd's block@ is their second user. S3 already depends on S1 and S2, so the cutover's order does not change. The cost: S3 grows, and it waits on P1.
  2. S1 gives its machinery a user now. diskserver's rows take each = ["pci-class:010802"] (NVMe), which ends the QEMU-only row roast 8 names. That change is outside this fence, as roast 8 says. It also reaches metal: on the T14 and the AMD bench, diskserver would claim every NVMe function and list its partitions. That needs its own design and roast.

Nothing in sections 1 to 9 changes either way. Only the Staging table does.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Ruling on the S1 hold (#901 (comment)): option 1. S1 becomes the two owed module tests alone: the FAT append-and-sync test, and the session reopen test. each, pci@, several block services, listing once settled and lib/blockserve move into S3, where usbd's block@ is their second user. The body's Staging says so. Build S1.

Japabu added a commit that referenced this pull request Oct 13, 2026
…release, not a keyboard claim

Step 10's exit named "the keyboard's by stage 7 step 2's kernel test on
usbd's claim", and stage 7 step 2 refused a keyboard claim while no
source existed and owed a kernel test of a keyboard claim handed to the
next claimant. Under the input ruling (#899, applied to usbd on #901)
neither driver holds a keyboard claim, so all three presupposed a kernel
feed that is gone.

Step 10's exit now holds a key usbd held at its death to the owner of
input's `toyos-input` release test, as #901's accepted design maps it.
Stage 7 step 2 names ps2server's one kernel claim, the `isa` row, and its
kernel test is `isa_lines_reach_their_holder`'s dead holder leaving no
record to the next, as #899's accepted design maps it. Taken here by the
ruling on #904 that widened its fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…s chain and data

The cache writes its dirty blocks lowest first, and a FAT volume's
directory clusters lie below the data it allocates later. So a sync put
the grown entry, the chain and the data in one flush, and the disk took
the entry before the clusters it names: a machine stopped there mounts a
file whose appended bytes are whatever those clusters held. A disk with a
volatile write cache keeps no order between writes until a flush either.

`level` now flushes the cache before it writes an entry that reaches more
than the disk holds (`File::needs_reconcile`), so the chain and the data
are durable before the entry is in the cache.

The module test the step-10 exit owes holds it: an append and its sync,
stopped after each block write in turn, mount and read back either the
last whole sync's bytes or the append's, never anything else. Without the
flush it is red at block write 9 of 13: a 15,288-byte file that is
neither the 3,000 bytes the last sync held nor the append.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 13, 2026
… owner of input, not the screen's

Review round 1 on #904. Step 10's exit now fails while `keyboard.rs`,
`mouse.rs`, `input_merge_test.rs` or a `Keyboard`/`Mouse` class in
`sdk/abi` remains. Step 10 and stage 7 step 2 deliver to the owner of
input, as the ruling names it, not the screen's owner. The `toyos-input`
test's controls are #901's two, exactly. Stage 7 step 2 states the
kernel merge's deletion in #899's words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu
Japabu force-pushed the wt/toyos-usbd-xhci branch from 98c6ea0 to 92c22a2 Compare October 13, 2026 05:41
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Accepted: shape A for the reopen test. diskserver's open order moves into one function that a host test calls, red under the mutation that moves the stores after the answer. The FAT fix, which flushes before writing an entry that grows its file, is accepted as built. Its second device flush is a cost to measure on the T14 when S1 is staged for metal. Finish S1.

Japabu and others added 2 commits October 13, 2026 07:45
…one function a host test reopens a session through

A client that opens its session again sends the page its killed server
left, and looks at it the moment it hears. So the server's stores of its
two cursors come before its answer. That order spanned `Service::open`,
which made the ends, and `handshake`, which sent the answer, and both
stand on syscalls, so no host test reached it.

`answer` now makes the ends and only then calls the closure that tells
the client; `handshake` passes it the `MSG_OPENED` send. Its test leaves
a used page, a request asked, taken and answered and the server gone,
makes the client's ends again as `Session::reconnect` does, and looks at
the page from inside the answer: room for every request and nothing to
read. With the answer told before `Opened::over`, it reads
`Err(HeadPastTail)`.

Closes issues/nothing-reopens-a-diskserver-session-over-a-used-page.md:
the test is the one its exit and the track's step-10 exit name. What the
client glue does with such a page is still read off the code, and is
issues/nothing-runs-a-block-sessions-client-glue-on-a-host.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu Japabu changed the title Design: the whole xHCI moves into usbd, its HID reaches the owner of input, and the kernel's USB, input streams, block layer and partition claims are deleted S1 of the xHCI move into usbd: a FAT entry that grows its file is written a cache flush after its chain and data, and diskserver tells an open only once its ends are made, each held by its module test Oct 13, 2026
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

T14 at 671e5c736, run by the orchestrator: S1 and its base. The base is S1 with the FAT flush removed (local commit 8e39da48e). Each ran metalcase and metaldevicecase and was judged with usb_reset_hands_devices_back. S1's judge exited 0, and so did the base's.

The extra flush's cost, from the timed write and sync of a grown file on /log (exit: usbwrite … code=<µs>): S1 70,480 µs, base 55,656 µs, so about 14.8 ms more, about 27%. That is one sample each, on the T14's USB stick. Accepted: the order a crash cannot corrupt costs one device flush.

@Japabu

Japabu commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Build review, S1: head 671e5c736

This is the first build round. The design rounds' BLOCKERs were settled by ruling 6138717771 and are not judged again here.

git log origin/main..671e5c736 has three commits: 92c22a2c6 (the FAT flush and its test), 4238b8b7b (a merge of 52e32385d) and 671e5c736 (answer and the reopen test). git diff --shortstat origin/main...671e5c736 gives 5 files, +176 −58:

  • production +40 −16: diskserver +31 −15, fileserver +9 −1;
  • tests +107: diskserver 28, fileserver 79;
  • issues +29 −42.

What I read for this round:

  • every changed file whole at the head;
  • cache.rs, session.rs, disk.rs, lib/fat32/src/fs.rs (write, flush_meta, reconcile, needs_reconcile), lib/blockring/src/wire.rs and transport/src/queue.rs;
  • the body, all 15 comments, evidence pr-901 at e88190bd8 (every s1b/ log and patch);
  • both T14 readbacks under agents/901-s1b/t14-{s1,base}.

What holds:

  • Gates. --ci host exits 0 ("76 step(s), all green"). The guest suite passes 53 of 53 with exit 0. --build-only exits 0.
  • Mutations. Each patch is applied at 671e5c736, and each log ends RESTORED_CLEAN=yes.
    • fat-m1 (the whole FAT change reverted) gives 101 at block write 9 of 13.
    • fat-m2 gives 101 with Err(Io) at 10 of 13.
    • fat-m3 gives 0.
    • reopen-m1 gives 101 with left: Err(HeadPastTail).
  • The T14 readbacks agree with comment 6139484438. Both arms report PASS usb_reset_hands_devices_back, "1 passed, 0 failed, 2 boot(s)" and "0 past its record".
    • S1 at 671e5c736: exit: usbwrite pid=9 code=70480 (metaldevicecase kernel.log:356).
    • The base at 8e39da48e: code=55656.
    • The difference is 14,824 µs.
  • The fix is correct as I read it. toyos-fat32's write leaves the entry alone until flush_meta. Cache::flush ends in disk.flush(), which cache.rs's a_write_reaches_the_disk_at_the_flush_and_not_before holds (flushes == 1). So the chain and the data are durable before the entry is dirty. A later self-flush at DIRTY_LIMIT, or another node's level in the same sync, writes that entry only after the barrier.
  • The one other caller of flush_meta on a live machine is system/update/src/install/mod.rs:168. It writes the unmarked idle slot, and its torn state is refused by the slot table that comes after it. That makes it no sibling.

BLOCKER

  1. PR body, "The extra flush's cost on the T14: staged, not read", and comment 6139484438 — the T14 readings are neither published nor cited (Evidence).

    • ToyOSOrg/evidence pr-901 ends at e88190bd8. It holds only the two staging logs, which exit 2 (Verdict::Staged).
    • The body still says the cost is "staged, not read".
    • Comment 6139484438 gives the numbers with no log, no exit code and no link.
    • The orchestrator ordered this cost measured on metal (6139084424), so the reading is evidence this change owes.
    • Needed: publish masked, as S0's lines were, so the judge logs' machine-model and BIOS strings stay out:
      • both arms' judge logs with their exit codes;
      • both arms' ===TEST_END usbwrite and exit: usbwrite lines;
      • both arms' heads.
    • Then cite them in the body in place of "staged, not read".
  2. issues/nothing-reopens-a-diskserver-session-over-a-used-page.md (deleted) — a close whose exit is not met.

    • The exit asks for "the restart test that Stage D … owes for blockd_survives_its_death … red with that Io under a mutation that moves blockd's two stores after its answer".
    • Shape A runs no Session and no caller. It is red with the transport's Err(HeadPastTail) (main.rs:913), not the caller's Io.
    • Stage D (issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md:114) still owes blockd_survives_its_death.
    • Comment 6139084424 accepts shape A as the reopen test. It does not rule that the exit narrows to the server's half, or that the Io clause moves.
    • Commit 671e5c736 says "the test is the one its exit … name[s]", which is false on the Io clause.
    • Needed, either:
      • the orchestrator rules on the PR that the exit is met by the server's half, with the caller's half moved to the new issue (BLOCKER 3 then has to hold); or
      • the issue stays open.
  3. issues/nothing-runs-a-block-sessions-client-glue-on-a-host.md:15 and :19 — the new issue says something false of the tree, and its exit names a check that cannot hold its scenario.

    • Violation::HeadPastTail is a producer's violation (transport/src/queue.rs:75, space). The client meets it only in Session::pump, at session.rs:231, as Err(_) => break, and that does not end the session.
    • drain (session.rs:286) checks only the completion ring: a pop violation, or a Completion the client refuses (Tag).
    • So "a page whose server cursor is past what the client published" does not end the session. It leaves wait blocked in poller.wait(1, u64::MAX) (session.rs:273) until the server hangs up. That is an unbounded wait on an untrusted server.
    • "Red with that check in Session::drain removed" cannot hold that scenario.
    • A real reopen ends only because the stale completion fails Tag in drain.
    • Needed: the issue names the two checks as they are, and its exit has a red condition that holds each one. Either it records the submission-ring hang with an exit, or that hang is filed as its own defect.
  4. system/diskserver/src/main.rs:732 (answer) and :894 (its test) — this ships for a test alone, and it guards something a reader already sees (Fit, Growth).

    • Opened has two constructors. Opened::over makes the server's ends and the answer together. The other is the client's decode. The compile_fail doctest at wire.rs:56 refuses any other.
    • The only mutation that turns the test red (reopen-m1) makes the server forge its answer with Opened::decode. A reader catches that in the diff, and root CLAUDE.md says a mutation a type refuses is not tested.
    • What the test needs is answer's generic tell: impl FnOnce(&Opened) -> R and its (ServerRings, Opened, R) return, with one production caller (the roast says so). That is +31 −15 production lines that change no behaviour.
    • Needed:
      • Opened::over is made inline at the MSG_OPENED send in handshake. Moving it out of Service::open may stay.
      • answer and the test are deleted.
      • If the decode escape must close, it closes by type: decode returns a client-side value the server cannot send.
    • The track's step-10 text names this test, so the orchestrator rules on it.
  5. system/fileserver/src/fat.rs:189-191 — the production change was built before it was accepted.

    • The fix was first committed as b003a26e7 at 05:21:55Z, by the branch's reflog; 92c22a2c6 is that commit amended and rebased.
    • Comment 6139084424 accepts the flush "as built" at 05:41:33Z.
    • The accepted design (6138717771, 6138755815) staged S1 as the two tests alone, with no production change.
    • Nothing in the tree closes this. Whether an acceptance after the fact stands is the orchestrator's ruling, and it should be stated on the PR as one.

NOTE

  1. system/fileserver/src/fat.rs:796-802 — the body names "fatgen103, through the checker's fixture" as the oracle, but each stop is judged by the driver under test.

    • The fixture is independent; FatVolume::mount and read are not.
    • toyos-fat32-check is the tree's declared outside judge for every volume the project writes, and its ChainTooShort is exactly fat-m2's defect.
    • Needed: take it as a dev-dependency and run check on each stop's image. The complaints a stop may leave (LostChain, FsInfoFreeCount, and FatMirror between the two FAT copies) are allowed by name, and every other complaint is red.
  2. system/fileserver/src/fat.rs:737-758 and :8-11 — Traced does not record flush, so every stop is a prefix of an in-order trace.

    • The module header's "the disk keeps no order until a flush" is therefore held only together with cache.rs's flush-count test. fat-m3's 0 holds only under the prefix model.
    • Needed: either the body says so, or Traced records flush epochs and the test stops on any subset of one epoch's writes.
  3. PR body, "The reopen test: shape A" ("It is the test issues/nothing-reopens-… owed") and "S1, as built" ("The cost on the T14 is staged below and not yet read") — prose, false of the record as BLOCKERs 1 and 2 say.

  4. agents/901-s1b/t14-{s1,base}/judge-*.log — the [metal] the timings line carries the machine's vendor, model and BIOS strings. Mask them before publishing (BLOCKER 1).

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on the S1 build review (#901 (comment)).

  1. BLOCKER 4: answer and its test go. Opened::over holds the server's order by type, made inline at the MSG_OPENED send. If decode's escape must close, it closes by type.
  2. BLOCKERs 2 and 3: issues/nothing-reopens-a-diskserver-session-over-a-used-page.md stays open. Its exit narrows to the client's half: a reopen over a used page ends the session with Io by drain's Tag refusal, held by a client host test, red with that refusal removed. The new client-glue issue folds into it, so there is no sibling. The submission-ring hang, wait blocked forever on an untrusted server whose cursor is past the client's, is filed as its own defect with an exit.
  3. BLOCKER 5: the FAT flush is accepted after the fact. It is correct, it is held by the test and its mutations, and it is measured on the T14 at about 14.8 ms. Building it before acceptance was out of order, and this ruling is the record.
  4. BLOCKER 1, NOTEs 1-4: taken as written. The fixture is judged by toyos-fat32-check, Traced records flush epochs, and the logs are masked.

Japabu and others added 2 commits October 13, 2026 08:58
…en issue keeps the client's half

`answer` and its host test go (ruling 6139586864, from review
6139576035's BLOCKER 4): `Opened::over` already makes a server's ends of
the session page and its answer together, and nothing but `Opened::over`
or the client's `decode` makes an `Opened`, so the order the test held
is a type's. `handshake` now calls `Opened::over` at the `MSG_OPENED`
send; `Service::open` no longer makes the ends before the claim is told.

`issues/nothing-reopens-a-diskserver-session-over-a-used-page.md` stays
open, narrowed to the client's half: a reopen over a used page ends the
session with `Io` by `drain`'s `Tag` refusal, held by a client host test
red with that refusal removed. The client-glue issue this branch had
filed folds into it. The submission-ring hang the review found
(`Session::pump` stops on `HeadPastTail` and nothing ends the session, so
`wait` blocks in `poller.wait(1, u64::MAX, ..)`) is filed as its own
defect, `issues/a-block-session-waits-forever-on-a-server-past-its-submission-ring.md`.
The kernel-is-small track's two citations name the type and the issue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…n103's checker judges each stop

Review 6139576035's NOTEs 1 and 2, taken by ruling 6139586864.

`Traced` keeps the blocks it is written per flush epoch, and the test
stops with every earlier epoch on the disk and any subset of one epoch's
writes: the disk keeps no order between flushes, so a prefix of an
in-order trace was a model of the cache's order, not the disk's. 70
stops on this append and its sync.

Each stop's image is judged by `toyos-fat32-check`, written from
fatgen103 and sharing no code with `toyos-fat32`, before the driver
mounts it. Allowed by name: `LostChain`, `FsInfoFreeCount` and
`FatMirror`, which the review named, and `ChainTooLong` on `/a.txt` at
the last sync's 3000 bytes alone. That last one is the window the
module header names between a growing file's chain and its entry, and
the one `toyos-fat32`'s own `a_stop_at_any_write_leaves_only_the_named_windows`
admits; without it the fixed code reds at its first stop past the chain
("DIR_FileSize is 3000 bytes, which needs 6 clusters, and the chain
holds 30"), which is the order this branch chose and not a defect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu Japabu changed the title S1 of the xHCI move into usbd: a FAT entry that grows its file is written a cache flush after its chain and data, and diskserver tells an open only once its ends are made, each held by its module test S1 of the xHCI move into usbd: a FAT entry that grows its file is written a cache flush after its chain and data, held by a module test that stops the disk inside a flush epoch Oct 13, 2026
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Build review, S1 round 2: head 17ea52983

git log origin/main..17ea52983: 92c22a2c6, 4238b8b7b (merge), 671e5c736, 8eb4cf09f (merge of bc8429e71), 7978cdecf, 17ea52983. git diff --shortstat origin/main...17ea52983: 7 files, +199 −45. Of that, production is +20 −14 (diskserver +11 −13, fileserver +9 −1), tests are +125 in fat.rs plus a 5-line dev-dependency and one Cargo.lock line, and issues are +48 −31. The production growth is the accepted barrier (ruling 6139586864, item 3).

Evidence read: pr-901 s1c/ at fbcd4102c (every patch and log) and 9602562ca (guest.log).

  • --ci host: EXIT=0, "76 step(s), all green", and the FAT test ran (ci-host.log:8702).
  • --build-only: EXIT=0.
  • Guest suite: EXIT=0, 53 passed of 53.
  • fat-m1: 101. fat-m2: 101 (ChainTooShort). fat-m3: 0. fat-m4: 101. Each ends RESTORED_CLEAN=yes.

The earlier BLOCKERs (6139576035)

  1. CLOSED. t14-s1.log and t14-base.log are published masked (<a bench>). Each has its head, the judge log, PASS usb_reset_hands_devices_back, and the usbwrite lines (code=70480 against code=55656). The body cites both in place of "staged, not read".
  2. CLOSED as ruled. The reopen issue stays open, and its exit is narrowed to the client's half, as 6139586864 item 2 words it.
  3. CLOSED as ruled. The client-glue issue is folded into the reopen issue. The submission-ring hang is filed as its own defect, with an owner and an exit a host test can fail.
  4. CLOSED. answer and its test are gone. Opened::over is made at the MSG_OPENED send (system/diskserver/src/main.rs:772). Every guest boot that starts diskserver goes through it, and the suite is green at the head.
  5. CLOSED by ruling 6139586864, item 3.

The earlier NOTEs 2 to 4 are CLOSED. NOTE 1 is closed except for the departure, which is BLOCKER 1 below.

The departure: the ChainTooLong allowance

The window is a real property of the design, and admitting it is right. An append changes two sectors, the FAT link and the directory entry, and the disk orders them only across a flush. A stop between the two writes therefore leaves exactly one of them on the disk.

  • With the chain first, the stop leaves ChainTooLong: the old size, with every byte it names present.
  • With the entry first, it leaves ChainTooShort: a size naming clusters that were never linked.

fat-m1 and fat-m2 measure the second case red (DIR_FileSize is 15288 bytes … the chain holds 6). toyos-fat32's a_stop_at_any_write_leaves_only_the_named_windows (lib/fat32/tests/refused_writes.rs:618) admits the first case as that crate's named window. The review's three-name list was incomplete.

The allowance as written covers more than the window, though. It holds at every stop, including the one after the last flush, where the second sync is whole on the disk. At that stop the test still accepts ChainTooLong on /a.txt at 3,000 bytes, and it accepts out == held. So a sync that returns Ok and never makes the append's size durable passes. That is the durability defect the allowance hides.

The runs that tell the two apart. The implementer runs them. I did not run them, because my role prompt forbids me to run any test or build, and I am raising that against the brief here rather than working around it.

  • (a) Is the window confined to the sync? Tighten the test: at the stop with every epoch on the disk, check(&bytes) must return nothing and the file must read whole. Allow ChainTooLong only at stops before that one. Run the tightened test at the new head. Exit 0 measures the window as confined to the inside of the sync, which makes it a property of the design. Red would mean the fixed code leaves the window open after a sync that returned Ok, which is a defect in the fix.
  • (b) Does the allowance hide a loss? Apply this patch and run cargo test -p fileserver --lib an_append_stopped once against the test as it is at 17ea52983, and once against the tightened test:
    --- a/lib/fat32/src/fs.rs
    +++ b/lib/fat32/src/fs.rs
    @@ fn record_meta
    -        raw.set_size(f.size);
    +        if f.entry_cluster.is_none() {
    +            raw.set_size(f.size);
    +        }
    Under this patch the first sync records 3,000, and the append's sync rewrites the entry with its old size. So the final image is chain 30, entry 3,000, and the sync returned Ok. My reading predicts that the current test exits 0 under it, which would show the allowance hiding the loss, and that the tightened test exits 101 at its final stop with ChainTooLong. Other tests also catch this patch (a_file_that_will_not_level_costs_only_its_own_file asserts 6000). It serves here only as the instrument that separates window from defect. The BLOCKER itself rests on the test's own claim.

BLOCKER

  1. system/fileserver/src/fat.rs:838 and :850: the ChainTooLong allowance and out == held hold at every stop, including the one with every flush epoch on the disk. So the test cannot fail on the claim in its own doc (:800) and in the track's step-10 exit: the stop "reads back all the last whole sync held", and after the last flush that is the append.
    • Needed: the final stop is held to check returning nothing and to out == whole. The allowance applies only before the append's entry epoch is whole on the disk.
    • Measurements: (a) green at the new head, and (b) both arms, each with its command, exit code and log on pr-901.
  2. PR body, "the fixed code is red at its first stop past the chain: stopped in flush epoch 2 of 5 with writes 0b1 of its 6 …" (Evidence): this run is the ground for departing from a ruling, and no log, command or exit code backs it. s1c/ has no run of the three-name list at the head. Publish it.

NOTE

  1. PR body, "S1, as built", the allowance bullet (prose): it presents the review's list as wrong without saying that the allowance also covers the stop after the last flush. Once BLOCKER 1 is fixed, say where the allowance stops.

SEND BACK

… the append's bytes

The stop with every flush epoch on the disk is the sync whole. The
ChainTooLong allowance, the window between the chain's epoch and the
entry's, and the last sync's bytes are admitted only before it; at it,
fatgen103's checker refuses nothing and the file reads as the append made
it. Before, the allowance held at every stop, so a sync that returned Ok
and never made the append's size durable passed.

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

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Build review, S1 round 3: head c7201d375

git log origin/main..c7201d375: 92c22a2c6, 4238b8b7b (merge), 671e5c736, 8eb4cf09f (merge of bc8429e71), 7978cdecf, 17ea52983, c7201d375. The merge base is origin/main at bc8429e71. git diff --shortstat origin/main...c7201d375: 7 files, +203 −45. Production is unchanged since round 2 (+20 −14). Tests grow by 4 lines net: 17ea52983..c7201d375 is +7 −3, all inside fat.rs's mod tests. Issues are unchanged.

Evidence read: pr-901 s1d/ at 66873012f. That is every patch, script and log, ci-host.log and build-only.log included.

The earlier BLOCKERs (6140441862)

  1. CLOSED. The fix is at system/fileserver/src/fat.rs:833: synced_whole is true for the stop with every write of the last non-empty epoch on the disk and only empty epochs after it. At that stop, _ if synced_whole => true admits no complaint, and out == whole || out == held && !synced_whole holds the read to the append's bytes. The ChainTooLong allowance now applies only before that stop.
    • (a) a-tightened.log: HEAD=c7201d375…, cargo test -p fileserver --lib an_append_stopped, the test ok, EXIT=0. The window closes inside the sync. The same test is also ok in ci-host.log:8701 at the same head.
    • (b), old arm b-at-17ea52983.log: fat.rs at 17ea52983 with the record_meta patch applied (the diff is in the log), EXIT=0. The old allowance hid the loss, as round 2 predicted.
    • (b), new arm b-tightened.log: the same patch at c7201d375, EXIT=101. It fails at stopped in flush epoch 3 of 5 with writes 0b11 of its 2: /a.txt: DIR_FileSize is 3000 bytes, which needs 6 clusters, and the chain holds 30. That is the last non-empty epoch, whole on the disk, so this run also shows that synced_whole fires at the stop it names.
    • Both (b) arms end with RESTORED_CLEAN=yes.
  2. CLOSED. three-names.log at c7201d375 applies three-names.patch, which removes only the ChainTooLong arm. It exits 101 at stopped in flush epoch 2 of 5 with writes 0b1 of its 6: … the chain holds 30, with RESTORED_CLEAN=yes. The PR body's quoted line matches the log, and the body cites the log, the patch and the head.

The round-2 NOTE (the allowance bullet did not say where the allowance stops) is CLOSED. The bullet now reads "only before the sync is whole on the disk".

Gates at head

  • Host suite: ci-host.log at c7201d375 reads "76 step(s), all green", EXIT=0. Every FAILED in it falls inside a [ci] control step that reached its verdict.
  • Build: build-only.log, EXIT=0.
  • Negative control: negative-control.log at c7201d375 removes the fileserver's flush and exits 101 at epoch 1 of 3 with writes 0b1000 of its 8 … the chain holds 6, with RESTORED_CLEAN=yes.
  • Guest suite: it stands at 17ea52983, 53 of 53 (s1c/guest.log). The only change since then is inside #[cfg(test)], which no guest image reaches.

BLOCKER

None.

NOTE

  1. PR body, "Each command is cargo test -p fileserver --lib an_append_stopped, run by run.sh and run2.sh": the (a) run is in neither script. run.sh has arms only for b-at-17ea52983, b-tightened and three-names, and a-tightened.log has no ARM= or RESTORED_CLEAN line (prose).

LAND

@Japabu
Japabu marked this pull request as ready for review October 13, 2026 07:29
@Japabu
Japabu added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit a3c471b Oct 13, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-usbd-xhci branch October 13, 2026 07:45
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