Repository navigation
blockd sets its ring cursors before it answers an open - #643
Conversation
`blockd_survives_its_death` has two red signatures. Neither is a write blockd lost or reordered; the second is a defect in blockd's open, fixed here. 1. "the blockd after it wrote [281592, 282600] first" (the deleted issue). Every red it cites predates the judge that reads it now: 8c5be84 and a55d62c do not contain 17bb264 (#534, "a reissue keeps the order of the writes that overlap"), and the issue was filed at 59bd29f, which does not either; its branch merged #534 thirteen minutes later (672c499). The two writes are one block each, 1008 sectors apart, so the judge since #534 takes [281592, 282600] as a reissue of [282600, 281592]. What reorders them is QEMU, not blockd: - the client reissues in first-acknowledged order and puts the two on the wire together, since they do not overlap (`Client::next_request` keeps an overlapping pair apart: `overlapping_writes_go_out_again_in_order`); - blockd's `roomiest` takes the last of equally empty queues, so the first goes down SQ 4 (the nightly log's "WITHHELD ... queue 4 identifier 0" is that choice) and the second down SQ 3; - QEMU v11.1.1's `nvme_process_db` schedules each SQ's bottom half, and `aio_bh_enqueue` inserts at the list's head while `aio_bh_poll` dequeues from it: two doorbells rung before the main loop polls run their SQs last-first, and `pci_nvme_write` is traced as each SQ is processed. So the trace reads [281592, 282600] exactly when both doorbells land inside one poll. NVMe promises no order between commands outstanding at once (NVM Express 1.4, §6.3 "Command Ordering Requirements": each command "is processed as an independent entity without reference to other commands submitted to the same I/O Submission Queue or to commands submitted to other I/O Submission Queues", and a host that needs an order enforces it above the controller), and two writes to disjoint blocks leave the same medium in either order. Across the orchestrator's logs on this host, 32 passes, all TCG, every one tracing [282600, 281592]; CI's reorder is KVM's timing. The version of Debian sid's QEMU on CI was not measured. 2. "blockd_io: FAIL /AFTER.BIN after the restart: Io" (nightly 36753172688, guest 7). blockd answered MSG_OPENED and only then made its ends of the session page (`admit` called `layout::server`). A fresh region is zero, so a first open cannot tell; a reconnect sends the page its last server wrote, whose SQ head and CQ tail are still that server's. `Session::reconnect` pumps the moment it hears, and its first wait drains: a client that gets there before blockd's `admit` finds no room to ask (the stale head is past its zeroed tail) and a completion ring holding the dead session's completions. The first one names a tag the client no longer has in flight, `Client::complete` answers `Violation::Tag`, `Session::wait` ends the session, and the FAT32 call on it is `Io`. In the log, the relay of the replacement blockd's (pid 14) output had reached its partition table at 23.909 and not its "session 0 opened" line, which blockd prints after `admit`, when the client failed at 23.909: consistent with the race, not proof of it. No acknowledged write is lost to this: the session's end requeues every acknowledged, unflushed write for the next reconnect. fsd reconnects on `Ended` and would meet the same race. The fix: `Service::open` makes the server's rings and the `Opening` carries them, so no answer is sent before the cursors are zero, and `admit` takes them from it. `layout::server` states the contract. The model (`toyos-blockring/src/model.rs`) used to zero all four cursors at a session's end, which no end does: the page now keeps each cursor where its end left it until the next session's two ends set theirs, and a ring is held to its queue only while an end is looking at it. New host test `the_new_server_sets_its_cursors_before_the_client_looks`: over the page a crashed model run leaves, a client looking before the new server's ends are made gets `HeadPastTail` asking and the last session's completion hearing, which `Client::complete` refuses with `Tag`; after, the page is a fresh one. With the model's session end zeroing the page again (a checked patch, restored), that test is FAILED (EXIT=101). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
One conflict, modify/delete: this branch deleted issues/filesystem/blockd-survives-its-death-reds-on-a-replacement-that-reorders-acknowledged-writes.md and main (45db225) took one sentence out of it, the `--known-red` answer. Main's file is kept whole, so that hunk stands: #660 cut the test the issue's exit names, and the issue no longer closes here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…ue stays open The first round's fix moved `layout::server` from `admit` into `Service::open`, where nothing but reading holds it: with that move reverted at 0878aa1 and the round's tests kept, `cargo test -p toyos-blockring` and blockd's host test both exit 0. The only arm that could red was a guest run of `blockd_survives_its_death`, which #660 cut. The order is now a type. `wire::Opened`'s fields are private and `Opened::over(page, blocks, unique)` returns a server's ends and its answer together; `layout::server` is the crate's own. blockd can encode no `MSG_OPENED` before its two cursors are 0, and its `Opening` carries both. A compile-fail case on `Opened` holds the refusal (E0451), beside a block that compiles. The issue is not closed. Its exit asks for the test green across a run of repeats, and the test and its trace judge left the suite in 520c0d1, so nothing that runs can meet it. Its body now says what the first round found (every cited red predates #534's judge, whose comparison passes the cited trace: run on the two orders, it passes where the comparison before it fails) and where the test went. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Controls and mutations at
Arm B's production files: The exits file:
|
|
One more mutation at --- a/toyos-blockring/src/wire.rs
+++ b/toyos-blockring/src/wire.rs
@@ -45,7 +45,7 @@ pub const GUID_BYTES: usize = 16;
///
/// and never without them:
///
-/// ```compile_fail,E0451
+/// ```compile_fail,E0308
/// let _answer = toyos_blockring::wire::Opened { blocks: 1, unique: [0; 16] };
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
|
Review of BLOCKER
NOTE
REMOVE
SEND BACK |
…test reads a used page Answers the review of 7a8c3b2 on #643. The issue `blockd-survives-its-death-reds-on-a-replacement-that-reorders- acknowledged-writes` is deleted, and not on this branch's type. Its subject was closed before it was filed: 17bb264 (#534) is the fix for its signature. That commit's message names run 36273557690 and the pair [282600, 281592] / [281592, 282600], and its judge holds the replacement to the same writes as a multiset, in order only among writes that overlap. `git merge-base --is-ancestor 17bb264 <head>` exits 1 for 8c5be84, a55d62c and 59bd29f, the three heads the issue cites and was filed at, and 0 for origin/main: every red it records is the verdict of the judge before #534, which is the exit's second arm, "the verdict is shown wrong about it". `Opened::over` orders the handshake and bears on neither arm. The exit's last clause named a test 520c0d1 (#660) deleted, and Stage D of `the-guest-suite-runs-only-what-no-cheaper-tier-reaches` already owes its replacement. The slug was cited nowhere else in the tree. Its durable rule is at its site already: requests in flight at once are unordered (`toyos-blockring/src/lib.rs`, and `Client::next_request`, which keeps an overlapping pair apart). What reorders the two writes, QEMU's bottom halves and NVM Express 1.4 section 6.3, is in 64f5854's message. What the branch does leave owed is filed: `nothing-reopens-a-blockd-session- over-a-used-page`. The nightly's `blockd_io: FAIL /AFTER.BIN after the restart: Io` (run 36753172688, guest 7) was fixed by reading and is reproduced by nothing, and no tier reopens a session over a used page. The model is origin/main's again. The last round split its session end into `ended` and `reopen` and gave `hold_empty` two parameters so that the page kept a dead session's cursors. The search does not visit the difference: a state is keyed by its queues, and nothing looks at the page between `notice` and `reconnect`. With and without the split, `cargo test -p toyos-blockring --lib -- --nocapture --test-threads 1` prints the same eighteen verdict lines (end states and flushes given up, per bound). The one test that wanted a used page walks to `crash`, where main's model already holds one, and ends the clone's session itself. That test is named for what it can fail on: `a_used_page_reads_fresh_once_a_servers_ends_are_made_over_it`. Its old name stated blockd's order, which it stays green without. The compile-fail case on `Opened` names no error code. rustdoc reads one only on a nightly build and the host suites run a stable one, so `E0451` was read by nothing: `E0308` in its place left the doc-tests green. The block beside it that compiles is what holds the case, as in `toyos-net-wire`. Filed as `a-compile-fail-case-names-an-error-code-rustdoc-never-reads`, with the same mutation run on `toyos_bootmap::DirectMapEnd` (E0603) and `toyos_transport::Place` (E0080): both doc-test runs exit 0 under E0308. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Controls and mutations at ExitsArm B: the whole fix reverted onto
|
|
A blockd session opening at The image ( Boot 0, Boot 1, the same config with no unit: blockd's claim is refused, and it prints no session line. Boot 2, |
Found while checking who else names toyos-blockring. 520c0d1 deleted `tests/toyos-rust-tests/src/bin/blockd_io.rs` and left `blockd`, `toyos-blockring` and `toyos-fat32` in that crate's manifest: `git grep` for the three under `tests/toyos-rust-tests` finds `Cargo.toml` and `Cargo.lock` alone. Filed, not fixed: it is off this branch's task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Round 2: review of Earlier BLOCKERs
Gates at the head, read from their logs: Weighed, nothing owed in the diff:
BLOCKERNone. NOTE
REMOVE
LAND AFTER NAMED CHANGES |
The review of 0d2642d on #643 found the file with evidence and an exit and no owner; the branch's other two issue files name theirs, and a compromise is recorded with all three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Main gained #643, blockd setting its ring cursors before it answers an open, with its issues. It shares no file with this branch and the merge has no conflict. Main's `rust` gitlink is unchanged at 3f6050fc829, so the branch's pin 6c7f996a4fe stands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Cargo.lock alone conflicted: both sides kept, main's pcap-file and byteorder_slice beside this branch's ureq, flate2 and tar trees, and cargo left the result as it resolves the merged manifests. Against origin/main the lockfile differs by what it did before the merge, +801 -6. #659 moves the rust gitlink and the kernel, so the freestanding and sysroot keys move with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…among them) into wt/toyos-winitstall Three content conflicts, each a deletion on main's side of a block this branch had edited: - kernel/src/inbox/mod.rs: main deleted `Staged`, `handler-post`'s ring, with the actuator (#660). This branch's hunks inside it — the `owed` field, `Poll::new`, the `WatchFlags` direction and "fires" for "completes" in its doc — adapted it to the ring's new fields and go with it. `Inbox::complete` keeps this branch's wording and loses main's `raise_if_staged` call. - kernel/src/watch.rs: main deleted the `handler_post` module, `holding`, `note_post` and the `raise_if_staged` call in `IrqLock::with`. This branch's one hunk inside it was "fires" for "completes" in the module's doc, which goes with it. - userland/fsd/src/main.rs: main deleted the four test actuators (`--end-on`, `--end-at-read`, `--end-at-mount`, `--let-go-at-read`) and kept the acceptor probe; this branch deleted the probe and kept the actuators. Both deletions stand: no `caps_len`, no `probe` field, no actuator field, and `accept` is this branch's. Two resolutions no marker asked for: - src/ci.rs: #668 made a control's verdicts `Fails(..)` values, so `post-is-an-answer`'s three verdict strings become three `Fails`. - issues/build: main filed the C++ runtime's scratch removal as an issue of its own (`the-cxx-runtimes-scratch-removal-dies-on-a-finder-file.md`) beside the sweep's, which this branch had merged into one file. Main's two files stand and this branch's file goes. The `rust` gitlink is main's, `95960d6c2`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Head
a5654353f.origin/main46af79d5dis merged in atc0c12408a, without conflict. Net against it: 9 files, +166 −43. Production +55 −14 (wire.rs,layout.rs, blockd'smain.rsandsession.rs; 25 of the added lines are doc comments, the two doc-tests among them). Tests +23 −1 (model.rs). Issues +88 −28.The defect, still on
mainblockd answers
MSG_OPENEDand only then makes its ends of the session page:admitcallslayout::serverafterhandshakehas sent the answer (userland/blockd/src/main.rs:331at46af79d5d). A fresh region is zero, so a first open cannot show it. A reconnect sends the page the last server wrote, whose request-ring head and completion-ring tail are still that server's, andSession::reconnectpumps the moment it hears. A client that gets there beforeadmit:Violation::HeadPastTail;Violation::Tag, the session ends, and the caller's call isIo.No acknowledged write is lost to it: the end requeues every acknowledged, unflushed write. fsd reconnects on
Endedand meets the same race.What changed
The order is a type.
wire::Opened's fields are private.Opened::over(page, blocks, unique)returns a server's ends of the page and its answer together, andlayout::serverispub(crate). blockd'sOpeningcarries both, so it has noMSG_OPENEDto encode before its two cursors are 0.Opened::decodeis the client's way to one.Two tests.
Opened: a literal outside the crate does not compile, beside a block that makes one withOpened::over. It names no error code: the block beside it is what holds it to its reason, as intoyos-net-wire.a_used_page_reads_fresh_once_a_servers_ends_are_made_over_it(toyos-blockring/src/model.rs): over the page a crashed model run leaves, a client's ends alone getHeadPastTailasking and the last session's completion hearing, whichClient::completerefuses withTag. Once a server's ends are made the page reads fresh. It holds nothing of blockd's order.The reorder issue closes, on #534.
issues/filesystem/blockd-survives-its-death-reds-on-a-replacement-that-reorders-acknowledged-writes.mdis deleted, and not on the type:Opened::overorders the handshake and bears on neither arm of its exit.17bb26416(#534) is the fix for its signature. That commit's message names run 36273557690 and the pair [282600, 281592] / [281592, 282600], and its judge holds a replacement to the same writes, in order only among writes that overlap.git merge-base --is-ancestor 17bb26416 <head>exits 1 for8c5be843,a55d62c6and59bd29ff2, the heads the issue cites and was filed at, and 0 fororigin/main: every red it records is the verdict of the judge before #534.git grepof the slug at this head exits 1: nothing cites it. Its durable rule is at its site already (toyos-blockring/src/lib.rs, "Requests in flight at once are unordered", andClient::next_request).Three issues filed.
issues/filesystem/nothing-reopens-a-blockd-session-over-a-used-page.md: the nightly'sblockd_io: FAIL /AFTER.BIN after the restart: Io(run 36753172688, guest 7) was fixed by reading and is reproduced by nothing. Its exit is the restart test Stage D owes, red with thatIounder blockd's order reverted.issues/build/a-compile-fail-case-names-an-error-code-rustdoc-never-reads.md: rustdoc reads the code only on a nightly build, and the host suites run a stable one.E0603intoyos-bootmapandE0080intoyos-transportchanged toE0308leave both crates' doc-tests at EXIT=0.issues/build/the-guest-test-crate-depends-on-three-crates-no-test-uses.md: off this task, found while searching for the crate's users.520c0d129deletedblockd_io.rsand left its three dependencies intests/toyos-rust-tests/Cargo.toml.Checks: a device server on a filesystem's path to its disk
Negative control, arm B. At
0d2642da2, the whole fix reverted onto46af79d5d's production files with the tests kept:userland/blockd/src/main.rs,session.rsandtoyos-blockring/src/layout.rsequalorigin/main's (git diff --quiet, EXIT=0), andwire.rsisorigin/main's plus the fifteen doc lines of the two doc-tests.cargo test -p toyos-blockring --doc: EXIT=101.wire::Opened (line 39) - compile fail ... FAILED, "Test compiled successfully, but it's markedcompile_fail." The block beside it fails too, becauseoveris gone; that proves nothing.cargo test -p toyos-blockring --lib: EXIT=0, 20 pass. The model test does not hold blockd's order; the type does.main's blockd, and it builds.Arm C. blockd's
main.rsas onorigin/main, against the crate at the head: EXIT=101, it does not build. E0603function server is privateatmain.rs:331, E0616field blocks ... is privateatmain.rs:723.Green arm.
cargo test -p toyos-blockring: EXIT=0, 20 unit tests and both doc-tests.Oracle. For the order: rustc's privacy check, a checker the author did not write. For what a used page holds and what the transport answers over it: the ring's interleaving model, whose walk drives the crate's own
ClientandServerSessionand the transport's own rings to the page the test reads. The model does not cover the order (arm B,--libEXIT=0). No recorded failure is reproduced: nothing reproduces the nightly'sIo, and that is filed.Mutations, each a checked patch, run and restored:
Consumer::newstores no headcargo test -p toyos-blockring --libmodel.rs:791,Err(HeadPastTail)forOk(4)cargo test -p toyos-transportProducer::newstores no tailcargo test -p toyos-blockring --libmodel.rs:792, and three model runscargo test -p toyos-transportOpened's fieldspubcargo test -p toyos-blockring --docpubcargo test -p toyos-blockring --docE0603toE0308,toyos-bootmapcargo test -p toyos-bootmap --docE0080toE0308,toyos-transportcargo test -p toyos-transport --docEvery patch, the script that applied, ran and reversed each, the logs and the exits: #643 (comment)
What the tests see that reading cannot. The compile-fail case: a
pubon both fields builds everywhere and reds only there. Onepubfield leaves the literal refused and the case green, and the order held. The model test: what the transport's cursor arithmetic answers over a page a crashed session left, which is a run's result; no test intoyos-transportholds that either constructor stores its cursor.A session opening at
0d2642da2.cargo test --test toyos-build -- iommu_virtio_platform --nocapture: EXIT=0, three boots.fsd read the partition through session 0 and formatted it. The third boot (
tests/testcases) prints the same lines for its own partition. The second,tests/netcasewith no unit, is the run's own control: blockd's claim is refused, it printsblockd: NOT SERVINGand no session line, and fsd saysDATA is absent this boot. The run's lines per boot: #643 (comment)This is a first open over a fresh region.
main's blockd opens one too: the run shows the changed path runs and carries requests, and cannot tell the order.Gates
a5654353fis0d2642da2plus the owner line of one issue file (git diff --stat 0d2642da2 a5654353f: 1 file, +2). Nothing cites that file:git grepof its slug ata5654353fexits 1. Ata5654353f:cargo run -- --ci hostAt
0d2642da2:cargo run -- --ci hostcargo run -- --build-onlycargo test --test toyos-buildcargo test --test toyos-build -- iommu_virtio_platform --nocapturecargo test -p toyos-blockringcargo test --manifest-path userland/blockd/Cargo.toml --target aarch64-apple-darwin0d2642da2is9a2ea85e9plus one issue file. Every gate of this table and every control and mutation above ran at both heads with the same exits; at9a2ea85e9the build compiledblockd,fsdandinitfor ToyOS from a clean userland target.git status --porcelain --ignore-submodules=noneis empty after them. No fork or checkout names the crate:grep -rlfortoyos_blockringandtoyos-blockringover~/.cargo/git/checkouts,rust/libraryandrust/src/bootstrapexits 1. In the tree its users are blockd, fsd and init, and all three build.No guest test is added or changed, and no T14 run is requested: the type is the cheapest tier that reaches the order, and a guest's capture reaches blockd's session line.
Unsure
Iowas this race is not proven, and nothing reopens a session over a used page. Both areissues/filesystem/nothing-reopens-a-blockd-session-over-a-used-page.md. The host test reachesHeadPastTailandTag; that the client's glue turns them into an ended session andIois read offSession::pump,drainandwait, which no host runs.Openedor its path moves. A literal that stopped compiling for another reason, a renamed field, would pass it.🤖 Generated with Claude Code
https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm