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,37 @@
---
status: open
kind: defect
opened: 2026-10-04
---

# A boot paint at `subsystems ready` stalled 4.3 ms on the T14

One T14 boot of `lantalkcase`, at `43cbe73d4` (pull request #710's head, run
on 2026-10-04), wrote `panel: paints=10 px=8513536 us=25527 max_us=8194`. The
machine's record for that boot is `boot.lantalkcase.panel_max_us = 3919`
(`tests/metal/lenovo-20w0003amz.toml`), and the judge's ceiling 7838, so the
judge exited 1. The same kernel's four other boots of that run read `max_us`
3921, 3802, 3878 and 3870, and the negative control's four 3837, 3883, 3927
and 3853.

The extra time is one stall, not slower painting: the pixel count matches the
other boots (8513536 against 8496384..8518016), and the total paint time is
about 4 ms over theirs (25527 µs against 20828..21906), all of it in the one
maximum. It sits at the `Boot: subsystems ready` checkpoint's paint: on that
boot the next record after `subsystems ready` (0.244 s) is at 0.253 s and the
xHCI's first remapping entry at 0.254 s, where every other boot of both
kernels reads 0.248..0.249 s for the one and 0.249 s for the other; `storage
ready` is late by as much (0.740 s against 0.734..0.736).

