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

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ members = [
"build/root",
"build/ssh",
"kernel",
"kernel/amdvi",
"kernel/cpuvuln",
"kernel/dma",
"kernel/gicv3",
Expand Down
1 change: 1 addition & 0 deletions build/sourcegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const BUS_MASTER_SITES: &[(&str, usize)] = &[
("kernel/src/drivers/virtio.rs", 1),
("kernel/src/drivers/hda.rs", 1),
("kernel/src/drivers/xhci/wait/boot.rs", 1),
("kernel/src/arch/x86_64/amdvi/mod.rs", 1),
];

/// The one place in `kernel/` a `!!!` may still appear, and how many times.
Expand Down
47 changes: 24 additions & 23 deletions issues/an-amd-vi-machine-isolates-no-device.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,31 @@ Held by the orchestrator's AMD laptop track: its stage 5, amdvi-translate, build
translation, and its stage 6, amdvi-intremap, interrupt remapping.

On a machine whose IVRS describes AMD-Vi units, `kernel/src/arch/x86_64/amdvi/`
switches every unit off, so every device's DMA reaches all of memory and every
message-signalled interrupt is delivered as written. `DeviceSpace::create`
answers `Untranslated`, and a function a process would drive is refused its
claim (`iommu::remapping` is `NotRemapped`), so no userland driver runs there.

What remains is translation, then interrupt remapping:

- **Translation.** A device table blocking by default, every one of the 65,536
entries `V=1, TV=1` with `IR=IW=0` — an entry left `V=0` passes its
requester's DMA untranslated — identity domains for kernel drivers, a
command buffer whose `COMPLETION_WAIT` is waited on under a `Tripwire`, and
the event log feeding `iommu::fault`.
- **Interrupt remapping.** Interrupt remapping tables, the I/O APIC's and the
HPET's requester ids from the IVRS's special entries, and `claim_msi`.
translates (stage 5): every requester id's entry blocks unless it names a
domain, enumerated functions sit on an identity domain until a kernel driver
moves one to its own, and the event log feeds `iommu::fault`. No interrupt
is remapped: every entry's interrupt half is invalid, so every
message-signalled interrupt is delivered as written, and a function a
process would drive is refused its claim (`iommu::remapping` is
`NotRemapped`), so no userland driver runs there.

What remains is interrupt remapping (stage 6): a remapping table per
requester indexed by vector, the I/O APICs' requester ids from the IVRS's
special entries, `claim_msi`, a claim clearing its entry's pass bits and
`SysMgt`, and a claim of a function an IVMD names refused.

Constraints a reader would otherwise pay to re-derive:

- QEMU 11.1.1's `amd-iommu` translates a function only with `dma-remap=on`,
and only once an `INVALIDATE_DEVTAB_ENTRY` names it; it takes a device table
entry of `Mode` 0 as pass-through whatever `IR` and `IW` say, where the
specification blocks it (`hw/i386/amd_iommu.c` at v11.1.1).
- No AMD machine is in reach of the metal suite.

**Exit:** a guest boot on `Profile::MetalAmdVi` in which a kernel driver's
DMA goes through a domain of its own, a DMA outside it is recorded as a
fault, and a claimed function's message is delivered through its own
remapping entry.
and only once an `INVALIDATE_DEVTAB_ENTRY` names it
(`issues/qemus-invalidate-iommu-all-leaves-every-function-untranslated.md`); it takes a
device table entry of `Mode` 0 as pass-through whatever `IR` and `IW` say,
where the specification blocks it, so the blocked default is unobservable
there; and its `IO_PAGE_FAULT` record carries no address
(`issues/qemus-amd-iommu-page-fault-record-carries-no-address.md`).
- The AMD bench is in the metal suite; the orchestrator runs every boot.

**Exit:** a guest boot on `Profile::MetalAmdVi` and a run on the AMD bench
in which a kernel driver's DMA goes through a domain of its own, a DMA
outside it is recorded as a fault, and a claimed function's message is
delivered through its own remapping entry.
29 changes: 29 additions & 0 deletions issues/qemus-amd-iommu-page-fault-record-carries-no-address.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
status: open
kind: tooling
opened: 2026-10-13
---

# QEMU's `amd-iommu` writes an `IO_PAGE_FAULT` record with no address

AMD 48882 rev 3.11, Figure 56 and Table 57: an `IO_PAGE_FAULT` record
carries the device address the requester tried in its second doubleword,
and in its first `RW` at bit 53 (+04 bit 21), `PR` at 52, `I` at 51 and
`NX` at 49. QEMU 11.1.1's `amd-iommu`, refusing an `iommu-testdev` write at
`0x400000000000` under `amdvi-selftest`, wrote
`0x200a000000000030 0x0000000000000000`: the right code and DeviceID
(`00:06.0`), the address zero, and bits 51 and 49 set, which 48882 reads as
an interrupt with `NX`. Measured by logging the raw record in
`kernel/src/arch/x86_64/amdvi/fault.rs` on PR #909's branch.

So `amdvi_selftest` (`tests/common/iommu.rs`) holds the record to its
requester and its name, not its address or its access, and the record's
`addr=`/`access=` under QEMU are QEMU's, not the device's.

Owner: the orchestrator's AMD laptop track
(`issues/an-amd-vi-machine-isolates-no-device.md`); the AMD bench's
`usbd` fault row reads a real unit's record at its aimed address.

**Exit:** under the QEMU `build/ci/qemu-version` names, the record of
`amdvi-selftest`'s refused write names `0x400000000000` and a write, and
`amdvi_selftest` asserts both.
1 change: 1 addition & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ toyos-abi = { path = "../sdk/abi" }
[target.'cfg(target_os = "none")'.dependencies]
bcachefs = { path = "../lib/bcachefs", default-features = false }
toyos-acpi = { path = "../lib/acpi" }
toyos-amdvi = { path = "amdvi" }
toyos-dma = { path = "dma" }
toyos-blackbox = { path = "../lib/blackbox" }
toyos-blockhold = { path = "../lib/blockhold" }
Expand Down
19 changes: 19 additions & 0 deletions kernel/amdvi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# A member of the host workspace (root `Cargo.toml`), like toyos-smmu: it is
# pure and its tests run on the host.
#
# What lives here is every word an AMD-Vi unit and its driver exchange. The
# unit decides which memory a device reaches, so a field one bit off is an
# isolation hole no boot shows. Each encoding is held on the host to AMD
# 48882's own field positions, and the kernel's `arch::x86_64::amdvi` is left
# the registers and the memory.

[package]
name = "toyos-amdvi"
description = "AMD-Vi's encodings: device table entries, host page table entries, commands, event records and register fields, pure, forbid(unsafe_code)."
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false

[dependencies]
toyos-phys = { path = "../phys" }
67 changes: 67 additions & 0 deletions kernel/amdvi/src/dte.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! The device table: one 256-bit entry per requester id (§2.2.2, Figure 7,
//! Table 7), indexed by the id.

use toyos_phys::Phys;

/// Every requester id a segment has; the table holds an entry for each.
pub const DEVICE_IDS: usize = 1 << 16;
/// Bytes per entry.
pub const ENTRY_BYTES: usize = 32;

const V: u64 = 1 << 0;
const TV: u64 = 1 << 1;
const MODE_SHIFT: u32 = 9;
const IR: u64 = 1 << 61;
const IW: u64 = 1 << 62;
/// `SysMgt`, bits 105:104: bits 41:40 of the second doubleword.
const SYS_MGT_SHIFT: u32 = 40;

/// The host table depth every translating entry names: `Mode` 100b, a 48-bit
/// input.
pub const MODE: u8 = 4;

/// What an IVHD device entry's DTE-setting byte declares for its requesters
/// (§5.2.2, Table 106). Of it, a translation entry carries `SysMgt` alone:
/// the pass bits are the interrupt half's, which nothing here sets valid.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Setting(pub u8);

impl Setting {
/// Bits 5:4, the `SysMgt` firmware declares the requester needs.
pub const fn sys_mgt(self) -> u64 {
(self.0 as u64 >> 4) & 0b11
}
}

/// One entry's translation half, the 128 bits a translation change replaces
/// in one write (§2.2.2.2); the interrupt half above it stays zero, `IV`
/// clear, so the unit forwards a requester's interrupts unremapped (Table 9).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Dte([u64; 2]);

impl Dte {
/// `V`, `TV`, `Mode` 000b and `IR`/`IW` clear: every read and write the
/// requester makes is target-aborted (Table 7 bits 11:9, 61, 62). Tagged
/// with `domain`, which Table 8 requires valid wherever `V` is set.
pub const fn blocked(domain: u16, setting: Setting) -> Self {
Self([V | TV, domain as u64 | setting.sys_mgt() << SYS_MGT_SHIFT])
}

/// Translating through the four-level table at `root`, tagged `domain`:
/// the requester reaches what that table maps and nothing else.
pub const fn translated(root: Phys<12>, domain: u16, setting: Setting) -> Self {
Self([
V | TV | (MODE as u64) << MODE_SHIFT | root.get() | IR | IW,
domain as u64 | setting.sys_mgt() << SYS_MGT_SHIFT,
])
}

pub const fn words(self) -> [u64; 2] {
self.0
}

/// The domain an entry read back out of the table is tagged with.
pub const fn domain_of(words: [u64; 2]) -> u16 {
words[1] as u16
}
}
18 changes: 18 additions & 0 deletions kernel/amdvi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Every word an AMD-Vi unit and its driver exchange, as AMD 48882 rev 3.11
//! lays it out: the device table entry ([`dte`]), the host page tables a
//! translating entry names ([`table`]), the commands and event records
//! ([`queue`]), and the register fields this driver writes or reads ([`reg`]).
//!
//! The crate encodes one configuration: a requester is blocked, or translated
//! through a four-level host table that maps 2 MiB pages, read and write,
//! forced coherent. No entry passes DMA untranslated.
//!
//! `no_std`, no allocation, no `unsafe`.

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

pub mod dte;
pub mod queue;
pub mod reg;
pub mod table;
83 changes: 83 additions & 0 deletions kernel/amdvi/src/queue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! The command buffer's commands (§2.4) and the event log's records (§2.5):
//! 128 bits each, a four-bit code in bits 63:60 of the first doubleword.

use toyos_phys::Phys;

/// Entries a 4 KiB command buffer or event log holds, its length field's
/// minimum (MMIO 0008h and 0010h, `1000b`).
pub const ENTRIES: usize = 256;

const CODE_SHIFT: u32 = 60;

/// A command this driver sends.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Command {
/// `COMPLETION_WAIT` (Figure 44) with `s`: once every earlier command has
/// completed, the unit stores `data` at `store`.
CompletionWait { store: Phys<3>, data: u64 },
/// `INVALIDATE_DEVTAB_ENTRY` (Figure 45): the unit rereads this id's entry.
InvalidateDevice(u16),
/// `INVALIDATE_IOMMU_PAGES` (Figure 46) over every address of one domain,
/// directories with them: `S` and `PDE` set, address 7_FFFF_FFFF_FFFFh.
InvalidateDomain(u16),
/// `INVALIDATE_IOMMU_ALL` (Figure 51): every cache the unit holds.
InvalidateAll,
}

impl Command {
pub const fn words(self) -> [u64; 2] {
match self {
Self::CompletionWait { store, data } => [1 << CODE_SHIFT | store.get() | 1, data],
Self::InvalidateDevice(id) => [2 << CODE_SHIFT | id as u64, 0],
Self::InvalidateDomain(domain) => [3 << CODE_SHIFT | (domain as u64) << 32, 0x7_FFFF_FFFF_FFFF << 12 | 0b11],
Self::InvalidateAll => [8 << CODE_SHIFT, 0],
}
}
}

/// One event record, decoded as far as what it ends.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Event {
/// A record a requester caused, which the unit has already refused or
/// dropped: `IO_PAGE_FAULT` (Figure 56), `IOTLB_INV_TIMEOUT` (Figure 61),
/// `INVALID_DEVICE_REQUEST` (Figure 62), and `INVALID_PPR_REQUEST` with
/// `RX` clear (Figure 64). `write` is the access where the record states
/// one; `address` is the record's, whatever it names.
Requester { code: u8, device: u16, address: u64, write: Option<bool> },
/// Every other record: an entry or command of this driver's the unit
/// refused, or the unit's own hardware failing.
Unit { code: u8 },
}

