Repository navigation
The SMMUv3 comes up from the IORT with every stream aborting, and both IOMMU backends read one fault policy, which halts only for a function this kernel drives, and one address window - #825
Conversation
… events reach the handler on their wired SPI The AArch64 IOMMU stub becomes arch/aarch64/smmu/, programmed with toyos-smmu's encodings (G1, #798): - init finds the one SMMUv3 the IORT names, sets GBPA to abort before anything else is written (and reads it back), turns off a unit left enabled, probes IDR0/1/5, gives every enumerated function the IORT routes through the unit an aborting stream table entry, brings up the command queue (CMD_CFGI_ALL, CMD_TLBI_NSNH_ALL, then a CMD_SYNC it waits on), the event queue and its wired interrupt, and sets SMMUEN. Any refusal leaves the unit aborting and says why. - domain: create/map/map_at/place/unmap/attach behind the generic seam, one stage 1 context descriptor per domain under its own ASID, 2 MiB leaves, addresses from 2^46 up to the first root-bridge window, unmap invalidated by ASID behind a CMD_SYNC, attach a CMD_CFGI_STE behind one. - fault: the event queue drained from the SPI with no lock, each record's function stopped from mastering, a claim's fault handed to pcidev::note_fault, a kernel-owned one halting the machine after the line, in VT-d's line format. - irqchip routes that one SPI, edge-triggered, to the boot CPU; trap's irq() takes it under the preempt count. - toyos-smmu names each event by its mnemonic. virt_smmu boots virt with iommu=smmuv3 and QEMU's iommu-testdev under the smmu-selftest actuator: the device's write is refused on the entry it starts with, lands where its own domain maps it, and where the domain maps nothing is refused and recorded, the event reaching the handler on SPI 106 named F_TRANSLATION for that function and address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Negative-control patches for head 562fc3a, each applied with nc1-gbpa-bypass (exit 1, red)diff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 8aba96fe5..5c153863c 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -260,13 +260,8 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[toyos_abi::boot::R
fn abort_unprogrammed(regs: Registers, base: u64) {
let gbpa = || regs.read(reg::GBPA);
regs.wait("GBPA free to update", || gbpa() & reg::GBPA_UPDATE == 0);
- regs.write(reg::GBPA, gbpa() | reg::GBPA_ABORT | reg::GBPA_UPDATE);
+ regs.write(reg::GBPA, gbpa() & !reg::GBPA_ABORT | reg::GBPA_UPDATE);
regs.wait("GBPA updated", || gbpa() & reg::GBPA_UPDATE == 0);
- assert!(
- gbpa() & reg::GBPA_ABORT != 0,
- "SMMU: GBPA reads {:#x} after ABORT was written: transactions bypass while SMMUEN is clear",
- gbpa()
- );
let cr0 = regs.read(reg::CR0);
if cr0 != 0 {
log!("IOMMU: the SMMUv3 at {base:#x} was handed over with CR0 {cr0:#x}; it goes off first");nc2-ste-bypass (exit 1, red)diff --git a/toyos-smmu/src/config.rs b/toyos-smmu/src/config.rs
index 6c28c1b4c..0995a79c4 100644
--- a/toyos-smmu/src/config.rs
+++ b/toyos-smmu/src/config.rs
@@ -26,7 +26,7 @@ impl Ste {
/// `V` set and `Config` `0b000`: every transaction of the stream is
/// aborted, and no event is recorded for it. An entry of all zeroes
/// aborts too, and records `C_BAD_STE` each time.
- pub const ABORT: Self = Self([STE_V, 0, 0, 0, 0, 0, 0, 0]);
+ pub const ABORT: Self = Self([STE_V | 0b100 << 1, 0, 0, 0, 0, 0, 0, 0]);
/// The stream translated by stage 1 through the one context descriptor
/// at `context`: `S1ContextPtr` [55:6], `S1CDMax` [63:59] zero so anc3-no-event-irq (exit 1, red)diff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 8aba96fe5..35f7ebcdb 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -366,7 +366,6 @@ fn program(
regs.write(reg::EVENTQ_CONS, 0);
fault::arm(regs, window(events_at, 32 << events_log2), events, devices, &live.routes);
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN, "its event queue enabled");
- regs.control(reg::IRQ_CTRL, reg::IRQ_EVENTQ, "its event interrupt enabled");
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN | reg::CR0_SMMUEN, "SMMUEN");
log!(nc5-spi-not-enabled (exit 1, red)diff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 863ce2afb..299ad0600 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -321,7 +321,6 @@ pub(super) fn route_iommu_events(intid: u32) -> Result<(), u32> {
gicd.write_u64(GICD_IROUTER + 8 * u64::from(intid), toyos_gicv3::unpacked_affinity(cpu::hardware_id()));
gicd.write_u32(GICD_ICPENDR + word, bit);
IOMMU_EVENTS.store(intid, Relaxed);
- gicd.write_u32(GICD_ISENABLER + word, bit);
Ok(())
}
|
|
Review of #825 at Net lines ( I checked that the x86 diff is empty: I checked the register sequence against IHI 0070 and found it correct:
BLOCKER
NOTE
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…y SMMUv3 the IORT names aborts before a refusal, its interrupts write nowhere, and every unrouted stream has one answer
The review of round 1 found the SMMUv3 bring-up's fault policy and domain
window to be second declarations of VT-d's, and four holes in the unit's
programming. This answers each.
One home for what both backends decide alike, above kernel/src/arch/:
- kernel/src/iommu/fault.rs holds the policy VT-d's handler applied: the
enumerated functions published once, BME cleared first, the per-function
and per-unit counts, the first faulting function, owner-or-halt, and the
`iommu: DMA FAULT` line. vtd/fault.rs keeps its recording registers and
reason names; smmu/fault.rs keeps its event queue. The line's format is
VT-d's, unchanged; the SMMUv3's now carries `reason=0x..` and VT-d's
`domain=` answer too. FIRST's address, reason and unit were written and
never read, and go; pci::NO_FUNCTION, their only sentinel, goes with them.
- kernel/src/iommu/window.rs holds a domain's addresses: the floor a quarter
up what the unit translates and above memory, the ceiling under the first
reserved window over it, reserve and handed_out, and both backends'
compile-time checks. VT-d keeps translatable_bits (SAGAW against MGAW).
issues/each-iommu-backend-keeps-its-own-domain-address-window.md is met:
`rg -n 'fn handed_out|fn ceiling' kernel/src/arch` prints nothing.
The SMMUv3:
- Every SMMUv3 the IORT names gets GBPA.ABORT before a machine with more
than one is refused: GBPA's reset value is IMPLEMENTATION DEFINED.
- IRQ_CTRL is cleared, acked, and GERROR_IRQ_CFG0, EVENTQ_IRQ_CFG0 and,
where IDR0.PRI is set, PRIQ_IRQ_CFG0 are zeroed before anything else, as
Linux's arm_smmu_setup_msis does: a non-zero ADDR makes the interrupt an
MSI write no STE governs, each resets UNKNOWN, and each is writable only
with its interrupt off (IHI 0070 H.a 3.18.2, 6.3.21, 6.3.30, 6.3.34).
- Every stream no enumerated function is routed from is aborted and
recorded, inside the stream table (an entry left invalid: C_BAD_STE) or
past it (CR2.RECINVSID: C_BAD_STREAMID), and halts the machine, as VT-d's
unenumerated requester does. Silence for both was tried first and is not
holdable: QEMU 11.1.1's hw/arm/smmuv3.c stores CR2 and never reads
RECINVSID, so an out-of-range StreamID is recorded there whatever CR2
says; measured, a C_BAD_STREAMID with RECINVSID clear.
- The event queue drains until it reads empty: the unit raises its
interrupt only as the queue goes from empty to non-empty (3.18.2), so a
record left behind raises nothing. An overflow is reported where it is
seen, mid-drain included.
- The edge trigger now cites IHI 0070 H.a 3.18.2 ("interrupt outputs are
effectively edge-triggered") and 12.4 ("required to be edge-triggered or
MSIs").
The selftest takes a second iommu-testdev, below the first, which the
actuator leaves unrouted, and writes four times: on the entry the routed one
starts with (refused, unrecorded), on its domain (lands), there again after
`unmap` (refused, recorded), and the unrouted one inside the table (refused,
recorded). The last two are made with interrupts masked so one drain reads
both; virt_smmu reads them as the unit's first and second events. The write
after `unmap` reds without its CMD_TLBI_NH_ASID: QEMU's SMMUv3 keeps an
IOTLB.
Issues: the SMMUv3's GERROR is filed as read and never delivered, owned by
stage 6's ITS work; a late write from a released function halting the
machine is filed against stage 6's claim through an SMMUv3 domain; the
descriptor half of the SMMUv3-and-ITS issue is re-owned to stage 4's owed
break-before-make work on paging.rs, and GICD_TYPER's two decoders are
added to its distributor half.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
… and atomic DATA commits (#816), into the SMMUv3 bring-up Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Negative controls for round 2, at
nc1-gbpa-bypass.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..5f68e1756 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -286,13 +286,8 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[toyos_abi::boot::R
fn abort_unprogrammed(regs: Registers, base: u64) {
let gbpa = || regs.read(reg::GBPA);
regs.wait("GBPA free to update", || gbpa() & reg::GBPA_UPDATE == 0);
- regs.write(reg::GBPA, gbpa() | reg::GBPA_ABORT | reg::GBPA_UPDATE);
+ regs.write(reg::GBPA, (gbpa() & !reg::GBPA_ABORT) | reg::GBPA_UPDATE);
regs.wait("GBPA updated", || gbpa() & reg::GBPA_UPDATE == 0);
- assert!(
- gbpa() & reg::GBPA_ABORT != 0,
- "SMMU: GBPA reads {:#x} after ABORT was written: transactions bypass while SMMUEN is clear",
- gbpa()
- );
let cr0 = regs.read(reg::CR0);
if cr0 != 0 {
log!("IOMMU: the SMMUv3 at {base:#x} was handed over with CR0 {cr0:#x}; it goes off first");nc2-ste-bypass.patchdiff --git a/toyos-smmu/src/config.rs b/toyos-smmu/src/config.rs
index 6c28c1b4c..0995a79c4 100644
--- a/toyos-smmu/src/config.rs
+++ b/toyos-smmu/src/config.rs
@@ -26,7 +26,7 @@ impl Ste {
/// `V` set and `Config` `0b000`: every transaction of the stream is
/// aborted, and no event is recorded for it. An entry of all zeroes
/// aborts too, and records `C_BAD_STE` each time.
- pub const ABORT: Self = Self([STE_V, 0, 0, 0, 0, 0, 0, 0]);
+ pub const ABORT: Self = Self([STE_V | 0b100 << 1, 0, 0, 0, 0, 0, 0, 0]);
/// The stream translated by stage 1 through the one context descriptor
/// at `context`: `S1ContextPtr` [55:6], `S1CDMax` [63:59] zero so anc3-no-event-irq.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..005776da1 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -402,7 +402,6 @@ fn program(
regs.write(reg::EVENTQ_CONS, 0);
fault::arm(regs, window(events_at, 32 << events_log2), events, &live.routes);
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN, "its event queue enabled");
- regs.control(reg::IRQ_CTRL, reg::IRQ_EVENTQ, "its event interrupt enabled");
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN | reg::CR0_SMMUEN, "SMMUEN");
log!(nc5-spi-not-enabled.patchdiff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 04ec59f72..717a6a712 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -321,7 +321,6 @@ pub(super) fn route_iommu_events(intid: u32) -> Result<(), u32> {
gicd.write_u64(GICD_IROUTER + 8 * u64::from(intid), toyos_gicv3::unpacked_affinity(cpu::hardware_id()));
gicd.write_u32(GICD_ICPENDR + word, bit);
IOMMU_EVENTS.store(intid, Relaxed);
- gicd.write_u32(GICD_ISENABLER + word, bit);
Ok(())
}
nc6-unmap-no-tlbi.patchdiff --git a/kernel/src/arch/aarch64/smmu/domain.rs b/kernel/src/arch/aarch64/smmu/domain.rs
index 4b61ff63c..4afeab440 100644
--- a/kernel/src/arch/aarch64/smmu/domain.rs
+++ b/kernel/src/arch/aarch64/smmu/domain.rs
@@ -167,7 +167,7 @@ pub fn unmap(id: DomainId, at: Iova, bytes: u64) -> Result<(), IommuError> {
}
}
// Whatever was cleared before a refusal is gone from the unit too.
- live.issue(&[Command::InvalidateAsid(asid)]);
+ live.issue(&[Command::InvalidateAsid(asid)][..0]);
result
}
nc7-every-entry-aborting.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..ee74eb7e7 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -358,9 +358,9 @@ fn program(
let streams = window(table, 64 << log2);
// Every other entry stays zero, invalid: a stream no function is routed
// from is recorded, as one past the table is.
- for (_, stream) in &routes {
+ for stream in 0..1u32 << log2 {
for (i, word) in Ste::ABORT.words().iter().enumerate() {
- streams.write_u64(u64::from(*stream) * 64 + 8 * i as u64, *word);
+ streams.write_u64(u64::from(stream) * 64 + 8 * i as u64, *word);
}
}
let (commands_log2, events_log2) =nc8-drain-one-record.patchdiff --git a/kernel/src/arch/aarch64/smmu/fault.rs b/kernel/src/arch/aarch64/smmu/fault.rs
index 8c273b10c..ba9b9f661 100644
--- a/kernel/src/arch/aarch64/smmu/fault.rs
+++ b/kernel/src/arch/aarch64/smmu/fault.rs
@@ -84,7 +84,7 @@ pub fn service() {
}
cons = events.after(cons, prod);
regs.write(reg::EVENTQ_CONS, cons);
- prod = regs.read(reg::EVENTQ_PROD);
+ prod = cons;
}
let errors = reg::active_errors(regs.read(reg::GERROR), regs.read(reg::GERRORN));
if errors & reg::GERROR_EVENTQ_ABORT != 0 { |
|
The argv is virtio-console, whole (323 lines)PL011, whole (214 lines; the console above carries the kernel's record from the start) |
|
The argv is virtio-console, whole (311 lines)PL011, whole (210 lines; the console above carries the kernel's record from the start) |
|
Review of #825 at Round-1 BLOCKERs
I read the x86 side line by line against
Ruling: unrouted streamsAborted and recorded, yes. Halting, no, on either backend.
The right answer for both backends:
BLOCKER
NOTE
SEND BACK |
|
T14 at |
…805), into the SMMUv3 bring-up Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
…goes on; the SMMUv3 reads at most a queue's worth of events per interrupt The shared fault policy halted on every record with no user owner, and that included a record naming no enumerated function: a requester this kernel never took on, whose write the unit had already refused. A hot-plugged function, a VF a claimed PF enabled, or any device writing under a StreamID no route names could then halt the machine at will. Fail-fast answers a defect of this kernel; a refused device input is not one. `report` now answers "halt" only for an enumerated function this kernel drives, and a record naming none is logged under `owner=none`, which the harness reads as a record (never clean) and not as a death. The VT-d line is unchanged but for that word, and an unenumerated requester on VT-d no longer halts. Nothing can clear `BME` on a requester nobody enumerated, so the handler's work per interrupt is bounded by itself: VT-d's already reads at most `CAP.NFR` records, and the SMMUv3's drain now reads at most the queue's entries per interrupt and, if records remain, pends its SPI again through `GICD_ISPENDR`, because the unit raises no edge for a record left behind (IHI 0070 H.a §3.18.2). `smmu-selftest` now has the unrouted testdev write twice, both read in one drain, and says the machine went on; the kernel-driven function's write on its taken-back address comes last and is the halting one. `virt_smmu` reads on to `panic_reboot::arm`'s line, which the halt path writes once every other CPU is stopped, and reds on a `FAIL` line or any death but the record: before, it returned at the record and never read the selftest's "the machine went on" panic. `toyos-smmu`'s per-mnemonic name test is deleted: it was a second copy of `Code::name`'s match, and the two names a gate reads are held by `virt_smmu`. A function the SMMUv3 does not route still panics at `attach` rather than being refused at `create`; filed with an owner and an exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Negative controls for round 3, at
nc9-conclude-never-halts.patchdiff --git a/kernel/src/iommu/fault.rs b/kernel/src/iommu/fault.rs
index 8c6eb26e6..54615c5ca 100644
--- a/kernel/src/iommu/fault.rs
+++ b/kernel/src/iommu/fault.rs
@@ -234,7 +234,7 @@ pub fn report(unit: usize, count: &AtomicU32, fault: Fault) -> bool {
/// other drivers are untouched — goes on. A requester nobody enumerated has
/// no driver here to be wrong, and the unit already refused what it sent.
pub fn conclude(kernel_owned: usize) {
- if kernel_owned > 0 {
+ if kernel_owned > usize::MAX - 1 {
crate::drivers::panic_console::capture();
crate::panic::halt_all_cpus();
}nc10-unrouted-halts.patchdiff --git a/kernel/src/iommu/fault.rs b/kernel/src/iommu/fault.rs
index 8c6eb26e6..289be3913 100644
--- a/kernel/src/iommu/fault.rs
+++ b/kernel/src/iommu/fault.rs
@@ -216,7 +216,7 @@ pub fn report(unit: usize, count: &AtomicU32, fault: Fault) -> bool {
if FIRST.load(Ordering::Relaxed) == key { 'y' } else { 'n' },
fault.name,
);
- matches!(owner, Owner::Kernel)
+ !matches!(owner, Owner::Slot(_))
}
/// The end of a drain that read `kernel_owned` faults on functions this kernelnc8-one-record-not-repended.patchdiff --git a/kernel/src/arch/aarch64/smmu/fault.rs b/kernel/src/arch/aarch64/smmu/fault.rs
index 995602fd0..9b715459c 100644
--- a/kernel/src/arch/aarch64/smmu/fault.rs
+++ b/kernel/src/arch/aarch64/smmu/fault.rs
@@ -67,7 +67,7 @@ pub fn service() {
let mut kernel_owned = 0usize;
let mut cons = regs.read(reg::EVENTQ_CONS);
let mut prod = regs.read(reg::EVENTQ_PROD);
- for _ in 0..events.entries() {
+ for _ in 0..1 {
if events.is_empty(prod, cons) {
break;
}
@@ -93,7 +93,6 @@ pub fn service() {
prod = regs.read(reg::EVENTQ_PROD);
}
if !events.is_empty(prod, cons) {
- super::super::irqchip::pend_iommu_events();
}
let errors = reg::active_errors(regs.read(reg::GERROR), regs.read(reg::GERRORN));
if errors & reg::GERROR_EVENTQ_ABORT != 0 {nc11-one-record-repended.patchdiff --git a/kernel/src/arch/aarch64/smmu/fault.rs b/kernel/src/arch/aarch64/smmu/fault.rs
index 995602fd0..19f7966a2 100644
--- a/kernel/src/arch/aarch64/smmu/fault.rs
+++ b/kernel/src/arch/aarch64/smmu/fault.rs
@@ -67,7 +67,7 @@ pub fn service() {
let mut kernel_owned = 0usize;
let mut cons = regs.read(reg::EVENTQ_CONS);
let mut prod = regs.read(reg::EVENTQ_PROD);
- for _ in 0..events.entries() {
+ for _ in 0..1 {
if events.is_empty(prod, cons) {
break;
}nc1-gbpa-bypass.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..5f68e1756 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -286,13 +286,8 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: &[toyos_abi::boot::R
fn abort_unprogrammed(regs: Registers, base: u64) {
let gbpa = || regs.read(reg::GBPA);
regs.wait("GBPA free to update", || gbpa() & reg::GBPA_UPDATE == 0);
- regs.write(reg::GBPA, gbpa() | reg::GBPA_ABORT | reg::GBPA_UPDATE);
+ regs.write(reg::GBPA, (gbpa() & !reg::GBPA_ABORT) | reg::GBPA_UPDATE);
regs.wait("GBPA updated", || gbpa() & reg::GBPA_UPDATE == 0);
- assert!(
- gbpa() & reg::GBPA_ABORT != 0,
- "SMMU: GBPA reads {:#x} after ABORT was written: transactions bypass while SMMUEN is clear",
- gbpa()
- );
let cr0 = regs.read(reg::CR0);
if cr0 != 0 {
log!("IOMMU: the SMMUv3 at {base:#x} was handed over with CR0 {cr0:#x}; it goes off first");nc2-ste-bypass.patchdiff --git a/toyos-smmu/src/config.rs b/toyos-smmu/src/config.rs
index 6c28c1b4c..0995a79c4 100644
--- a/toyos-smmu/src/config.rs
+++ b/toyos-smmu/src/config.rs
@@ -26,7 +26,7 @@ impl Ste {
/// `V` set and `Config` `0b000`: every transaction of the stream is
/// aborted, and no event is recorded for it. An entry of all zeroes
/// aborts too, and records `C_BAD_STE` each time.
- pub const ABORT: Self = Self([STE_V, 0, 0, 0, 0, 0, 0, 0]);
+ pub const ABORT: Self = Self([STE_V | 0b100 << 1, 0, 0, 0, 0, 0, 0, 0]);
/// The stream translated by stage 1 through the one context descriptor
/// at `context`: `S1ContextPtr` [55:6], `S1CDMax` [63:59] zero so anc3-no-event-irq.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..005776da1 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -402,7 +402,6 @@ fn program(
regs.write(reg::EVENTQ_CONS, 0);
fault::arm(regs, window(events_at, 32 << events_log2), events, &live.routes);
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN, "its event queue enabled");
- regs.control(reg::IRQ_CTRL, reg::IRQ_EVENTQ, "its event interrupt enabled");
regs.control(reg::CR0, reg::CR0_CMDQEN | reg::CR0_EVENTQEN | reg::CR0_SMMUEN, "SMMUEN");
log!(nc5-spi-not-enabled.patchdiff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 04ec59f72..717a6a712 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -321,7 +321,6 @@ pub(super) fn route_iommu_events(intid: u32) -> Result<(), u32> {
gicd.write_u64(GICD_IROUTER + 8 * u64::from(intid), toyos_gicv3::unpacked_affinity(cpu::hardware_id()));
gicd.write_u32(GICD_ICPENDR + word, bit);
IOMMU_EVENTS.store(intid, Relaxed);
- gicd.write_u32(GICD_ISENABLER + word, bit);
Ok(())
}
nc6-unmap-no-tlbi.patchdiff --git a/kernel/src/arch/aarch64/smmu/domain.rs b/kernel/src/arch/aarch64/smmu/domain.rs
index 4b61ff63c..4afeab440 100644
--- a/kernel/src/arch/aarch64/smmu/domain.rs
+++ b/kernel/src/arch/aarch64/smmu/domain.rs
@@ -167,7 +167,7 @@ pub fn unmap(id: DomainId, at: Iova, bytes: u64) -> Result<(), IommuError> {
}
}
// Whatever was cleared before a refusal is gone from the unit too.
- live.issue(&[Command::InvalidateAsid(asid)]);
+ live.issue(&[Command::InvalidateAsid(asid)][..0]);
result
}
nc7-every-entry-aborting.patchdiff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs
index 6df76be26..ee74eb7e7 100644
--- a/kernel/src/arch/aarch64/smmu/mod.rs
+++ b/kernel/src/arch/aarch64/smmu/mod.rs
@@ -358,9 +358,9 @@ fn program(
let streams = window(table, 64 << log2);
// Every other entry stays zero, invalid: a stream no function is routed
// from is recorded, as one past the table is.
- for (_, stream) in &routes {
+ for stream in 0..1u32 << log2 {
for (i, word) in Ste::ABORT.words().iter().enumerate() {
- streams.write_u64(u64::from(*stream) * 64 + 8 * i as u64, *word);
+ streams.write_u64(u64::from(stream) * 64 + 8 * i as u64, *word);
}
}
let (commands_log2, events_log2) = |
|
T14 reading at
Judge ( |
|
Review of #825 at Round-2 BLOCKERs
Round-2 NOTEs
BLOCKERNone. NOTE
LAND AFTER NAMED CHANGES |
…he re-pend cites IHI 0069D A requester no enumerated function is cannot be stopped from here: its BME is nobody's to clear, so a device writing without end under an unrouted stream wrote one owner=none line per record, up to a queue's worth per interrupt, and could evict every other record from the shard ring before logkeeper read it. Each stray is now counted apart (the first eight by key, every later one in a shared ninth count), the count goes in streamfaults=, and only a record whose count is a power of two is written: at most 32 lines per count for the boot, and each written line says how many were read since the last. unitfaults= still counts every record, so a later line shows the gap. The selftest's unrouted function now writes three times: records 1 and 2 are written, 3 is counted and not, and the kernel-owned record reads unitfaults=4, which is how virt_smmu sees a record read and not written. The comment at pend_iommu_events cites the GICv3 sections the re-pend rests on: GICD_ISPENDR makes an active SPI active and pending (8.9.16), which is never signalled (4.1.2), and deactivation leaves it pending (4.1.1, with EOImode 0 as init writes ICC_CTLR_EL1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
|
Round 4 at
diff --git a/kernel/src/iommu/fault.rs b/kernel/src/iommu/fault.rs
index f55043863..d2718ab58 100644
--- a/kernel/src/iommu/fault.rs
+++ b/kernel/src/iommu/fault.rs
@@ -228,7 +228,7 @@ pub fn report(unit: usize, count: &AtomicU32, fault: Fault) -> bool {
let count = count.fetch_add(1, Ordering::Relaxed) + 1;
// A stray's line is bounded by its count, never by the device: the next
// line it writes says how many were read in between.
- if function.is_none() && !seen_here.is_power_of_two() {
+ if false && function.is_none() && !seen_here.is_power_of_two() {
return false;
}
log!(
diff --git a/kernel/src/arch/aarch64/smmu/fault.rs b/kernel/src/arch/aarch64/smmu/fault.rs
index 995602fd0..9b715459c 100644
--- a/kernel/src/arch/aarch64/smmu/fault.rs
+++ b/kernel/src/arch/aarch64/smmu/fault.rs
@@ -67,7 +67,7 @@ pub fn service() {
let mut kernel_owned = 0usize;
let mut cons = regs.read(reg::EVENTQ_CONS);
let mut prod = regs.read(reg::EVENTQ_PROD);
- for _ in 0..events.entries() {
+ for _ in 0..1 {
if events.is_empty(prod, cons) {
break;
}
@@ -93,7 +93,6 @@ pub fn service() {
prod = regs.read(reg::EVENTQ_PROD);
}
if !events.is_empty(prod, cons) {
- super::super::irqchip::pend_iommu_events();
}
let errors = reg::active_errors(regs.read(reg::GERROR), regs.read(reg::GERRORN));
if errors & reg::GERROR_EVENTQ_ABORT != 0 {
diff --git a/kernel/src/iommu/fault.rs b/kernel/src/iommu/fault.rs
index 8c6eb26e6..289be3913 100644
--- a/kernel/src/iommu/fault.rs
+++ b/kernel/src/iommu/fault.rs
@@ -216,7 +216,7 @@ pub fn report(unit: usize, count: &AtomicU32, fault: Fault) -> bool {
if FIRST.load(Ordering::Relaxed) == key { 'y' } else { 'n' },
fault.name,
);
- matches!(owner, Owner::Kernel)
+ !matches!(owner, Owner::Slot(_))
}
/// The end of a drain that read `kernel_owned` faults on functions this kernel
diff --git a/kernel/src/arch/aarch64/smmu/fault.rs b/kernel/src/arch/aarch64/smmu/fault.rs
index 995602fd0..19f7966a2 100644
--- a/kernel/src/arch/aarch64/smmu/fault.rs
+++ b/kernel/src/arch/aarch64/smmu/fault.rs
@@ -67,7 +67,7 @@ pub fn service() {
let mut kernel_owned = 0usize;
let mut cons = regs.read(reg::EVENTQ_CONS);
let mut prod = regs.read(reg::EVENTQ_PROD);
- for _ in 0..events.entries() {
+ for _ in 0..1 {
if events.is_empty(prod, cons) {
break;
}virt_smmu at 3fc0086, EXIT=0control nc12-every-stray-record-written, EXIT=1control nc11-one-record-repended, EXIT=0 |
|
Round 4 at control nc8-one-record-not-repended, EXIT=1control nc10-unrouted-halts, EXIT=1 |
|
T14 reading at The staged request carried no sha256 lines. The orchestrator therefore hashed the two staged images ( Judge ( |
…ring-up (#825) and the hotkey removal (#824), into consent The one conflict is Profile::arch in tests/common/qemu.rs: main adds HeadlessUsbSpare and this branch adds Desktop to the same x86-64 arm; both stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Stage G2 of the AArch64 desktop plan: the SMMUv3 the IORT names is armed with every stream aborting, a domain can be created, mapped and attached through stage 1 context descriptors for G4's claims, and the unit's event-queue records reach
pcidev::note_fault, or halt the machine, through its wired SPI. Builds on G1'stoyos-acpiIORT decoder andtoyos-smmuencodings (#798). What both IOMMU backends decide alike, the fault policy and a domain's address window, is declared once abovekernel/src/arch/and read by VT-d and the SMMUv3 both. One policy change reaches x86-64 too: a DMA fault record that names no enumerated function no longer halts the machine on either backend.Head measured:
3fc00867c, onorigin/mainata1eb2c0b9(#805), which93c4107c8merged.What changed, per decision
One home for what both backends decide alike (
kernel/src/iommu/):fault.rsholds the policy VT-d's handler applied: the enumerated functions published once before any unit is armed,BMEcleared first, the per-function and per-unit counts, the first faulting function, and theiommu: DMA FAULTline.vtd/fault.rskeeps its fault recording registers and reason names;smmu/fault.rskeeps its event queue. A record names aWho: a requester id, or an SMMU StreamID no enumerated function is routed from.owner=says which:owner=kernel, and the machine halts (conclude). Its fault is a defect of this kernel.owner=slot<N>,pcidevis told, and the machine goes on.owner=none, and the machine goes on. This is a requester this kernel never took on, whose write the unit has already refused. Onmain, VT-d halted here, and round 2 extended that to the SMMUv3. A hot-plugged function, a VF a claimed PF enabled, or any device writing under an unrouted StreamID could then halt the machine at will. "Fail fast" answers a defect in this kernel; a device's input that the unit already refused is not one.owner=noneline is written only when its stray's count is a power of two. Nothing can stop such a requester, so a device writing without end under an unrouted stream would otherwise write one line per record and evict every other record from the log ring before logkeeper reads it. Each stray is counted apart, the first eight byWho::keyin a lock-free table and every later one in a shared ninth count. The count is the line'sstreamfaults=, so each written line says how many were read since the last, andunitfaults=still counts every record. That is at most 32 lines per count for the boot. A function's records (owner=kernel,owner=slot<N>) are all written, as before.tests/common/serial.rsreads onlyowner=kernelas a death.owner=none, likeowner=slot, is inNEVER_CLEAN: a boot that did not stage one reds on it.tests/checks/serial.rsholds both directions.BMEon a requester nobody enumerated, so each backend bounds a handler's work per interrupt by itself. VT-d already reads at mostCAP.NFRrecords per interrupt. The SMMUv3 now reads at most its queue's entries (next section).window.rsis a domain's addresses: the floor a quarter up what the unit translates and above memory, the ceiling under the first root-bridge window or reserved region over it,reserve,handed_out, and both backends' compile-time checks, the T14's windows andvirt's among them. VT-d keepstranslatable_bits(SAGAWagainstMGAW); the SMMUv3 passes its 48-bit input.FIRST's address, reason and unit were written and never read, so they are gone, andpci::NO_FUNCTION, their only sentinel, goes with them.issues/each-iommu-backend-keeps-its-own-domain-address-window.mdis closed: its exitrg -n 'fn handed_out|fn ceiling' kernel/src/archprints nothing.The SMMUv3 (
kernel/src/arch/aarch64/smmu/, re-exported asiommu_unitthe way x86-64 re-exportsvtd):GBPA.ABORTbefore any refusal. That covers a machine with more than one unit, which is then refused and every unit left aborting, sinceGBPA's reset value is IMPLEMENTATION DEFINED. TheUpdatehandshake runs, the value is read back and asserted, and a unit handed over enabled is turned off first.IRQ_CTRLis cleared and acked. ThenGERROR_IRQ_CFG0,EVENTQ_IRQ_CFG0and, whereIDR0.PRIis set,PRIQ_IRQ_CFG0are zeroed, as Linux'sarm_smmu_setup_msisdoes. A non-zeroADDRmakes the interrupt an MSI write that no STE governs. Each register resets UNKNOWN and can be written only while its interrupt is off (IHI 0070 H.a §3.18.2, §6.3.21, §6.3.30, §6.3.34).Ste::ABORTand records nothing. Firmware may leave a function mastering the bus until its driver resets it.owner=none):C_BAD_STEon an entry left invalid, orC_BAD_STREAMIDunderCR2.RECINVSID.hw/arm/smmuv3.cstoresCR2and never readsRECINVSID. Measured in round 1: withRECINVSIDclear, an out-of-range StreamID was still recorded asC_BAD_STREAMID. Recording both is what QEMU can hold. Since neither halts, it costs a log line.CR1/CR2, then the stream table and command queue, thenCMDQEN, thenCMD_CFGI_ALL+CMD_TLBI_NSNH_ALL+CMD_SYNC, waited on. Then come the event queue,EVENTQEN,IRQ_CTRL.EVENTQ_IRQENandSMMUEN, each waited on its*ACK. Every wait is bounded by a one-secondTripwireand panics by name, and it panics onGERROR.CMDQ_ERRwithCMDQ_CONS.ERRdecoded.toyos_smmu::table::plan. The addresses come fromwindow.rs.unmapissuesCMD_TLBI_NH_ASID+CMD_SYNCbefore it returns, and a test fails without it (nc6).attachwrites the STE's first doubleword last, then issuesCMD_CFGI_STE+CMD_SYNC.Events::entries, 128 here), and a drain that stops short of empty pends the SPI again throughGICD_ISPENDR. The unit raises its interrupt only as the queue goes from empty to non-empty (§3.18.2: "Event queue transitions from empty to non-empty"), so a record left behind raises nothing. The re-pend is taken onceenddeactivates the SPI (IHI 0069D, under High-risk checks), so the CPU leaves the handler between batches, and a device that never stops writing cannot hold it. Round 2 drained until empty with no bound, which a stream with noBMEto clear could make endless. An overflow is reported where it is seen, including mid-drain.irqchip::route_iommu_events. DEN 0049 gives the GSIV no trigger, and Linux'siort.cregisters it edge-triggered.irqchip.rsandtrap.rsnext.irqchip.rsgainsroute_iommu_eventsandpend_iommu_events;trap.rs'sirq()gains one arm, under the preempt count.toyos-smmu:GERROR_IRQ_CFG0,EVENTQ_IRQ_CFG0,PRIQ_IRQ_CFG0andIDR0_PRIare declared, and each has a host test at its §6.2 offset or bit. A decodedEventcarries its number, which feeds the line'sreason=.Code::name()gives the §7.3 mnemonic. Round 2's per-mnemonic test is deleted: it was a second copy ofname's match, and the two names a gate reads (F_TRANSLATION,C_BAD_STE) are held byvirt_smmu.Events::entriesis the drain's bound.Harness:
Profile::VirtSmmuadds aShape::smmudimension,AbsentorWithTestdev.WithTestdevputsiommu=smmuv3on the machine and two of QEMU'siommu-testdevon bus 0. A q35 that declares it is refused by name.virt_smmureads on topanic_reboot::arm's line.halt_all_cpuswrites that line once every other CPU is stopped, so nothing the guest can still say is missed. The test reds on anysmmu-selftest: FAILline and on any death other than theowner=kernelrecord. Round 2 returned at the record and never read the selftest's "the machine went on" panic.smmu-selftest, in order:owner=none,streamfaults=1and=2) and the third is counted and not, and the selftest says the machine went on.owner=kernelF_TRANSLATIONwithunitfaults=4, which is how the test sees the third stray record read and not written, and the machine halts.The x86 side
VT-d's fault handler and domain window read the shared modules. Its behaviour changes in one case: a record whose source-id is no enumerated function was
owner=kernel … bme=unknown-function domain=unknownand halted. It is nowowner=none, and the machine goes on; its line is written only at a power of two of that source-id's count, which is itsstreamfaults=(was 0). Everything else is meant to be identical, and a reader can check it:DMA FAULTline's format string is unchanged; only that one case'sOwnerword, itsstreamfaults=and whether it is written differ.BMEis cleared, then the function and unit are counted,FIRSTlatched,pcidevtold, the line logged and the record cleared.concludehalts beforeapic::eoi, as before.Window::newrefuses exactly whereDomain::newdid (WindowBelowMemory,NoRoomwhen belowmax(round(room), 2 MiB)), then reservesroom, ascreatedid. The one difference is wherebytes.next_multiple_of(PAGE_2M)would overflow: before, that panicked in debug and wrapped in release; nowreserveandhanded_outrefuse it.rebindtakes the domain's root, id and width instead of theDomain, so its compile-time check no longer builds aDomain.What a run shows: the x86-64 image builds; the guest suite's x86 boots, every one of which creates domains through
window.rsfor its kernel drivers, are green. No guest test on this host reaches VT-d's fault handler. The orchestrator ran the T14'sboot:testcasesat93c4107c8, round 3's head (comment 6096300030): both images' sha256 matched the staged ones (f8a91ae7…,852c54d4…), each boot rc=0, and the judge returned EXIT=0, "249 passed, 0 failed, 2 boot(s)". That reads the moved VT-d policy and window on real hardware. This round's change is in the sharedreport, which the T14's VT-d handler also runs, so it is staged again at3fc00867cwith readback only (testcasessha25616e8c2ec…9e1c8c2b,testcases-watchdogsha256e6a7b1f4…2af7a971). That run is owed and not yet read. No row there stages a fault from an unenumerated source-id, so the new bound is reached on the SMMUv3 alone.Gates (head
3fc00867c; logs under the orchestrator'sarm-g2/r3/round4/logs/, each opening with its head)cargo run -- --ci hostcargo run -- --build-only(x86-64)cargo run -- --build-only --arch aarch64cargo test --test toyos-build -- virt_smmucargo test --test toyos-build, the whole suite,virt_smmuin itcargo test --test toyos-build -- --metal --metal-readback <dir> boot:testcasescargo test -p toyos-smmu(exit 0 at93c4107c8) was not re-run: this round does not touchtoyos-smmu, and--ci hostruns it. The whole suite ran under load:uptimeread 11.67 17.00 28.21 before and 16.05 17.38 27.71 after.virt_smmu's echo (HVF, QEMU 11.1.1), from its own run; the full log is a comment:(The echo prints the selftest's verdicts before the records, and not the "goes on" line. The test asserts the console's own order: the second
owner=nonerecord, then "the unrouted function's three events were read, and the machine goes on", then theowner=kernelrecord, then the stop's line. It also asserts exactly three records were written, so the third stray record's line is absent;unitfaults=4shows it was read.)bme=clearedsays the handler found the function and wrote itsCOMMANDregister. It is not read back.iommu-testdevwrites whatever itsCOMMANDholds, so nothing on this architecture measures the storm ceiling that clearingBMEis.cargo run --arch aarch64, booted (round 2, at9490bcfdf)src/qemu.rspassesiommu=smmuv3tocargo run --arch aarch64, so the unit is armed on that machine. Both boots ransrc/qemu.rs's argv for--arch aarch64(Profile::Virtio,--smp 8, HVF), changed only where it reaches the host. Both logs are posted in full as comments. Neither was re-run at this head. What this head changes on that machine is the drain's bound, theowner=noneword and that line's bound, and neither boot had aDMA FAULT.origin/main(5a3b74345):IOMMU: the SMMUv3 is the port's stage 6; no device is translated this boot. The boot ends wherecompositorpanics atsession.rs:151, then 20 s of silence.9490bcfdf: the unit is armed with 8 functions' streams. virtio-console, virtio-sound and virtio-gpu move to domains 1, 2 and 3. NoDMA FAULT. The boot ends at the samecompositorpanic.High-risk checks (device memory isolation)
Independent oracles.
QEMU 11.1.1's SMMUv3 model is a second implementation of IHI 0070, and it is asked through
iommu-testdev, which makes a DMA write at a device address and reads the word back at a physical one.0xdead0002means the unit refused the write.0x0means it landed exactly where the domain maps it, which only an STE, CD and stage 1 walk that QEMU accepted can produce.RECINVSID).IHI 0070 H.a was read for the trigger (§3.18.2, §12.4), the
*_IRQ_CFG0registers (§6.3.21, §6.3.30, §6.3.34),RECINVSID(§6.3.12) and Service Failure Mode (§12.3).IHI 0069D (GICv3/v4, issue D) was read for the re-pend. The PDF is Arm's
documentation-service.arm.com/static/6012f2e54ccc190e5e681256, sha25684aa6255…929957f4c. Three passages carry it:GICD_ISPENDR<n>: writing 1 "changes the state of the corresponding interrupt from inactive to pending, or from active to active and pending".pend_iommu_eventswrites it from inside the handler, while the SPI is active.end.ICC_CTLR_EL1.EOImode0 the priority drop and deactivation happen together on theICC_EOIR1_EL1write, andirqchip.rswritesICC_CTLR_EL1as zero, soendis the deactivation. The SPI is then pending, routed to this CPU and enabled, and is signalled again.GICD_ISPENDRwrite, or from the unit's own edge. Either way it is pending, which is all the re-pend needs.pend_iommu_eventscites the three sections. nc11 measures the same thing on HVF's vGIC.Linux's
arm_smmu_setup_msisis the reference for the zeroing.Every encoding is held on the host to H.a's field positions (
toyos-smmu/tests/encodings.rs).Negative controls. Each was applied as a checked patch, built, run through
virt_smmu, and restored in the same script. nc12, nc8, nc10 and nc11 ran at3fc00867c; their patches and logs are in this comment and this one. nc8, nc10 and nc11 were re-run because the selftest's stray now writes three times. The rest ran at93c4107c8, and their patches are in an earlier comment.false && …)virt_smmu: 4 events reached the handler, not 3concludenever halts (the review's mutation)smmu-selftest: FAIL: the handler read the event of a function this kernel drives and the machine went onreporthalts on every record without a user owner (round 2's predicate)the selftest never said "which its domain no longer maps": halted after the first drain'sowner=nonerecordsthe handler had read 1 events, not 3, 1000ms … after the unrouted function's three refused writesGBPAwritten withoutABORT, the kernel's assert removedGBPA does not read back aborting: Some(4096)Ste::ABORTmadeConfig 0b100(bypass)… on the entry its stream starts with, answered 0x0: FAILIRQ_CTRL.EVENTQ_IRQENnever writtenthe handler had read 0 events, not 2 …GICD_ISENABLERbit never writtenthe handler had read 0 events, not 2 …unmapissues noCMD_TLBI_NH_ASIDthe handler had read 2 events, not 3 … after the kernel-driven function's refused writeSte::ABORTthe handler had read 0 events, not 2 …toyos-smmuchange reverted ontoa1eb2c0b9, the test kept"smmu-selftest" is a kernel_params and the kernel declares no such actuatorWhat no test here reaches.
virthas one unit.*_IRQ_CFG0zeroing: QEMU 11.1.1 sends no MSI from these registers;smmuv3_trigger_irqonly pulses the wired line.RECINVSID: QEMU records an out-of-range StreamID whateverCR2says. Its bit is held on the host.iommu-testdevwrites once per trigger, so no guest fills 128 records in one interrupt. nc8/nc11 show the re-pend path at a bound of one.owner=nonepath: no guest on this host reaches VT-d's fault handler. The SMMUv3 reaches the samereport.u32wrap are read, not run.Each of these is a reading of the diff against the spec.
Why the guest test needs QEMU
virtwithiommu-testdevis the cheapest tier where a DMA meets the unit.iommu-testdevis the only function a guest can make write under a StreamID no function is routed from. The actuator leaves it unrouted, so its entry is the one a never-enumerated function would have.Filed
issues/the-smmuv3s-global-errors-are-read-and-never-delivered.md:GERRORis read in waits and after drains, its SPI is not routed, andSFM_ERRandEVENTQ_ABT_ERRraised between reads go unseen. Owner: stage 6's ITS work.issues/a-late-write-from-a-released-function-halts-the-machine.md:tear_downclears the ownership before the reset quiesces in-flight DMA, so a late faulting write isowner=kerneland halts. This holds on VT-d today. Owner: stage 6's claim through an SMMUv3 domain, which lands before G4.issues/a-function-the-smmuv3-does-not-route-panics-at-attach.md:createtakes no function, so a function the IORT routes elsewhere, or not at all, panics inLive::streamatattachinstead of being refused atcreate. Not reachable onvirt. Owner: stage 6's claim.issues/the-smmuv3-and-its-crates-declare-what-the-aarch64-kernel-also-declares.md: the descriptor is re-owned to stage 4's owed break-before-make work, andGICD_TYPER's two decoders are added with an exit.What I am unsure of
RECINVSID.kernel/src/iommu/mod.rs. Itsinitdoc still names a context entry per function.Net lines
git diff --shortstat origin/main...3fc00867c: 30 files, +1898 / −566.kernel/but the selftest and the actuator): +1364 / −545.tests/,toyos-smmu/tests, the selftest, the actuator): +381 / −10.toyos-smmu/src,Cargo.lock,kernel/Cargo.toml: +51 / −4.This round's own commit,
93c4107c8..3fc00867c: 5 files, +61 / −24.🤖 Generated with Claude Code
https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C