diff --git a/docs/bounds-track-plan.md b/docs/bounds-track-plan.md index 3cb7be2..92a56f0 100644 --- a/docs/bounds-track-plan.md +++ b/docs/bounds-track-plan.md @@ -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 | | 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` -no longer exists and the suite is 118/118. Questions 6 and 7 dissolved, so the order is -now **D0 → D5 → D6 → D8**, which deletes `maximum_children_per_parent`. Only D7 is still -blocked, on question 8, and it is needed for neither ceiling. D10 is optional. +**Run 2 stops, blocked.** Landed: D0, D1, D2, D4, D9, and D5 for three of five +claimants (`usb-xhci-bus`, `pci-bus`, `virtio-gpu`). `maximum_devices` no longer exists, +delegation is atomic with the spawn, and the suite is 118/118. + +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 it. D4–D5 move each claimant across one at a time, so the suite stays green throughout diff --git a/system/kernel/iommu.zig b/system/kernel/iommu.zig index 5afb203..029ac19 100644 --- a/system/kernel/iommu.zig +++ b/system/kernel/iommu.zig @@ -234,6 +234,17 @@ pub fn reassign(device_id: u64, owner: u32) void { 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 { if (!active) return; for (confined) |*c| { diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 5217221..9059ae2 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -1447,6 +1447,52 @@ fn iommuTest() void { } else { 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}); result(); }