iommu: assert directly that a confinement moves with its device

reassign was added at D4 to fix a regression and has been proven only
indirectly since — three IOMMU+USB cases going green. That covered the
visible symptom (a driver's DMA rings unbound) and neither of the latent
ones: the confinement still naming the giver, so the giver's death would
tear down a domain a live driver was using, and the receiver's death would
leave one behind. Those are now asserted.

confinementOwner exposes the record's owner so the suite can see it. The
sequence is the delegation in miniature: unconfined, confine as this task,
reassign to another, confirm the new holder owns it and the old one does
not, then kill the new holder and confirm the domain goes with it.

Two attempts at this test could not have failed. The first found no PCI
function to confine — pciAddressOf needs a pci_device entry and this case
runs no pci-bus — so every assertion skipped silently while the case stayed
green. It now synthesizes a function the way pci-bus does, a 4 KiB config
window inside the bridge's ECAM, and asserts that precondition explicitly so
a skip is a failure.

Verified to discriminate: making reassign a no-op flips three assertions,
including the domain surviving its holder's death.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 19:57:36 +01:00
parent 2ebfccc8ed
commit 3ae541214f
3 changed files with 64 additions and 4 deletions
+7 -4
View File
@@ -51,10 +51,13 @@ that cannot safely run in user space.**
| D8 | **`maximum_children_per_parent` deleted** — the authorisation it stood in for exists | **blocked on D6**, and now ordered after D9 | | D8 | **`maximum_children_per_parent` deleted** — the authorisation it stood in for exists | **blocked on D6**, and now ordered after D9 |
| D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | **done** — one of the two invented numbers is gone | | D9 | The device table becomes dynamic; **`maximum_devices` deleted**; per-holder quota declared | **done** — one of the two invented numbers is gone |
**Run 2 resumes at D0.** D1, D2, D4, D5 (`pci-bus` only) and D9 landed; `maximum_devices` **Run 2 stops, blocked.** Landed: D0, D1, D2, D4, D9, and D5 for three of five
no longer exists and the suite is 118/118. Questions 6 and 7 dissolved, so the order is claimants (`usb-xhci-bus`, `pci-bus`, `virtio-gpu`). `maximum_devices` no longer exists,
now **D0 → D5 → D6 → D8**, which deletes `maximum_children_per_parent`. Only D7 is still delegation is atomic with the spawn, and the suite is 118/118.
blocked, on question 8, and it is needed for neither ceiling. D10 is optional.
Blocked: **D6 and D8 on question 9** (`ps2-bus` needs two devices and ignores its
assignment; discovery needs a node nobody assigns), and **D7 on question 8**. So
`maximum_children_per_parent` — the second invented number — is one answer away.
Ordering is load-bearing. D1–D2 built and proved the mechanism with nothing depending on Ordering is load-bearing. D1–D2 built and proved the mechanism with nothing depending on
it. D4–D5 move each claimant across one at a time, so the suite stays green throughout it. D4–D5 move each claimant across one at a time, so the suite stays green throughout
+11
View File
@@ -234,6 +234,17 @@ pub fn reassign(device_id: u64, owner: u32) void {
record.owner = owner; record.owner = owner;
} }
/// The task a device's confinement is recorded against, or null if it has none. The
/// confinement's owner decides whose death tears the domain down, so a delegation that
/// moved the device but not this record would leave a live driver's domain destroyed by
/// its manager's exit — which is why `reassign` exists and why the suite asserts it.
pub fn confinementOwner(device_id: u64) ?u32 {
if (!active) return null;
if (device_id >= confined.len) return null;
const record = confined[@intCast(device_id)];
return if (record.active) record.owner else null;
}
pub fn releaseAllOwnedBy(owner: u32) void { pub fn releaseAllOwnedBy(owner: u32) void {
if (!active) return; if (!active) return;
for (confined) |*c| { for (confined) |*c| {
+46
View File
@@ -1447,6 +1447,52 @@ fn iommuTest() void {
} else { } else {
check("scratch domain allocated", false); check("scratch domain allocated", false);
} }
// Delegation moves a device between tasks, and the IOMMU confinement must move
// with it. When it did not, the driver's DMA rings were never bound into the
// device's domain and every transfer faulted — but two worse consequences were
// latent and invisible to those tests: the domain still named the *giver*, so the
// giver's death would tear down a domain a live driver was using, and the
// receiver's death would leave one behind. Asserted directly here rather than
// inferred from the USB cases going green.
// This case runs no pci-bus, so there are no PCI functions to confine — and a
// silently skipped assertion is worse than none. Register one the way pci-bus does:
// a child of the host bridge whose resource 0 is a 4 KiB config window inside the
// bridge's ECAM, which is what `pciAddressOf` derives a requester id from.
var iommu_table: [64]device_abi.DeviceDescriptor = undefined;
const iommu_total = devices_broker.enumerate(&iommu_table);
const bridge: ?device_abi.DeviceDescriptor = for (iommu_table[0..@min(iommu_total, iommu_table.len)]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_host_bridge) and d.resource_count >= 2) break d;
} else null;
check("the kernel seeded a PCI host bridge to parent a function under", bridge != null);
const subject: ?u64 = if (bridge) |b| blk: {
const me_bridge = scheduler.currentId();
if (!claimOk(b.id, me_bridge)) break :blk null;
var function = std.mem.zeroes(device_abi.DeviceDescriptor);
function.class = @intFromEnum(device_abi.DeviceClass.pci_device);
function.pci_class = device_abi.no_pci_class;
function.resource_count = 1;
function.resources[0] = .{
.kind = @intFromEnum(device_abi.ResourceKind.memory),
.start = b.resources[0].start, // the first config slot in the ECAM window
.len = 4096,
};
break :blk devices_broker.register(b.id, me_bridge, &function) catch null;
} else null;
check("a PCI function exists to confine", subject != null);
if (subject) |device_id| {
check("a device starts unconfined", iommu.confinementOwner(device_id) == null);
const me = scheduler.currentId();
check("confining records the owner", iommu.confineDevice(device_id, devices_broker.pciAddressOf(device_id).?, me));
check("the confinement names the confiner", iommu.confinementOwner(device_id) == me);
iommu.reassign(device_id, me + 1000);
check("reassign moves the confinement to the new holder", iommu.confinementOwner(device_id) == me + 1000);
check("and the previous holder no longer owns it", iommu.confinementOwner(device_id) != me);
iommu.releaseAllOwnedBy(me + 1000);
check("the new holder's death tears the domain down", iommu.confinementOwner(device_id) == null);
}
log("DANOS-IOMMU: enabled base=0x{x} domains active\n", .{pinfo.iommu_base}); log("DANOS-IOMMU: enabled base=0x{x} domains active\n", .{pinfo.iommu_base});
result(); result();
} }