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
+8
View File
@@ -35,6 +35,14 @@ pub const Device = struct {
return self.call(.attach, {}, handle, &reply) != null;
}
/// The reverse of `attach`: the buffer leaves the device's reach. The same
/// region capability rides again (the kernel matches the region). Do not name
/// the buffer's physical address in `read`/`write` after this.
pub fn detach(self: Device, handle: ipc.Handle) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.detach, {}, handle, &reply) != null;
}
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
+9
View File
@@ -136,6 +136,15 @@ pub const Device = struct {
return self.call(.dma_attach, {}, &.{}, handle, &reply) != null;
}
/// The reverse of `attachDma`: unbind the buffer from the controller's IOMMU
/// domain. The same region capability rides again — the kernel matches the
/// region, so neither side kept state between the two calls. Do not name the
/// buffer's physical address in any transfer after this.
pub fn detachDma(self: *Device, handle: ipc.Handle) bool {
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
return self.call(.dma_detach, {}, &.{}, handle, &reply) != null;
}
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
@@ -54,6 +54,12 @@ pub const Protocol = envelope.Define(.{
// physical addresses (named in later read/write) are reachable by the
// device under an enforcing IOMMU. Call once per buffer before using it.
.{ .name = "attach" },
// detach(): the reverse — the same region capability rides the cap slot
// (the caller still holds its handle; the kernel matches the region) and
// the buffer leaves the device's domain. Every grant a live process
// makes is revocable by the granter while alive; death remains the
// mechanical backstop (storage-architecture.md, the lifecycle rule).
.{ .name = "detach" },
},
});
@@ -156,6 +156,11 @@ pub const Protocol = envelope.Define(.{
// target says which caller's device is attaching, so there is nothing
// left for a body to carry.
.{ .name = "dma_attach" },
// dma_detach: the reverse, same shape — the region capability rides the
// cap slot again (the kernel matches the region; the provider retains
// nothing between the two calls) and the buffer leaves the controller's
// domain. Appended, so every existing verb keeps its number.
.{ .name = "dma_detach" },
},
.events = &.{
.{ .name = "interrupt_report", .payload = InterruptReport },
@@ -188,6 +193,7 @@ test "the verb numbering, and the device token in the header" {
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe));
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk));
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach));
try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.dma_detach));
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report));
var buffer: [message_maximum]u8 = undefined;