protocols: attach gets its reverse — block detach, usb-transfer dma_detach

The kernel was always symmetric (dma_bind 51 / dma_unbind 52); the two
protocols that forward an attachment up the stack were one-way, so a live
client could grant a device reach into its buffer but never revoke it while
alive — exactly the one-way lifecycle the storage architecture's enforcement
section forbids. Death stays the mechanical backstop; detach is the living
process's path.

Both verbs are appended, so every existing number holds. The shape mirrors
attach precisely: the same region capability rides the cap slot again — the
kernel matches the region, so no layer retains anything between the calls
(the bus never kept the handle; now it never needs to).

fat's bring-up does attach -> detach -> attach, exercising both verbs
through the whole chain (fat -> storage -> bus -> kernel) on every boot: a
broken detach fails every fat case instead of lying dormant until the first
buffer replacement. Honest scope: the round trip proves the plumbing; unbind
semantics are the kernel iommu tests' (map/unmap/translationOf); the full
composition (detach then DMA faults) is a future iommu-fault extension.
This commit is contained in:
Daniel Samson
2026-08-09 15:18:21 +01:00
parent fa54ef6915
commit a44b397bed
7 changed files with 58 additions and 0 deletions
@@ -204,12 +204,20 @@ fn onAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
return if (device.attachDma(handle)) 0 else refused;
}
/// The reverse: forward the same region capability so the controller unbinds
/// the buffer. As with attach, our copy stays the turn's to close.
fn onDetach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
const handle = invocation.capability orelse return -envelope.EPROTO;
return if (device.detachDma(handle)) 0 else refused;
}
const handlers = Serve.Handlers{
.geometry = onGeometry,
.read = onRead,
.write = onWrite,
.flush = onFlush,
.attach = onAttach,
.detach = onDetach,
};
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
@@ -593,6 +593,7 @@ const handlers = Serve.Handlers{
.interrupt_subscribe = onInterruptSubscribe,
.bulk = onBulk,
.dma_attach = onDmaAttach,
.dma_detach = onDmaDetach,
};
/// open: the target is the class driver's assigned device id. Resolve it to an
@@ -694,6 +695,14 @@ fn onDmaAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
return if (device.dmaBind(controller_id, handle)) 0 else refused;
}
/// The reverse: the same region capability arrives again and the buffer leaves
/// the controller's domain (`dma_unbind` matches the region — nothing was
/// retained here between the two calls). The turn closes the arriving copy.
fn onDmaDetach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
const handle = invocation.capability orelse return -envelope.EPROTO;
return if (device.dmaUnbind(controller_id, handle)) 0 else refused;
}
/// A timer tick or an MSI landed: drain the event ring, reconcile ports, and fan out.
/// The timer arm re-arms itself (8 ms drain when polling, 250 ms reconcile under MSI);
/// the MSI arm clears the interrupter's pending bit FIRST, then drains — so an event
+12
View File
@@ -196,10 +196,22 @@ fn tryBringUp() void {
// enforcing IOMMU. No-op binding otherwise.
const bounce = memory.dmaAlloc(engine.max_transfer_sectors * 512, memory.dma_coherent | memory.dma_shareable) orelse return;
if (bounce.handle) |handle| {
// Attach, detach, and attach again: the round trip exercises BOTH verbs
// of the DMA-window lifecycle through the whole chain (fat → storage →
// bus → kernel) on every boot, so a broken detach fails every fat case
// rather than lying dormant until the first buffer replacement.
if (!device.attach(handle)) {
_ = logging.write("/system/services/fat: could not attach the DMA bounce buffer\n");
return;
}
if (!device.detach(handle)) {
_ = logging.write("/system/services/fat: could not detach the DMA bounce buffer\n");
return;
}
if (!device.attach(handle)) {
_ = logging.write("/system/services/fat: could not re-attach the DMA bounce buffer\n");
return;
}
_ = ipc.close(handle); // the binding holds its own reference now
}
ipc_block = .{ .device = device, .bounce = bounce };