Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <crate> --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.
Original file line number Diff line number Diff line change
@@ -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.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion toyos-blockring/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ pub fn client<W: Word>(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<W: Word>(page: &[W; RING_WORDS]) -> ServerRings {
/// A server's way to them is [`crate::wire::Opened::over`].
pub(crate) fn server<W: Word>(page: &[W; RING_WORDS]) -> ServerRings {
(Consumer::new(page, REQUESTS), Producer::new(page, COMPLETIONS))
}

Expand Down
24 changes: 23 additions & 1 deletion toyos-blockring/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down
44 changes: 42 additions & 2 deletions toyos-blockring/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<W: Word>(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());
Expand Down
20 changes: 10 additions & 10 deletions userland/blockd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion userland/blockd/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading