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
1 change: 1 addition & 0 deletions Cargo.lock

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

19 changes: 12 additions & 7 deletions issues/a-claimed-function-can-storm-its-cpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ opened: 2026-10-04

# A claimed function can storm its CPU

A function a process drives raises `pcidev::VECTORS[slot]` on cpu0 for every
message it sends (`kernel/src/pcidev/mod.rs`, `isr`), and nothing bounds how
often. Its holder programs the device through the BARs it maps, so a hostile
or broken holder can make the device send messages as fast as the bus carries
A function a process drives raises its claim slot's interrupt on cpu0 for
every message it sends — on x86-64 the slot's vector
(`kernel/src/arch/x86_64/idt/user_dev.rs`), on AArch64 the slot's LPI through
the ITS (`kernel/src/arch/aarch64/irqchip/its.rs`) — and `pcidev::isr`
(`kernel/src/pcidev/mod.rs`) takes each one; nothing bounds how often. Its
holder programs the device through the BARs it maps, so a hostile or broken
holder can make the device send messages as fast as the bus carries
them, and each one is an interrupt cpu0 takes before anything else it runs.
`issues/an-xhci-storm-starves-the-cpu-that-takes-it.md` measured what
that does to a CPU for a kernel driver's source; a claim is the same source
with its holder outside the kernel.

The lever is the slot's own remapping entry: making it not present stops the
function's messages at the unit, where masking through the device would need
the holder's cooperation. What rate is a storm is not decided, and a number
The lever is the slot's own translation: on x86-64 its remapping entry, which
made not present stops the function's messages at the unit; on AArch64 its
LPI's configuration byte, which cleared and followed by an `INV` disables
the LPI at the redistributor. Masking through the device would need the
holder's cooperation. What rate is a storm is not decided, and a number
chosen without a measurement behind it is the silent decision this file
exists to refuse.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# A lane's reader counted one refusal fewer than its writer made

`toyos/src/log/proof.rs`'s
`a_lane_gives_its_reader_every_record_in_order_or_counts_it_refused` went red
once in `cargo run -- --ci host`'s `the toyos SDK` step, on the branch
that brings up the GICv3 ITS, which touches nothing under `toyos/`:

```
panicked at toyos/src/log/proof.rs:161:5:
assertion `left == right` failed
left: 49999
right: 50000
```

The reader's `Reader::refused` total was one short of the refusals the
writer thread counted from `push_lane`. The host's load average was 44.31
on 14 cores when it was read; the same step was green an hour earlier on
a tree whose `toyos/` was the same. Either the lane loses a refusal between
the writer's count and the reader's last read after `writer.is_finished()`,
or the test reads the count before the last refusal is published: which one
is not determined.

**Exit**: the cause is named, and the test reds on it without load, or the
lane's refusal count is fixed and the test holds under the load above.
42 changes: 0 additions & 42 deletions issues/an-aarch64-crash-report-reads-through-any-user-leaf.md

This file was deleted.

22 changes: 0 additions & 22 deletions issues/msi-and-pin-routing-take-an-x86-vector-and-apic-id.md

This file was deleted.

36 changes: 21 additions & 15 deletions issues/toyos-runs-on-arm64.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ before any aarch64 file exists, with x86 as its only user:

Each is its own issue, owned by the stage that removes it:

- `issues/msi-and-pin-routing-take-an-x86-vector-and-apic-id.md` (stage 6)
- `issues/the-crash-evidence-records-x86-fault-registers.md` (stage 5)
- `issues/the-aarch64-kernel-builds-with-dead-code-allowed.md` (stage 7)

