diff --git a/issues/build/a-compile-fail-case-names-an-error-code-rustdoc-never-reads.md b/issues/build/a-compile-fail-case-names-an-error-code-rustdoc-never-reads.md new file mode 100644 index 00000000000..9d35eb5caf5 --- /dev/null +++ b/issues/build/a-compile-fail-case-names-an-error-code-rustdoc-never-reads.md @@ -0,0 +1,35 @@ +--- +status: open +kind: tooling +opened: 2026-10-02 +--- + +# A compile-fail case names an error code rustdoc never reads + +rustdoc reads the error code of a `compile_fail` block only on a nightly build +(`ErrorCodes::from(…is_nightly_build())`, `rust/src/librustdoc/doctest.rs`), +and the host suites run the host's stable one, 1.98.1 where this was measured: +the `toyos` toolchain builds no rustdoc. There the code is a word of the fence, +and the block passes on any compile error, a renamed item or a typo included. + +Each code changed to `E0308`, a checked patch run and restored, leaves its +crate's doc-tests green: + +| case | names | `cargo test -p --doc` under `E0308` | +|---|---|---| +| `toyos_bootmap::DirectMapEnd` | `E0603` | EXIT=0 | +| `toyos_transport::Place`, three blocks | `E0080` | EXIT=0 | + +What does hold a case to its reason in this tree is the block beside it that +compiles (`toyos-net-wire/src/lib.rs`, `mod compile_fail`). `Place`'s three +have one; `DirectMapEnd`'s has none. The "compile-fail case" that Stages C and +H of `issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` +exit on is held the same way and no other. + +**Exit**: `git grep 'compile_fail,E'` finds nothing outside `rust/` and every +compile-fail case sits beside a block that compiles; or the host suites' +rustdoc reads the code, and a case under a wrong one reds its crate's +doc-tests. + +Owner: the orchestrator, which dispatches the stages whose exits name a +compile-fail case. diff --git a/issues/build/the-guest-test-crate-depends-on-three-crates-no-test-uses.md b/issues/build/the-guest-test-crate-depends-on-three-crates-no-test-uses.md new file mode 100644 index 00000000000..dacd305d119 --- /dev/null +++ b/issues/build/the-guest-test-crate-depends-on-three-crates-no-test-uses.md @@ -0,0 +1,17 @@ +--- +status: open +kind: tooling +opened: 2026-10-02 +--- + +# The guest test crate depends on three crates no test uses + +`tests/toyos-rust-tests/Cargo.toml` names `blockd`, `toyos-blockring` and +`toyos-fat32`, "for `blockd_io`". `520c0d129` deleted +`tests/toyos-rust-tests/src/bin/blockd_io.rs`, and `git grep` for the three +under `tests/toyos-rust-tests` finds the manifest and its lockfile alone. + +**Exit**: the three lines and their comment are gone, the lockfile follows, and +`cargo test --test toyos-build` is green. + +Owner: the orchestrator. diff --git a/issues/filesystem/blockd-survives-its-death-reds-on-a-replacement-that-reorders-acknowledged-writes.md b/issues/filesystem/blockd-survives-its-death-reds-on-a-replacement-that-reorders-acknowledged-writes.md deleted file mode 100644 index 05e5f8a0bf9..00000000000 --- a/issues/filesystem/blockd-survives-its-death-reds-on-a-replacement-that-reorders-acknowledged-writes.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# `blockd_survives_its_death` reds on a replacement that rewrites acknowledged writes out of order - -The test kills blockd with writes in flight and judges QEMU's -block trace. It has gone red on one signature, the same sectors each time: - -``` -FAIL blockd_survives_its_death: QEMU's trace, after the reset: Ok([411648]); after the kill: Err("blockd died with the writes at sectors [282600, 281592] acknowledged and no flush after them (and 280584 withheld); the blockd after it wrote [281592, 282600] first") -``` - -- PR #524's branch nightly at `8c5be843` (run 36273557690), wide and alone, - with x86-64 linked by toyos-ld; -- PR #532's branch nightly at `a55d62c6` (run 36287592139), wide and alone; -- PR #532's branch on the dev host, `cargo test --test toyos-build -- --nightly - blockd_survives_its_death`: 1 red in 5. - -It passed in main's nightly at `fd62f567` (run 36278449733) and in #532's -nightly at `e4317d3f` (run 36281465192), whose guest binaries are the ones -`a55d62c6` booted. So it is a rate, and it predates the rust-lld switch. Whether the defect is blockd's replay -order or the test's reading of the trace is not measured. - -Exit: the ordering the verdict names cannot happen, or the verdict is shown -wrong about it, with the test green across a run of repeats. diff --git a/issues/filesystem/nothing-reopens-a-blockd-session-over-a-used-page.md b/issues/filesystem/nothing-reopens-a-blockd-session-over-a-used-page.md new file mode 100644 index 00000000000..26322aa9057 --- /dev/null +++ b/issues/filesystem/nothing-reopens-a-blockd-session-over-a-used-page.md @@ -0,0 +1,36 @@ +--- +status: open +kind: tooling +opened: 2026-10-02 +--- + +# Nothing reopens a blockd session over a used page + +A nightly went red on `blockd_io: FAIL /AFTER.BIN after the restart: Io` (run +36753172688, guest 7). blockd answered an open and only then made its ends of +the session page. A client that reconnects sends the page its last server +wrote, and one that looks before the new server's ends are made has no room to +ask (`Violation::HeadPastTail`) and hears the dead session's completion +(`Violation::Tag`). The order is a type now: `wire::Opened::over` makes a +server's ends and its answer together. + +That was fixed by reading. Nothing reproduced the `Io` before it, and nothing +that runs reopens a session over a used page: + +- blockd's host test reaches `Service::place` and `Service::listing`, never + `Service::open`; +- `toyos-blockring`'s model test holds what the transport answers over a page + a crashed session left, and is green with blockd's order reverted; +- no host runs the client's glue (`Session::pump`, `drain` and `wait` in + `userland/blockd/src/session.rs`), so that `HeadPastTail` and `Tag` end the + session and the caller's call is `Io` is read off it, not run; +- `520c0d129` cut `blockd_survives_its_death`, the guest test that killed + blockd under its client, and no guest test or metal row ends blockd. + +**Exit**: the restart test that Stage D of +`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` owes +for `blockd_survives_its_death`, at the tier that stage builds it on: red with +that `Io` under a mutation that moves blockd's two stores after its answer, +and green without it. + +Owner: the orchestrator, which dispatches Stage D. diff --git a/toyos-blockring/src/layout.rs b/toyos-blockring/src/layout.rs index 05f60c90d85..56f65d70163 100644 --- a/toyos-blockring/src/layout.rs +++ b/toyos-blockring/src/layout.rs @@ -62,7 +62,8 @@ pub fn client(page: &[W; RING_WORDS]) -> ClientRings { /// The server's ends of a session page it was sent, every word it owns set to /// 0. Whatever the client left in its own is bounded when first looked at. -pub fn server(page: &[W; RING_WORDS]) -> ServerRings { +/// A server's way to them is [`crate::wire::Opened::over`]. +pub(crate) fn server(page: &[W; RING_WORDS]) -> ServerRings { (Consumer::new(page, REQUESTS), Producer::new(page, COMPLETIONS)) } diff --git a/toyos-blockring/src/model.rs b/toyos-blockring/src/model.rs index 0edc7115fba..9a103bc0e73 100644 --- a/toyos-blockring/src/model.rs +++ b/toyos-blockring/src/model.rs @@ -35,7 +35,7 @@ use core::sync::atomic::Ordering; use std::collections::HashSet; use toyos_blockhold::Holds; -use toyos_transport::{Consumer, Place, Producer, Untrusted, Word}; +use toyos_transport::{Consumer, Place, Producer, Untrusted, Violation, Word}; use crate::client::{Client, Outcome, Ticket, MAX_ATTEMPTS}; use crate::entry::{Completion, Op, Request}; @@ -770,6 +770,28 @@ mod tests { world } + /// A used page reads fresh once a server's ends are made over it. A + /// client's ends alone leave the dead server's two cursors on it: no room + /// to ask, and the last session's completion to hear, which the client + /// refuses as the server breaking the protocol. + #[test] + fn a_used_page_reads_fresh_once_a_servers_ends_are_made_over_it() { + let world = walk(start(at_most(0, 1, 0)), &["ask", "take", "done", "read", "crash"]); + let page = &world.queues.page; + let mut client = world.client.clone(); + client.session_ended(); + client.session_started(); + let (mut asks, mut hears): ClientEnds = (Producer::new(page, SQ), Consumer::new(page, CQ)); + assert_eq!(asks.space(page), Err(Violation::HeadPastTail)); + let stale = hears.pop(page).expect("a tail inside the ring").expect("the last session's completion"); + let stale = Completion::decode(stale).expect("a completion the last server wrote"); + assert_eq!(client.complete(stale), Err(Violation::Tag)); + + let ((mut asks, mut hears), _server) = Queues::ends(page); + assert_eq!(asks.space(page), Ok(DEPTH)); + assert_eq!(hears.pop(page), Ok(None)); + } + /// Two states a key that orders tags by value merges, though a reset parts /// them: F2 on the device and slot 1 free, reached with W1 asked after W0 /// was answered, and with the two in flight together. Named alike, they act diff --git a/toyos-blockring/src/wire.rs b/toyos-blockring/src/wire.rs index 489444b9c06..71236c917bf 100644 --- a/toyos-blockring/src/wire.rs +++ b/toyos-blockring/src/wire.rs @@ -5,6 +5,16 @@ //! with it, and is answered [`MSG_OPENED`] or [`MSG_REFUSED`] with a //! [`Refusal`]. After `MSG_OPENED` the connection carries nothing but doorbell //! bytes, each way. +//! +//! **The answer comes with the server's ends of the page.** A client looks at +//! the page the moment it hears, and one that opens the same region again +//! sends the cursors its last server left on it. So [`Opened::over`] makes a +//! server's ends and its answer together: nothing else makes the ends, and an +//! answer is otherwise only read off the wire ([`Opened::decode`]). + +use toyos_transport::Word; + +use crate::layout::{self, ServerRings, RING_WORDS}; /// Open the partition whose unique GUID is the payload, over the region sent /// with it. Handles: the region. @@ -24,15 +34,45 @@ pub const GUID_BYTES: usize = 16; /// What an open was answered with: the partition's length in blocks, and its /// unique GUID as the table stores it. +/// +/// A server has one from [`Opened::over`], with its ends of the page: +/// +/// ``` +/// use core::sync::atomic::AtomicU32; +/// use toyos_blockring::{layout::RING_WORDS, wire::Opened}; +/// let page: [AtomicU32; RING_WORDS] = core::array::from_fn(|_| AtomicU32::new(0)); +/// let (_ends, _answer) = Opened::over(&page, 1, [0; 16]); +/// ``` +/// +/// and never without them: +/// +/// ```compile_fail +/// let _answer = toyos_blockring::wire::Opened { blocks: 1, unique: [0; 16] }; +/// ``` #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Opened { - pub blocks: u64, - pub unique: [u8; GUID_BYTES], + blocks: u64, + unique: [u8; GUID_BYTES], } impl Opened { pub const BYTES: usize = 8 + GUID_BYTES; + /// A server's ends of the session `page` it was sent, every word it owns + /// set to 0, and the answer to send after: `blocks` of the partition + /// `unique`. + pub fn over(page: &[W; RING_WORDS], blocks: u64, unique: [u8; GUID_BYTES]) -> (ServerRings, Self) { + (layout::server(page), Self { blocks, unique }) + } + + pub fn blocks(&self) -> u64 { + self.blocks + } + + pub fn unique(&self) -> [u8; GUID_BYTES] { + self.unique + } + pub fn encode(&self) -> [u8; Self::BYTES] { let mut out = [0u8; Self::BYTES]; out[..8].copy_from_slice(&self.blocks.to_le_bytes()); diff --git a/userland/blockd/src/main.rs b/userland/blockd/src/main.rs index 09a3aa282ef..b98b9a6cbe3 100644 --- a/userland/blockd/src/main.rs +++ b/userland/blockd/src/main.rs @@ -55,7 +55,7 @@ use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; use toyos_abi::syscall::{DEV_PREFIX, SyscallError}; use toyos_blockhold::Holds; use toyos_blockring::entry::{Completion, Op}; -use toyos_blockring::layout::{self, ServerRings, DEPTH}; +use toyos_blockring::layout::{ServerRings, DEPTH}; use toyos_blockring::server::{ServerSession, Taken}; use toyos_blockring::wire::{self, Opened, Refusal}; use toyos_blockring::{BLOCK_BYTES, PORT, SESSION_BYTES}; @@ -247,10 +247,10 @@ struct Service { /// A session decided on and not yet told to its client. struct Opening { region: Region, + rings: ServerRings, + opened: Opened, device_addr: u64, first: u64, - blocks: u64, - unique: [u8; 16], } impl Service { @@ -303,7 +303,8 @@ impl Service { let (first, blocks) = self.place(guid)?; match self.ctrl.up().claim().dma_map(region.handle()) { Ok(mapping) if mapping.bytes == SESSION_BYTES as u64 => { - Ok(Opening { region, device_addr: mapping.device_addr, first, blocks, unique: guid }) + let (rings, opened) = Opened::over(region.words(), blocks, guid); + Ok(Opening { region, rings, opened, device_addr: mapping.device_addr, first }) } // A region longer than a session would spend the claim's bound on // the kernel's side for every other client: refused whole. @@ -328,16 +329,15 @@ impl Service { fn admit(&mut self, opening: Opening, conn: Connection) -> u64 { let id = self.next_id; self.next_id += 1; - let rings = layout::server(opening.region.words()); self.sessions.insert( id, Served { conn, region: opening.region, device_addr: opening.device_addr, - rings, - state: ServerSession::new(opening.first, opening.blocks), - unique: opening.unique, + rings: opening.rings, + state: ServerSession::new(opening.first, opening.opened.blocks()), + unique: opening.opened.unique(), closing: false, requests: 0, posted: false, @@ -714,13 +714,13 @@ fn handshake(service: &mut Service, p: Pending, msg_type: u32, payload_len: usiz refuse(&p.conn, why); } Ok(opening) => { - let opened = Opened { blocks: opening.blocks, unique: guid }; + let opened = opening.opened; if p.conn.try_send_bytes(wire::MSG_OPENED, &opened.encode()).is_err() { service.abandon(opening); return; } let id = service.admit(opening, p.conn); - println!("blockd: session {id} opened {} ({} blocks)", guid_text(guid), opened.blocks); + println!("blockd: session {id} opened {} ({} blocks)", guid_text(guid), opened.blocks()); } } } diff --git a/userland/blockd/src/session.rs b/userland/blockd/src/session.rs index e00613a55c5..8de1de510bd 100644 --- a/userland/blockd/src/session.rs +++ b/userland/blockd/src/session.rs @@ -159,7 +159,7 @@ impl Session { /// The partition's length in blocks. pub fn blocks(&self) -> u64 { - self.opened.blocks + self.opened.blocks() } /// The most requests this session has had on the wire at once.