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
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ members = [
"toyos-blockring/transport",
"toyos-bootmap",
"toyos-cpuvuln",
"toyos-device-memory",
"toyos-dhcp",
"toyos-dns",
"toyos-elf",
Expand Down Expand Up @@ -66,6 +67,7 @@ members = [
"toyos-untrusted",
"toyos-update",
"toyos-userbound",
"toyos-virtio",
"toyos-wallclock",
"toyos-xhci",
"toyos-xhci/sim",
Expand Down

This file was deleted.

9 changes: 9 additions & 0 deletions issues/every-driver-is-still-in-the-kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ What is left of the staged work:
`virtio-sound` classes, their arms of `SYS_DEVICE_REG_READ`/`WRITE`, and
`SYS_GPU_*`, which is an ABI change. GOP stays: it is memory the loader
hands over, and the panic console paints it.
A virtio holder stands on `toyos-virtio`, whose first client is netstack's
NIC, and the second one owes that crate two things. The walk of the
capability list moves into it from `userland/netstack/src/virtio_net.rs`,
over a configuration read the caller passes in, which closes
`issues/the-virtio-capability-walk-reads-a-refused-configuration-read-as-zeros.md`.
And before a client ends its device on `UsedRefusal::Written` for a chain
the device only reads, whose bound is 0, what QEMU's device reports as
`len` on such a queue is measured: the NIC's transmit queue is the only
one read so far.
2. Done: **BAR sizing and re-assignment onto 2 MiB boundaries** is
`pcidev::place_bar`, with the overlap refusal kept as the assertion that it
worked rather than as the mechanism.
Expand Down
35 changes: 35 additions & 0 deletions issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# netstack ends for the boot on a NIC it cannot drive on

`system.toml` gives `restart = true` to diskserver and fileserver and not to
netstack. netstack ends by name, "this NIC cannot be driven on", wherever a
card stops being one it can drive: a claim that refuses its interrupt read, an
Intel part that kept stranded descriptors past the driver's deadline
(`Card::begin_pass`, `userland/netstack/src/card.rs`), and a virtio device
whose used ring is not believed (`finished`,
`userland/netstack/src/virtio_net.rs`), which is any of a head past the table,
a head with no chain in flight, a length past a chain's writable bytes and a
used index past the available one. After any of them the machine has no
network until it boots again, and every program holding the `netstack` port
holds a port nobody serves.

Ending is the right answer to the device: none of the four is something a
conforming device writes, and a ring read on past one loses frames silently.
What is missing is what comes after the end.

A restart is not one line in `system.toml`. A second netstack on the same
address meets `issues/a-replacement-netstack-reuses-its-predecessors-ports.md`,
every claim it mints spends device addresses
(`issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md`), and
section 3 of `issues/the-lan-is-not-yet-production-grade.md` splits the driver
from the stack so that a driver's end does not take the stack's connections.

Owned by whoever takes that section. Exit: a guest test ends netstack's driver
and reads the network served again without a boot, or the owner rules that a
NIC whose device broke its ring stays down for the boot and this file becomes
that ruling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
status: open
kind: defect
opened: 2026-10-09
---

# The virtio capability walk reads a refused configuration read as zeros

`vendor_caps` (`userland/netstack/src/virtio_net.rs`) walks a function's
capability list through its claim, and two things in it believe more than they
were told:

- A field of a vendor capability is read as
`dev.config_read(next + at, width).unwrap_or(0)`, so a read the kernel
refused becomes a `cfg_type`, `bar`, `offset` or `length` of 0. A capability
made of zeros is then refused or ignored by `toyos_virtio::pci::Layout::of`
under some other name (`MissingCap`, `TooShort`), and the kernel's word for
what happened is gone.
- The capabilities pointer and every `next` link are used as read. The *PCI
Local Bus Specification*, revision 3.0, section 6.7, reserves the bottom two
bits of each and has software mask them before using the value as an offset
(cited from memory: the document was not at hand when this was filed).
An unmasked pointer with either bit set is an offset no capability is at,
and the 32-bit reads at `next + 8` and on are then misaligned, which the
claim refuses and the first item turns into zeros.

Neither reaches memory: the claim bounds every read to the function's own
configuration space (`config_space_is_bounded`). What they cost is a device
refused for the wrong reason.

Owned by the second client of `toyos-virtio`, which moves the walk into the
crate (stage 1 of `issues/every-driver-is-still-in-the-kernel.md`). Exit: the
walk is the crate's, over a configuration read its caller passes in; a refused
read is a refusal by name and a link is masked before it is an offset; and a
host test is red against each of the two as they stand today.
18 changes: 18 additions & 0 deletions toyos-device-memory/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# A member of the host workspace (root `Cargo.toml`), like toyos-i219 and
# toyos-virtio, the two driver crates written against it: it is pure and has
# no dependencies.
#
# What lives here is two traits and their contract: the memory a userland
# driver and its device both reach, which is the whole of what a driver's
# decisions are separated from the instructions that carry them out by. A
# driver crate is generic over them and host-tested against a model of its
# device; the program that holds the claim implements them once, for every
# driver it runs.

[package]
name = "toyos-device-memory"
description = "The memory a userland driver and its device share, as the boundary a driver crate is written against: a register window, a DMA grant and the two barriers that order a store against the device, pure."
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
96 changes: 96 additions & 0 deletions toyos-device-memory/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! The memory a userland driver and its device both reach, as the boundary a
//! driver crate is written against.
//!
//! Two traits, named by what they do and not by what device is behind them:
//! [`Registers`] is one access to a mapped BAR, and [`DmaBuffers`] is the
//! grant the device reaches through its IOMMU domain, with the two barriers
//! that order the driver's accesses to it against the device's. A driver crate
//! takes each as a generic parameter, resolved at compile time, and makes
//! every decision above them, where its host tests drive it against a model of
//! its device.
//!
//! **An implementation decides nothing**: each method is one volatile load or
//! store of the width its name says, or one barrier. The program that holds
//! the claim implements both once, for every driver it runs, so what an
//! architecture needs for a store to reach a device is one body.
//!
//! **Widths are in the names**, because the width of an access to a device is
//! a decision — a register file says which each field takes — and one a
//! driver states at the site rather than has inferred. Each is there because a
//! driver in the tree makes that access.
//!
//! Every word is little-endian, as a PCI device's registers and rings are and
//! as every machine ToyOS runs on is.
//!
//! What only one driver needs of its substrate — a clock, the claim's
//! interrupt record — is that driver crate's own trait until a second driver
//! needs the same.

#![no_std]
#![forbid(unsafe_code)]

/// One access to the BAR that holds a device's registers.
///
/// **The driver bounds an offset before it reaches an implementation**:
/// against [`Self::bytes`], and aligned for the access's width. Volatile,
/// because the device writes the same bytes: a plain load of a status
/// register can be hoisted out of the loop that waits on it.
pub trait Registers {
/// Bytes the window covers.
fn bytes(&self) -> usize;
fn read8(&self, at: usize) -> u8;
fn read16(&self, at: usize) -> u16;
fn read32(&self, at: usize) -> u32;
fn write8(&self, at: usize, value: u8);
fn write16(&self, at: usize, value: u16);
fn write32(&self, at: usize, value: u32);
}

/// The memory this process and its device both reach: one DMA grant.
///
/// **Two addresses, because neither derives from the other**: an offset is
/// where the driver's own loads and stores land, and [`Self::device_addr`] is
/// what the device is told for the same byte. Never a physical address once a
/// unit translates for the function.
///
/// Every access is volatile and aligned for its width: the device reads and
/// writes the same bytes.
///
/// # The order of a driver's accesses against its device's
///
/// A device acts on the grant when a register tells it to, or whenever it
/// likes, and on a weakly ordered machine neither a store nor a load of this
/// memory is ordered against another without a barrier. The two are here
/// rather than beside a doorbell because the order they impose is over *this*
/// memory:
///
/// - **[`Self::publish`] before the device is pointed at what was stored.**
/// A driver stores what it publishes, calls `publish`, and only then makes
/// the store that tells the device of it — a register write, or the index
/// of a ring the device polls.
/// - **[`Self::observe`] after the load that said the device was done.** A
/// driver loads the word by which the device hands memory back, calls
/// `observe`, and only then loads what that word covers.
///
/// **Neither orders a store against a later load of what the device wrote in
/// answer** — an index stored, then the device's suppression word loaded to
/// decide whether to notify it. No driver here makes that pair; the first
/// that does adds a method for it and does not reach for `publish`.
pub trait DmaBuffers {
/// Bytes in the grant.
fn bytes(&self) -> usize;
/// Where the device reaches byte `at`.
fn device_addr(&self, at: usize) -> u64;
fn read16(&self, at: usize) -> u16;
fn read32(&self, at: usize) -> u32;
fn read64(&self, at: usize) -> u64;
fn write16(&self, at: usize, value: u16);
fn write32(&self, at: usize, value: u32);
fn write64(&self, at: usize, value: u64);
/// One release barrier: every store made above this call is visible to
/// the device before any store made below it.
fn publish(&self);
/// One acquire barrier: every load made below this call sees memory at
/// least as new as the load made above it.
fn observe(&self);
}
20 changes: 13 additions & 7 deletions toyos-i219/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# A member of the host workspace (root `Cargo.toml`), like toyos-hda, toyos-pci
# and toyos-dma: it is pure, it has no dependencies, and its tests run on the
# host with no guest and no QEMU.
# and toyos-dma: it is pure, and its tests run on the host with no guest and no
# QEMU.
#
# What lives here is every decision the I219 driver makes — the descriptor
# rings, the interrupt causes, the link — against four traits netstack implements
# one instruction deep. The device is on the far side of a trust boundary, so
# the numbers it writes into a descriptor have to be driven with values no
# working card would send far more often than a boot allows; and the machine
# that matters cannot be booted by a test at all, so the specification is the
# oracle and `stub.rs` is what it is written into.
# one instruction deep, two of them the boundary every userland driver shares.
# The device is on the far side of a trust boundary, so the numbers it writes
# into a descriptor have to be driven with values no working card would send
# far more often than a boot allows; and the machine that matters cannot be
# booted by a test at all, so the specification is the oracle and `stub.rs` is
# what it is written into.

[package]
name = "toyos-i219"
Expand All @@ -17,3 +18,8 @@ version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false

[dependencies]
# `Registers` and `DmaBuffers`: the boundary every userland driver is written
# against, and the order of its two barriers.
toyos-device-memory = { path = "../toyos-device-memory" }
Loading
Loading