pub const fn event(words: [u64; 2]) -> Event {
let device = words[0] as u16;
let code = (words[0] >> CODE_SHIFT) as u8;
let write = match code {
// `I` [51]: an interrupt carries no read or write; else `RW` [53].
0b0010 if words[0] & 1 << 51 != 0 => None,
0b0010 => Some(words[0] & 1 << 53 != 0),
// `RX` [48] set is a `COMPLETE_PPR_REQUEST` of this driver's failing.
0b1001 if words[0] & 1 << 48 != 0 => return Event::Unit { code },
// `WP` [53]: the write permission the peripheral asked for.
0b1001 => Some(words[0] & 1 << 53 != 0),
0b0111 | 0b1000 => None,
_ => return Event::Unit { code },
};
Event::Requester { code, device, address: words[1], write }
}

/// The name 48882 gives an event code, for a record's line.
pub const fn name(code: u8) -> &'static str {
match code {
0b0001 => "ILLEGAL_DEV_TABLE_ENTRY",
0b0010 => "IO_PAGE_FAULT",
0b0011 => "DEV_TAB_HARDWARE_ERROR",
0b0100 => "PAGE_TAB_HARDWARE_ERROR",
0b0101 => "ILLEGAL_COMMAND_ERROR",
0b0110 => "COMMAND_HARDWARE_ERROR",
0b0111 => "IOTLB_INV_TIMEOUT",
0b1000 => "INVALID_DEVICE_REQUEST",
0b1001 => "INVALID_PPR_REQUEST",
_ => "reserved",
}
}
Loading