kernel: the give paths take the lock, and a give confines afresh after a death

Three holes from the real-AMD audit, one shared root: the delegation flag-day
added paths that touch the broker table and the IOMMU records without the big
kernel lock, and a loan-return rule whose re-delegation skipped confinement.

- device_enumerate walked the table with no lock. The table stopped being a
  static array in the bounds track — reserve() regrows it through realloc on
  every boot — so an unlocked reader can be mid-copy out of a slice that
  device_register on another core has already freed and reused, or pair a fresh
  count with a stale slice. Each chunk is now snapshotted under the lock; the
  copy to the user stays outside it.

- system_spawn's give ran entirely unlocked — the comment claiming "the lock
  has not been dropped" was false (spawnProcessSupervised takes and releases it
  internally). The ownership pre-check now only spares creating a doomed child;
  the give itself re-checks, confines and moves in one lock hold, and a give
  that fails after the spawn kills the child rather than leaving it running
  without the hardware it was spawned for.

- A re-delegated device after a driver death was never re-confined. Death tears
  the domain down before the loan returns to the lender, so the next give found
  no active record, reassign no-op'd, and the respawned driver ran the device
  with a V=0 device-table entry and no domain — silently unconfined, the exact
  fail-open the fail-closed claim was built to remove. Both give paths now share
  one body (giveDeviceLocked): check first, confine afresh when no record is
  active — refusing with ECONFINE like the claim — and move last, when nothing
  can fail.

The iommu test drives the death-and-respawn sequence directly; with the old
reassign-only behaviour its two confinement checks fail, with this change the
suite is 118/118.
This commit is contained in:
Daniel Samson
2026-08-09 09:57:55 +01:00
parent 72807c20e4
commit 35f43057f4
3 changed files with 112 additions and 25 deletions
+27
View File
@@ -1491,6 +1491,33 @@ fn iommuTest() void {
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);
// The restart hole. A driver's death returns its device to the lender with the
// domain torn down (asserted just above) — so the NEXT delegation of the same
// device finds no active confinement record, and the transfer path's bare
// `reassign` no-ops: the respawned driver would run the device with a V=0
// device-table entry and no domain, silently unconfined. The give path must
// confine afresh in that case, exactly as a first claim would.
check("the lender holds the returned device", claimOk(device_id, me));
const driver: u32 = me + 2000;
check("delegating it confines it to the receiver", process.giveDeviceLocked(device_id, me, driver) == 0);
check("the give's confinement names the receiver", iommu.confinementOwner(device_id) == driver);
// The receiver dies: confinement torn down first, then the broker loans the
// device back to the lender — the same order releaseTaskResourcesLocked runs.
iommu.releaseAllOwnedBy(driver);
devices_broker.releaseAllOwnedBy(driver);
check("death returns the loan to the lender", devices_broker.ownerOf(device_id) == me);
check("and leaves the device unconfined", iommu.confinementOwner(device_id) == null);
// The regression this guards: re-delegation after that death.
const respawned: u32 = me + 3000;
check("re-delegation after the death succeeds", process.giveDeviceLocked(device_id, me, respawned) == 0);
check(
"and the respawned driver's device is confined, not silently naked",
iommu.confinementOwner(device_id) == respawned,
);
iommu.releaseAllOwnedBy(respawned);
devices_broker.releaseAllOwnedBy(respawned);
_ = devices_broker.unclaim(device_id, me);
}
log("DANOS-IOMMU: enabled base=0x{x} domains active\n", .{pinfo.iommu_base});