Skip to content
Open
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,29 @@
---
status: open
kind: defect
opened: 2026-09-08
---

# `toyos-i219` refuses a part outside MSI-X mode at `IVAR`, and the T14's I219 is one

`toyos-i219`'s `open` writes §10.2.4.9's `IVAR` and reads it back, refusing a
part that does not take the write:

```
nic.regs.write(regs::IVAR, ivar::ALL_ON_VECTOR_ZERO);
nic.accepted(regs::IVAR, ivar::ALL_ON_VECTOR_ZERO)?;
```

§10.2.4.9 defines that register only "in MSI-X mode" and says nothing about what
a part outside it answers, so the read-back is a guess refused rather than a
guess driven on.

The T14's `00:1f.6` is outside that mode: Linux's own reading of the same
function is `IR-PCI-MSI-0000:00:1f.6 … enp0s31f6` in `/proc/interrupts` and
`mode=msi` at `/sys/bus/pci/devices/0000:00:1f.6/msi_irqs/162`. So the driver may
refuse the part the moment a claim on it is granted, and what `IVAR` answers on
an MSI part has never been read: no hand-over of `00:1f.6` has reached the
driver yet, so nothing has run the write.

Owned by the stage-2 I219 worker: the first `nic.accepted(regs::IVAR, …)` on the
bench either passes or names the register that has to be driven differently.

This file was deleted.

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

# Nothing asserts that a claim answers no configuration write

`SYS_DEVICE_REG_WRITE` on a `RegTarget::PciConfig` target is refused
`NotSupported` in `kernel/src/arch/syscall/device.rs`, and no test in any tier
reads that refusal. It is what a handed-over MSI function's safety rests on: its
message address and data are words of configuration space rather than a table in
a BAR, so nothing is withheld from the holder and the whole of the boundary is
that the write path does not exist. A one-field mutation there — the arm
answering `Ok` — hands the holder the ability to aim the device's write at any
address the LAPIC decodes, and every arm in every tier stays green.

The SDK's `PciDev` offers `config_read` and no write, so a driver cannot express
the call without reaching past it into `toyos_abi::syscall`.

Owned by whoever next adds a boot config with a test binary holding a claimable
function. Exit condition: a guest arm in which the holder calls
`SYS_DEVICE_REG_WRITE` on its own claim and the kernel refuses it, red against a
kernel whose `PciConfig` write arm answers `Ok`.
30 changes: 30 additions & 0 deletions issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
status: open
kind: tooling
opened: 2026-09-08
---

# Nothing reaches the MSI arm of a claimed function

`pcidev::bring_up` arms a claimed function on MSI where it publishes no MSI-X,
and no test in any tier arms one. `virtio_net_no_msix` calls
`PciDevice::enable_msi` from `bring_up` and reads false back; nothing reaches a
true, and so nothing reaches:

- `PciDevice::disable_msi` from either hand-back site (`bring_up`'s `place_bars`
failure and `tear_down`), or `Armed::Msi`'s teardown, which turns the
capability off where there is no table entry to mask;
- `Refusal::MsixUnusable` and `Unarmed::Blocked`, owed only by a function that
publishes MSI-X this kernel cannot arm and by a unit that refuses the message.

The two pre-existing MSI armings in this kernel — xHCI's and HDA's
`arm_interrupt` — never disarm, so MSI teardown is exercised nowhere in the tree
at all.

