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
22 changes: 22 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 @@ -108,6 +108,7 @@ members = [
"userland/snake",
"userland/soundserver",
"userland/soundserver/mixer",
"userland/soundserver/virtio-sound",
"userland/sprite",
"userland/sshserver",
"userland/supervisor",
Expand All @@ -117,6 +118,7 @@ members = [
"userland/test-runner",
"userland/toybox",
"userland/toyos-font",
"userland/toyos-pci-claim",
"userland/toyos-window",
"userland/trace",
"userland/update",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ opened: 2026-10-08

# A class claim's call in flight acts after the claim's release

A claim on a class this kernel still drives — the framebuffer, HDA audio,
virtio-sound — is a per-class flag (`device::TAKEN`), and a call on one checks
A claim on a class this kernel still drives — the framebuffer, HDA audio — is
a per-class flag (`device::TAKEN`), and a call on one checks
the handle and then acts with nothing held across the two:

- `SYS_DEVICE_REG_READ` and `SYS_DEVICE_REG_WRITE` resolve the claim to
`RegTarget::Hda` or `RegTarget::VirtioSound` under the process-data lock
(`sys_device_reg`, `kernel/src/syscall/device.rs`) and reach
`drivers::hda::reg_write` or `drivers::virtio_sound::reg_write` after it;
`RegTarget::Hda` under the process-data lock (`sys_device_reg`,
`kernel/src/syscall/device.rs`) and reach `drivers::hda::reg_write` after
it;
- `SYS_GPU_PRESENT`, `SYS_GPU_SET_CURSOR`, `SYS_GPU_MOVE_CURSOR` and
`SYS_GPU_SET_RESOLUTION` ask `holds_claim` and then call `crate::gpu`
(`kernel/src/syscall/dispatch.rs`).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# diskserver's NVMe bring-up open-codes what `toyos-pci-claim` holds

`Controller::open` (`userland/diskserver/src/nvme.rs`) maps BAR 0 with
`PciDev::map_bar` and an `unsafe Window::new` over it, allocates its grant with
`PciDev::dma_alloc` and another `unsafe Window::new`, and names each kernel
refusal as `Refusal::Kernel(call, e)`. That is `Bar::map`, `Grant::alloc` and
`KernelRefused` of `userland/toyos-pci-claim/src/lib.rs`, which netstack's two
drivers and soundserver's virtio-sound driver use: two more `unsafe` sites
whose argument is the one the crate already makes.

Exit: diskserver reaches its BAR and grant through `toyos-pci-claim`, or the
reason it cannot is at the crate's module header.
37 changes: 23 additions & 14 deletions issues/every-driver-is-still-in-the-kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,32 @@ is not built.

What is left of the staged work:

1. **Audio and virtio-gpu, re-scoped.** `drivers/hda.rs` and
`drivers/virtio_sound.rs` bring their device up and gate soundd's register
access; `drivers/virtio_gpu.rs` is the only `Gpu` whose `SYS_GPU_*` calls do
anything, since GOP's are all no-ops. Each leaves when its userland holder
claims the function as `pci`, as netd does, retiring the `hda-audio` and
`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
1. **HDA and virtio-gpu, re-scoped.** `drivers/hda.rs` brings its device up
and gates soundserver's register access; `drivers/virtio_gpu.rs` is the only
`Gpu` whose `SYS_GPU_*` calls do anything, since GOP's are all no-ops. Each
leaves when its userland holder claims the function as `pci`, as netstack
and soundserver's virtio-sound driver do, retiring the `hda-audio` class,
its 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. HDA leaving feeds soundserver's DLL from a claim's record on
every machine, so that diff lands
`issues/soundservers-virtio-clock-can-take-a-period-one-notification-early.md`'s
exit.
A virtio holder stands on `toyos-virtio`, which walks the capability list
too. 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.
`iommu_virtio_platform`'s decline control, `declining_is_not_free`
(`tests/common/iommu.rs`), declines through the kernel's
`virtio-no-access-platform` actuator, which reaches only a virtio function
the kernel drives; with the NIC and virtio-sound in processes that is the
virtio-gpu `Profile::HeadlessVirtioGpu` adds for it, and virtio-gpu leaving
leaves the control nothing to decline. Exit for the control, in the diff
that moves virtio-gpu out: the decline is made where the features are
negotiated, `toyos-virtio`'s `Offer::accept`, for a function a process
drives, and the control reds when that function keeps `FEATURES_OK`;
`Profile::HeadlessVirtioGpu` goes in the same diff.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# soundserver's virtio-sound clock can take a period one notification early

`Sound::played` (`userland/soundserver/virtio-sound/src/lib.rs`) reads the
transmit queue's used ring only with the claim's interrupt record in hand, and
`Virtio::completion` (`userland/soundserver/src/virtio.rs`) hands the mix loop
that record's `last_nanos`. A record says when its notifications landed and
nothing about which period each was for. So when the device has written a
newer period's used element and its notification has not landed by the ring
read, that period is counted in the mask and stamped with the older
notification's time, and `dll.update(last_nanos, n)` in
`userland/soundserver/src/mix.rs` takes an update whose newest period is
clocked at least one period early: an error shaped like `n − 1` periods on
that update. Its notification then lands into the next record, which comes
back with no period.

How often the window opens is unmeasured, and no metal machine runs
virtio-sound. Nothing bounds the error but that rarity.

Owned by stage 1 of `issues/every-driver-is-still-in-the-kernel.md`: the
diff that moves HDA onto a `pci` claim feeds soundserver's DLL from a claim's
record (`kernel/src/pcidev/record.rs`) on every machine, and lands this exit
with it. Exit: the
record ties a time to the used elements it answers for — or the driver holds
back from a DLL update every period past those its record's notifications
answer for — and `toyos-virtio-sound`'s interleaving test,
`no_period_is_stranded_between_its_used_element_and_its_notification`, asserts
that no period comes back stamped earlier than its own notification.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ user memory through an `unsafe` `from_raw_parts` of the kernel's own whose
|---|---|---|
| `DmaGrant` | `grant_bytes`, `kernel/src/syscall/device.rs` | `size_of == 4 + 4 + 8 + 8`, a sum written by hand |
| `DmaMapping` | inline in `sys_device_dma_map`, the same file | `size_of == 8 + 8`, a sum written by hand |
| `DeviceIrqRecord` | `record_bytes`, `kernel/src/object/ops.rs` | `SIZE == 4`, a total written by hand; the struct is one `u32` |
| `DeviceIrqRecord` | `record_bytes`, `kernel/src/object/ops.rs` | `SIZE == 4 + 4 + 8 + 8`, a sum written by hand |

Read, nothing run: none of the three has padding today. Each assertion
compares the struct's size with a number a person wrote, and nothing ties
Expand Down
5 changes: 3 additions & 2 deletions issues/toyos-runs-on-arm64.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,9 @@ Each stage names its exit; "measured" means a number from a run.
stage's SMMUv3 first. Stubbed on AArch64, each owned by the small-kernel
track, which moves the driver out of the kernel:
- `arch::msi_message` refuses, so the kernel's xHCI (`virt`'s boot stick),
HDA, virtio-sound, virtio-console and virtio-gpu drivers each
refuse their function by name.
HDA, virtio-console and virtio-gpu drivers each refuse their function by
name, and a process's claim on one (netstack's, soundserver's
virtio-sound) is refused with them.
- `drivers::gop` refuses a scanout that is not whole 2 MiB pages of its
own, which a `ramfb` scanout carved out of RAM need not be.

Expand Down
12 changes: 8 additions & 4 deletions issues/whether-a-region-may-be-lent-is-a-runtime-option.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ may be lent a region by reading `Region::pages` — `Some` for pages the kernel
allocated for the region, `None` for an aperture, firmware's framebuffer, or a
pool a kernel driver owns — together with its cache policy. A new region kind
that fills the field the wrong way is lendable, or refused, with no compile
error; the only check is `blockd_lends_within_its_bound`'s refusal of
virtio-sound's pool. And the field's name says nothing about lending: the
virtio-gpu scanout and cursor carry `Some` and are lendable today, which is the
same authority their holder already has but was decided by nobody.
error. The one test written for it, `blockd_lends_within_its_bound`, refused
virtio-sound's pool, which left the kernel with its driver; the test is out of
the guest suite and staged as a T14 row in
`issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`'s stage J,
where the kernel driver's pool it can refuse is HDA's. And the field's name
says nothing about lending: the virtio-gpu scanout and cursor carry `Some` and
are lendable today, which is the same authority their holder already has but
was decided by nobody.

**Exit condition.** A region's kind is a type — owned pages, a kernel driver's
pool, an aperture — and `dma_map` takes only the owned-pages kind, so a region
Expand Down
85 changes: 68 additions & 17 deletions kernel/loom/tests/device_irq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
//!
//! The kernel programs one MSI-X vector per claimed function and accumulates
//! what arrives into a record its holder reads through a syscall. So there are
//! two parties on two CPUs and one word between them: an ISR that bumps a
//! count, and a reader that takes it.
//! two parties on two CPUs: an ISR that counts a message and stamps when it
//! landed, and a reader that takes the count and its times.
//!
//! **The invariant is that every message is counted exactly once.** Nothing
//! here orders anything else — the word is the whole of what it says, so the
//! orderings are `Relaxed` and the property is an interleaving. What makes it
//! hold is that the taking side is a read-modify-write: a reader that loaded a
//! **The invariant is that every message is counted exactly once, and a
//! read's times are its own messages'.** What makes the first hold is that
//! both sides change the count by a read-modify-write: a reader that loaded a
//! count and then cleared it drops every message the ISR recorded in between.
//!
//! That is the record's whole design, so the negative control is it turned off
//! — a cargo feature rather than a comment:
//!
Expand All @@ -19,8 +17,8 @@
//! --test device_irq
//! ```
//!
//! makes the `swap` a load and a store and the ISR's `fetch_add` a load, an
//! add and a store, and [`every_message_is_counted_once`] must red.
//! makes every compare-exchange a load and a store, and
//! [`every_message_is_counted_once`] must red.

use kernel_loom::device_irq::Interrupt;
use loom::sync::Arc;
Expand All @@ -29,8 +27,8 @@ use loom::sync::Arc;
///
/// Two messages against one reader that may run at any point between them: what
/// the reader took plus what is left is exactly what arrived. A `store` where
/// the count has a `fetch_add` fails this, and so does a reader that loads and
/// then clears instead of swapping.
/// the count has a compare-exchange fails this, and so does a reader that
/// loads and then clears instead of exchanging.
#[test]
fn every_message_is_counted_once() {
loom::model(|| {
Expand All @@ -39,14 +37,14 @@ fn every_message_is_counted_once() {
let isr = {
let irq = irq.clone();
loom::thread::spawn(move || {
irq.took();
irq.took();
irq.took(1);
irq.took(2);
})
};

let taken = irq.take().unwrap_or(0);
let taken = irq.take().map_or(0, |record| record.count);
isr.join().unwrap();
let left = irq.take().unwrap_or(0);
let left = irq.take().map_or(0, |record| record.count);

assert_eq!(
taken + left,
Expand All @@ -57,6 +55,58 @@ fn every_message_is_counted_once() {
});
}

/// A read's first time is the landing of the first message it counts, and its
/// newest time no older than the newest it counts.
///
/// Three messages, landing at 1, 2 and 3, against a reader that takes once at
/// any point among them and then takes what is left: whatever the split, each
/// read's `first_nanos` is its own first message's, and a message that lands
/// while the reader is between its exchange and its loads stamps the next
/// read and never this one. A single slot for both reads' first times fails
/// this, and so does a stamp made after the exchange that counts it.
#[test]
fn a_reads_times_are_its_own_messages() {
loom::model(|| {
let irq = Arc::new(Interrupt::new());

let isr = {
let irq = irq.clone();
loom::thread::spawn(move || {
for at in 1..=3 {
irq.took(at);
}
})
};

let taken = irq.take();
isr.join().unwrap();
let left = irq.take();

let counted = taken.map_or(0, |record| record.count);
if let Some(record) = taken {
assert_eq!(record.first_nanos, 1, "the first read counted from message 1 and was told {}", record.first_nanos);
assert!(
record.last_nanos >= u64::from(counted),
"a read that counted {counted} message(s) was told the newest landed at {}",
record.last_nanos
);
}
match left {
None => assert_eq!(counted, 3, "the first read counted {counted} and nothing was left"),
Some(left) => {
assert_eq!(
left.first_nanos,
u64::from(counted) + 1,
"the second read counted from message {} and was told {}",
counted + 1,
left.first_nanos
);
assert_eq!(left.last_nanos, 3, "the newest message landed at 3 and the last read was told {}", left.last_nanos);
}
}
});
}

/// A reader that finds nothing answers nothing, and leaves nothing behind.
///
/// Single-threaded and deliberately so: this is the answer on the path a
Expand All @@ -68,9 +118,10 @@ fn an_idle_record_answers_nothing() {
let irq = Interrupt::new();
assert!(irq.take().is_none(), "an untouched record answered a reader");
assert!(!irq.armed(), "an untouched record read ready");
irq.took();
irq.took(7);
assert!(irq.armed(), "a record with a message in it read empty");
assert_eq!(irq.take(), Some(1));
let record = irq.take().expect("a record with a message in it answered nothing");
assert_eq!((record.count, record.first_nanos, record.last_nanos), (1, 7, 7));
assert!(irq.take().is_none(), "the same message was answered twice");
assert!(!irq.armed(), "a drained record still reads ready");
});
Expand Down
Loading
Loading