Expand Down Expand Up @@ -327,10 +326,9 @@ Each stage names its exit; "measured" means a number from a run.
function is its only consumer the small-kernel track leaves, and it needs that
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-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.
- a driver in this kernel is given no message (`irqchip::its::msi`), so
the kernel's xHCI (`virt`'s boot stick), HDA, virtio-console and
virtio-gpu drivers each refuse their function by name.
- `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 Expand Up @@ -369,16 +367,24 @@ Each stage names its exit; "measured" means a number from a run.
before its body) get a test here that reds with `put` written back as one
`self.buf.write(off, trb)`; x86's TSO hides all three from every guest
test until then.
**The claim's handler is the first arm of `irq()`
(`kernel/src/arch/aarch64/trap.rs:135`) that posts a watch or lets go of a
`Lock`**, and either runs `preempt::enable`, whose pass at depth zero
(`kernel/src/preempt.rs:67`) reads nothing of `DAIF`; `do_preempt`'s
`assert_baseline(BASELINE_IRQ_EXIT)` (`kernel/src/scheduler.rs:358`) passes
at depth zero, so that pass would run inside the handler, before
`irqchip::end`. Owed before that arm lands: `irq()` holds the preempt count
across every device arm, as x86-64's `device_irq_entry` does, or
`preempt::enable` refuses a pass with interrupts masked, which `IrqOff`'s
SAFETY (`kernel/src/sched/driver.rs:49`) already assumes.
A claimed function's message is its claim slot's LPI through the GICv3
ITS (`kernel/src/arch/aarch64/irqchip/its.rs`), on QEMU's emulated GIC:
HVF's own refuses an ITS, so a `virt` with one is
`its=on,kernel-irqchip=off`. QEMU's SMMUv3 translates the message as a
write of the function's, so every domain maps the doorbell's page
(`virt_claim_lpi`, red without it on a `DMA FAULT` at `GITS_TRANSLATER`).
`irq()` holds the preempt count across every device arm, as x86-64's
`device_irq_entry` does, and `pcidev::isr` asserts it.
A released function's stream stays on its slot's domain, which maps
the doorbell; its bus mastering off and its message masked are what
stop its writes, and a write it makes regardless reaches the ITS, where
the release's `DISCARD` and `MAPD` with `V` clear leave its DeviceID
translating nothing. That last is read, not tested: no function QEMU
offers writes once mastering is off but `iommu-testdev`, which no claim
can tell to write. **Owed**: a `smmu-selftest` arm that puts an
`iommu-testdev` on a domain, maps its DeviceID to a slot and unmaps it as
a release does, has it write event 0 to `GITS_TRANSLATER`, and is red
when the slot's LPI is raised.

7. **Userland boots.** `init`, `logd`, the compositor, netd, soundd and sshd,
built for `aarch64-unknown-toyos`. Every program of `system.toml` builds
Expand Down
1 change: 1 addition & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ toyos-elf = { path = "../toyos-elf" }
toyos-gicv3 = { path = "gicv3" }
toyos-gpt = { path = "../toyos-gpt" }
toyos-hda = { path = "../toyos-hda" }
toyos-its = { path = "../toyos-its" }
toyos-pci = { path = "pci" }
toyos-phys = { path = "../toyos-phys" }
toyos-smmu = { path = "../toyos-smmu" }
Expand Down
51 changes: 29 additions & 22 deletions kernel/src/arch/aarch64/irqchip.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
//! The interrupt controller and the timer: a GICv3 — the distributor the
//! MADT names, this CPU's redistributor, and the system-register CPU
//! interface (GIC architecture specification IHI 0069H) — and the generic
//! timer's EL1 virtual timer (Arm ARM K.a, chapter D12), whose PPI the GTDT
//! names.
//! MADT names, this CPU's redistributor, the system-register CPU interface
//! and the ITS ([`its`]) (GIC architecture specification IHI 0069H) — and the
//! generic timer's EL1 virtual timer (Arm ARM K.a, chapter D12), whose PPI the
//! GTDT names.
//!
//! **What this kernel takes is SGIs, the timer's PPI and the IOMMU's event
//! SPI ([`route_iommu_events`]), and nothing else.** No other SPI is routed
//! and no LPI exists: a device's interrupt is a message the
//! GICv3 ITS translates, and every device that would take one is either a
//! driver the small-kernel track moves out of the kernel or a claimed function,
//! which the SMMUv3 of the port's stage 6 must translate first
//! (`super::msi_message`).
//! **What this kernel takes is SGIs, the timer's PPI, the IOMMU's event SPI
//! ([`route_iommu_events`]) and one LPI per claim slot ([`its`]), and nothing
//! else.** No other SPI is routed: a device's interrupt is a message the ITS
//! translates, and only a claimed function's is.
//!
//! **The virtual timer, not the physical**: it is the one EL1 owns outright —
//! under a hypervisor the physical one traps — and with `CNTVOFF_EL2` written
//! zero by the entry from EL2 the two count alike. It is level-triggered, so a
//! handler that neither re-arms nor stops it takes it again at once.

pub mod its;

use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::Relaxed};

use alloc::vec::Vec;
Expand All @@ -31,9 +30,7 @@ use crate::mm::policy::MmioPolicy;
use crate::mm::{DirectMap, Mmio};
use crate::hw::MIN_ONE_SHOT;

/// Every INTID this kernel names, in one space: the SGIs it raises, then the
/// identities the generic drivers program for a message-signalled interrupt,
/// which `super::msi_message` refuses on this machine.
/// The SGIs this kernel raises.
#[repr(u8)]
pub(super) enum Intid {
/// Asks a CPU for a scheduler pass, or for its counters: x86-64's kick.
Expand All @@ -44,7 +41,6 @@ pub(super) enum Intid {
Off,
/// What `irq-storm` floods this CPU with.
Storm,
Hda,
}

pub(super) const SGI_KICK: u32 = Intid::Kick as u32;
Expand Down Expand Up @@ -164,17 +160,20 @@ impl Gic {
pub fn init(rsdp_addr: u64) -> Gic {
let madt = toyos_acpi::find_table(direct_phys(), rsdp_addr, b"APIC", toyos_acpi::MADT_ENTRIES)
.unwrap_or_else(|e| panic!("GIC: the MADT is unusable: {e:?}"));
let (mut gicd, mut ranges, mut cpus) = (None, Vec::new(), Vec::new());
let (mut gicd, mut ranges, mut cpus, mut translators) = (None, Vec::new(), Vec::new(), Vec::new());
for entry in toyos_acpi::madt_entries(&madt) {
let entry = entry.unwrap_or_else(|halt| {
panic!("GIC: a MADT entry at +{} declares {} bytes of a {}-byte list", halt.at, halt.declared, halt.list_len)
});
match entry {
Ok(MadtEntry::Gicd { base, .. }) => gicd = Some(base),
Ok(MadtEntry::Gicr { base, length }) => ranges.push((
MadtEntry::Gicd { base, .. } => gicd = Some(base),
MadtEntry::Gicr { base, length } => ranges.push((
base,
crate::mm::paging::map_mmio(base, u64::from(length), MmioPolicy::Uncacheable),
)),
Ok(MadtEntry::Gicc(gicc)) if gicc.enabled => cpus.push(gicc),
Ok(_) => {}
Err(halt) => panic!("GIC: a MADT entry at +{} declares {} bytes of a {}-byte list", halt.at, halt.declared, halt.list_len),
MadtEntry::Gicc(gicc) if gicc.enabled => cpus.push(gicc),
MadtEntry::Its { id, base } => translators.push((id, base)),
_ => {}
}
}
let gicd = gicd.expect("GIC: the MADT names no distributor");
Expand Down Expand Up @@ -204,10 +203,18 @@ pub fn init(rsdp_addr: u64) -> Gic {
);

let gic = Gic { cpus, ranges };
init_cpu(gic.redistributor(cpu::hardware_id()));
let frame = gic.redistributor(cpu::hardware_id());
init_cpu(frame);
its::init(&translators, frame);
gic
}

/// `GICD_TYPER`, which says whether the distributor takes LPIs and how wide
/// an INTID is.
fn distributor_typer() -> u32 {
Mmio::new(DirectMap::from_phys(DISTRIBUTOR.load(Relaxed)), FRAME).read_u32(GICD_TYPER)
}

/// Bring this CPU's redistributor — the frame at `frame`, which
/// [`Gic::redistributor`] mapped — its CPU interface and its timer up: every
/// SGI and the timer's PPI enabled at [`PRIORITY`], the timer stopped.
Expand Down
Loading
Loading