Owned by the network track's stage-2 I219 worker. Exit condition: the first
`userdev` interrupt counted against a claim on `00:1f.6` on the bench, which
needs the 32-bit BAR window before it, plus netd exiting from that claim, which
runs `tear_down`'s MSI arm. A guest exit is the alternative and costs more: an
actuator that hides a function's MSI-X capability from the claim path, a boot
config whose own test binary holds a claimable function, and the tier row and CI
price of the boot that carries them.
4 changes: 2 additions & 2 deletions kernel/src/drivers/hda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ fn probe(pci: &PciDevice) -> Option<(Mmio, u16, u16)> {
/// Put the function in D0 if firmware left it lower; D3hot reads all ones, indistinguishable from
/// an absent controller.
fn power_up(pci: &PciDevice) {
let Some(cap) = pci.capabilities().find(|c| c.id() == CAP_POWER_MANAGEMENT) else {
let Some(cap) = pci.capability(CAP_POWER_MANAGEMENT) else {
return;
};
let pmcsr = cap.read_u16(PM_CONTROL_STATUS);
Expand Down Expand Up @@ -663,7 +663,7 @@ fn reset_stream(stream: Mmio) -> bool {
/// panic, over a peripheral.
fn arm_interrupt(pci: &PciDevice) -> bool {
let vector = crate::arch::idt::HDA_VECTOR;
if pci.enable_msix(vector).is_some() || pci.enable_msi(vector) {
if pci.enable_msix(vector).is_ok() || pci.enable_msi(vector) {
return true;
}
log!(
Expand Down
51 changes: 40 additions & 11 deletions kernel/src/drivers/pci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ const MSG_ADDR: u32 = 0xFEE0_0000;
// The same CPU, named as a destination rather than encoded in an address, for the unit to put in an entry.
const MSG_DEST: u32 = 0;

/// Why [`PciDevice::enable_msix`] armed nothing. Named rather than collapsed
/// into one `None` because a caller choosing a mechanism answers each
/// differently.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unarmed {
/// This function publishes no MSI-X capability.
Absent,
/// It publishes one whose table this kernel could not reach.
Unusable,
/// The unit refuses this function's message, and MSI would carry the same one.
Blocked,
}

pub struct Capability<'a> {
device: &'a PciDevice,
offset: u64,
Expand Down Expand Up @@ -224,17 +237,16 @@ impl PciDevice {
///
/// Answers the entry's own window, which stays this kernel's: masking is a
/// write to it, and a claimant that could reach it could aim the device's
/// message at any address the LAPIC decodes. `None` is MSI-X that could not
/// be armed.
pub fn enable_msix(&self, vector: u8) -> Option<Mmio> {
let cap = self.capabilities().find(|c| c.id() == msix::CAP_ID)?;
/// message at any address the LAPIC decodes.
pub fn enable_msix(&self, vector: u8) -> Result<Mmio, Unarmed> {
let cap = self.capability(msix::CAP_ID).ok_or(Unarmed::Absent)?;
let control = cap.read_u16(msix::MESSAGE_CONTROL);
let table = match msix::Msix::decode(control, cap.read_u32(msix::TABLE)) {
Ok(table) => table,
Err(why) => {
log!("PCI {:02x}:{:02x}.{}: MSI-X not armed, {}",
self.bus, self.dev, self.func, why);
return None;
return Err(Unarmed::Unusable);
}
};
// Decoded, not assumed memory: a device may name a BAR that is an I/O BAR.
Expand All @@ -243,19 +255,19 @@ impl PciDevice {
Err(why) => {
log!("PCI {:02x}:{:02x}.{}: MSI-X not armed, its table names BAR {} and {}",
self.bus, self.dev, self.func, table.bir(), why);
return None;
return Err(Unarmed::Unusable);
}
};
let address = match table.table_address(base) {
Ok(address) => address,
Err(why) => {
log!("PCI {:02x}:{:02x}.{}: MSI-X not armed, {}",
self.bus, self.dev, self.func, why);
return None;
return Err(Unarmed::Unusable);
}
};

let (message, data) = self.message(vector)?;
let (message, data) = self.message(vector).ok_or(Unarmed::Blocked)?;

let entry = address + MSIX_ENTRY as u64 * msix::ENTRY_BYTES;
let table = crate::mm::paging::map_mmio(entry, 0x1000, MmioPolicy::Uncacheable);
Expand All @@ -271,14 +283,14 @@ impl PciDevice {
table.read_u32(msix::ENTRY_ADDRESS_LO),
table.read_u32(msix::ENTRY_DATA),
);
Some(table)
Ok(table)
}

/// Put MSI-X back off, for a hand-over that armed a vector and was then
/// refused: a function left enabled at a vector nobody holds delivers into
/// a slot with no reader.
pub fn disable_msix(&self) {
let Some(cap) = self.capabilities().find(|c| c.id() == msix::CAP_ID) else { return };
let Some(cap) = self.capability(msix::CAP_ID) else { return };
let control = cap.read_u16(msix::MESSAGE_CONTROL);
cap.write_u16(msix::MESSAGE_CONTROL, msix::Msix::disabled(control));
}
Expand Down Expand Up @@ -310,7 +322,7 @@ impl PciDevice {

/// Point this function's single MSI message at `vector` and enable it.
pub fn enable_msi(&self, vector: u8) -> bool {
let Some(cap) = self.capabilities().find(|c| c.id() == msi::CAP_ID) else {
let Some(cap) = self.capability(msi::CAP_ID) else {
return false;
};

Expand All @@ -337,6 +349,23 @@ impl PciDevice {
true
}

/// Put MSI back off: the counterpart of [`Self::disable_msix`].
///
/// The per-vector mask an arming cleared stays clear: a function whose Mask
/// bit this set would owe a message on the set-to-clear transition a later
/// arming makes of it, with its Pending bit set (PCIe §7.7.1.7).
pub fn disable_msi(&self) {
let Some(cap) = self.capability(msi::CAP_ID) else { return };
let control = cap.read_u16(msi::MESSAGE_CONTROL);
cap.write_u16(msi::MESSAGE_CONTROL, msi::Msi::disabled(control));
}

/// The capability this function publishes under `id`, or `None` where it
/// publishes none. Every reader that asks a function what it has asks here.
pub fn capability(&self, id: u8) -> Option<Capability<'_>> {
self.capabilities().find(|cap| cap.id() == id)
}

pub fn capabilities(&self) -> CapabilityIter<'_> {
let first = self.mmio.read_u8(CAPABILITIES_PTR);
CapabilityIter { device: self, walk: caps::CapWalk::new(), next: first }
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/drivers/virtio_sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ fn build_chains(
/// every period in flight forever.
fn arm_interrupt(pci: &PciDevice, device: &VirtioDevice) -> bool {
let vector = crate::arch::idt::VIRTIO_SOUND_VECTOR;
if pci.enable_msix(vector).is_none() {
if pci.enable_msix(vector).is_err() {
log!(
"virtio-sound: NOT INITIALISED at PCI {:02x}:{:02x}.{} — its MSI-X could not be \
armed and this driver has no other way to be told a period completed",
Expand Down
2 changes: 1 addition & 1 deletion kernel/src/drivers/xhci/wait/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn await_connect_settle(controllers: &[XhciController]) {
// `None` must stay a refusal, never a degradation: there is no polled mode, and
// every event-ring read depends on `irq_ring`, which only the ISR sets.
fn arm_interrupt(pci_dev: &PciDevice) -> Option<&'static str> {
if pci_dev.enable_msix(XHCI_VECTOR).is_some() {
if pci_dev.enable_msix(XHCI_VECTOR).is_ok() {
return Some("MSI-X");
}
pci_dev.enable_msi(XHCI_VECTOR).then_some("MSI")
Expand Down
Loading
Loading