The review of #710 read it as not that change's
(https://github.com/ToyOSOrg/ToyOS/pull/710#issuecomment-5976306774): every
panel paint is at a boot checkpoint, the last at `Boot: complete`, before the
I219's claim; and nothing the change adds runs between `peripherals ready` and
the claim. What stalled a paint for 4.3 ms is not known — a single boot, with
no instrument inside the paint.

Owner: the orchestrator, who holds the T14 record. Exit: the stall's cause is
named from a measurement — the review's alternating `lantalkcase` boots of
`e81d26db8` and `43cbe73d4`, three each, comparing `max_us` and the gap from
`subsystems ready` to the next record, is the first — and removed, and three
`lantalkcase` boots then read `panel_max_us` inside the record's ceiling.
27 changes: 27 additions & 0 deletions issues/kernel/a-claimed-function-can-storm-its-cpu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
status: open
kind: defect
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
them, and each one is an interrupt cpu0 takes before anything else it runs.
`issues/kernel/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
chosen without a measurement behind it is the silent decision this file
exists to refuse.

Owner: the IOMMU track. Exit: a claimed function's messages past a bound the
kernel derives and states are stopped at its entry and recorded against its
claim, and a T14 row with a holder that provokes them shows cpu0's timer and
other sources still served.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ under the T14's `CONFIG_INTEL_IOMMU_DEFAULT_ON=y` and
default domain (`drivers/iommu/iommu.c:197-210`), which maps only what its
driver maps.

**Owner ruling, 2026-10-04: a T14 row in which a test program claims the GPU
(`00:02.0`, alone in `rmrr0`) is allowed, and the kernel panel goes dark for
that boot.** It is the one row that can reach a claim of a function with a
reserved region of its own.

**Exit**: a function's DMA reaches only what its driver mapped and the reserved
ranges firmware names for it; a `boot-actuators` arm that points an unclaimed
function's DMA at a kernel page records a translation fault, and binding it to
Expand Down
18 changes: 11 additions & 7 deletions issues/kernel/a-machine-without-an-iommu-refuses-every-claim.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Owner ruling, 2026-09-29: on a machine with no IOMMU unit a driver signed and
shipped in the ToyOS image may claim a device, and no other may.
It is the same driver code as on a machine with a unit, never a second
driver: the kernel's DMA layer hands it a physical address where there is no
unit and a domain address where there is (`DeviceSpace`,
`kernel/src/iommu/mod.rs`). Such a machine states plainly that it has no
unit and a domain address where there is. A claim's space is an
`iommu::OwnSpace` (`kernel/src/iommu/mod.rs`), which has no untranslated form
until the ruled path adds one. Such a machine states plainly that it has no
isolation — the full isolation guarantee needs an IOMMU, and there a bad
signed driver can still crash or corrupt the system.

Expand All @@ -20,11 +21,14 @@ where it is checked) and so what the "identity check" compares. No signature
or image-identity mechanism exists in the kernel today. The exit below cannot
be written as a test until it is.

Today `pcidev`'s `bring_up` refuses every claim on such a machine with
`Refusal::Untranslated`, whoever asks. A claim that goes on without a domain
cannot pass through `slot_space` as it stands: it calls `reserve`, grants
call `place`, and both panic on `DeviceSpace::Untranslated` — a kernel panic
a userland claim would reach.
Today `pcidev`'s `bring_up` refuses every claim on such a machine first with
`Refusal::NotRemapped`, whoever asks: no unit remaps its interrupts, and a
claimed function is armed only through `iommu::claim_msi`, which takes a
`Remapping` such a machine never mints. Behind that, `Refusal::Untranslated`:
a slot's space is an `iommu::OwnSpace`, which has no untranslated form, so
there is no physical address for a grant to answer with. The signed driver's
claim is the path where `NotRemapped` and `Untranslated` both give way under
the ruling.

Exit, in both arms of `iommu_virtio_platform` (`tests/common/iommu.rs`):

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ with translation on` at 0.083 s, just before translation goes off, and
`translating` at 0.090 s, just after it is back; two of the hand-over's own log
lines fall between.

One case keeps the window whatever this kernel does: a unit handed over with
`TTM` not `00` and `CAP.ESRTPS` clear, where §6.6 forbids switching tables
under translation, so `TE` has to go off. **Owner ruling, 2026-10-04: accept
it and file it.** The fix that closes this issue logs that case and files it
as its own defect, whose exit covers the window with protected memory regions
(`PMEN` over every pmm frame) or has the kernel speak scalable mode.
No machine here reaches it.

**Exit**: under `iommu-firmware-left`, on QEMU and on the T14, no unit logs
`was handed over with translation on; it goes off first`, and every unit's
`translating` line reads `tes=y`.
25 changes: 0 additions & 25 deletions issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: tooling
opened: 2026-09-08
---

# Nothing reaches the MSI refusal arms of a claimed function

`pcidev::bring_up` arms a claimed function on MSI where it publishes no MSI-X.
The T14's I219 is one: `claim_reuses_its_remapping_entry` arms it through
`arm_claimed_msi` and releases it through `tear_down`'s MSI arm, and
`lan_message_delivery` counts its messages. Nothing reaches:

- `bring_up`'s refusal after the MSI is armed — `place_bars` refusing a
function armed on MSI — where `PciDevice::disable_msi` runs and the slot's
remapping entry is dropped;
- `Refusal::MsixUnusable`, owed only by a function that publishes MSI-X this
kernel cannot arm.

Owned by the network track's stage-2 I219 worker. Exit condition: a bench run
logs a claim refused after its `msi address=` line, with that slot's
`irteN … p=0 released` after it; and a function whose MSI-X cannot be armed is
refused by `MsixUnusable`'s reason.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-10-04
---

# x2APIC is enabled without reading firmware's opt-out

VT-d Rev. 4.1 §8.1, DMAR Flags bit 1 `X2APIC_OPT_OUT`: firmware asks system
software "to opt out of enabling Extended xAPIC (X2APIC) mode", and software
checks it "as part of detecting X2APIC mode support". This kernel enables
x2APIC unconditionally in `arch::apic::init` (`enable_x2apic`), reached from
`arch::boot::interrupts`, before the DMAR is first read in `iommu::init`
(`kernel/src/main.rs`). The flag is logged (`x2apic_opt_out=` on the `iommu:
DMAR` line) and decides nothing.

The owner's ruling on extended interrupt mode
(`issues/kernel/the-iommu-refuses-nothing-yet.md`) is about the width of a
remapping entry's destination, not whether x2APIC is on, so it does not cover
this. The T14 clears the flag (`flags=0x05`), so no machine in reach sets it.

Owner: the IOMMU track. Exit: the flag is read before the local APIC is put in
x2APIC mode, and a machine that sets it either runs xAPIC or is refused by
name; an actuator standing in for the flag reaches both arms on the T14.
4 changes: 4 additions & 0 deletions kernel/src/actuator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ actuators! {
/// kernel programs it. Judged by `iommu_firmware_left`.
iommu_firmware_left = "iommu-firmware-left";

/// Read the DMAR's flags with `INTR_REMAP` clear, as on a platform whose
/// firmware says it does not remap. Judged by `claim_refused_without_remapping`.
iommu_no_remap = "iommu-no-remap";

/// Leave every AP holding the CR0/CR4 that INIT left it.
no_ap_control_regs = "no-ap-control-regs";

Expand Down
20 changes: 14 additions & 6 deletions kernel/src/arch/aarch64/iommu_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
use crate::drivers::pci::PciDevice;
use crate::log;

pub fn init(_rsdp_addr: u64, _devices: &[PciDevice]) {
pub fn init(
_rsdp_addr: u64,
_devices: &[PciDevice],
_windows: &[toyos_abi::boot::RootBridgeWindow],
) {
log!("IOMMU: the SMMUv3 is the port's stage 6; no device is translated this boot");
}

pub mod domain {
use crate::iommu::{DomainId, IommuError, Iova, StreamId};

/// No unit is driven, so there is no domain to give.
pub fn create() -> Result<DomainId, IommuError> {
pub fn create(_room: u64) -> Result<(DomainId, Iova), IommuError> {
Err(IommuError::NoUnit)
}

Expand All @@ -24,10 +28,6 @@ pub mod domain {
unreachable!("no domain exists: `create` refuses every one")
}

pub fn reserve(_id: DomainId, _bytes: u64) -> Result<Iova, IommuError> {
unreachable!("no domain exists: `create` refuses every one")
}

pub fn place(_id: DomainId, _at: Iova, _phys: u64, _bytes: u64) -> Result<u16, IommuError> {
unreachable!("no domain exists: `create` refuses every one")
}
Expand Down Expand Up @@ -63,6 +63,14 @@ pub mod interrupt {
unreachable!("no interrupt remapping without an IOMMU unit, and `is_armed` said so")
}

pub fn claim(_slot: usize, _source: StreamId, _vector: u8) -> Msi {
unreachable!("no interrupt remapping without an IOMMU unit, and `is_armed` said so")
}

pub fn release(_slot: usize, _source: StreamId) {
unreachable!("no interrupt remapping without an IOMMU unit, and `is_armed` said so")
}

pub fn pin(_apic_id: u8, _vector: u8, _dest: u32, _level: bool) -> Result<Pin, Refused> {
unreachable!("no interrupt remapping without an IOMMU unit, and `is_armed` said so")
}
Expand Down
33 changes: 16 additions & 17 deletions kernel/src/arch/x86_64/vtd/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ struct Domains {
/// By id minus [`FIRST`]; never shrinks, since a released id would name a
/// domain some unit may still have cached.
live: Vec<Domain>,
/// What no domain's addresses may reach: the root bridges' windows and
/// the regions firmware reserved, as `(start, end)`.
reserved: Vec<(u64, u64)>,
}

static DOMAINS: Lock<Domains> =
Lock::new(Domains { agreement: Agreement::None, live: Vec::new() });
Lock::new(Domains { agreement: Agreement::None, live: Vec::new(), reserved: Vec::new() });

/// Before any domain is made: every one is built clear of these.
pub fn avoid(reserved: Vec<(u64, u64)>) {
DOMAINS.lock().reserved = reserved;
}

pub fn unit_agrees(width: AddressWidth, ceiling: u32, mgaw: u8) {
let mut domains = DOMAINS.lock();
Expand All @@ -57,7 +65,9 @@ pub fn unit_agrees(width: AddressWidth, ceiling: u32, mgaw: u8) {
};
}

pub fn create() -> Result<DomainId, IommuError> {
/// A new domain, with `room` bytes of it handed out first: refused, with no id
/// spent, where the domain would have less than that.
pub fn create(room: u64) -> Result<(DomainId, Iova), IommuError> {
let mut domains = DOMAINS.lock();
let (width, ceiling, mgaw) = match domains.agreement {
Agreement::None => return Err(IommuError::NoUnit),
Expand All @@ -68,7 +78,8 @@ pub fn create() -> Result<DomainId, IommuError> {
if u32::from(id) >= ceiling {
return Err(IommuError::DomainsExhausted(ceiling));
}
let domain = Domain::new(&mut TABLES.lock(), id, width, mgaw)?;
let mut domain = Domain::new(&mut TABLES.lock(), id, width, mgaw, &domains.reserved, room)?;
let first = domain.reserve(room).expect("`Domain::new` refuses a domain without the room");
log!(
"iommu: domain{id} root={:#x} aw={} mgaw={} addresses from {:#x} to {:#x}",
domain.root().phys(),
Expand All @@ -78,7 +89,7 @@ pub fn create() -> Result<DomainId, IommuError> {
domain.ceiling()
);
domains.live.push(domain);
Ok(DomainId::new(id))
Ok((DomainId::new(id), first))
}

pub fn map(id: DomainId, phys: u64, bytes: u64) -> Result<Iova, IommuError> {
Expand All @@ -87,11 +98,7 @@ pub fn map(id: DomainId, phys: u64, bytes: u64) -> Result<Iova, IommuError> {
}
let mut domains = DOMAINS.lock();
let domain = domains.at(id);
let at = domain
.reserve(bytes)
// Named by what actually ran out: the unit's translatable width, which
// on a machine whose `MGAW` is under its `SAGAW` is not the table depth.
.ok_or(IommuError::AddressesExhausted(domain.translatable()))?;
let at = domain.reserve(bytes).ok_or(IommuError::AddressesExhausted(domain.ceiling()))?;
let (did, domain) = (domain.id(), *domain);
let mut units = UNITS.lock();
table::map(&mut TABLES.lock(), &domain, at, phys, bytes);
Expand All @@ -107,14 +114,6 @@ pub fn map(id: DomainId, phys: u64, bytes: u64) -> Result<Iova, IommuError> {
Ok(at)
}

/// Hand out room for `bytes` and map nothing there: a caller that places its
/// own mappings in it later, with [`place`].
pub fn reserve(id: DomainId, bytes: u64) -> Result<Iova, IommuError> {
let mut domains = DOMAINS.lock();
let domain = domains.at(id);
domain.reserve(bytes).ok_or(IommuError::AddressesExhausted(domain.translatable()))
}

/// Put `bytes` at `phys` at `at`, room this domain handed out before and whose
/// mapping was taken back: a device still aimed there reaches these pages.
/// Room it never handed out is a kernel bug, since [`map`] may yet hand it out.
Expand Down
Loading
Loading