Files
danos/library/protocol/usb-transfer/usb-transfer-protocol.zig
T
Daniel Samson 4e7cbc9792 iommu: DMA-region capabilities — per-grant reachability, protocol flag-day
Replaces L2's interim DMA pool (every buffer reachable by every claimed
device) with true per-grant confinement: a device reaches only buffers
whose capability was delegated to its driver.

Kernel:
- DmaRegionObject (handle kind 2): a delegation token naming a dma_alloc'd
  region, passable across processes on the IPC cap slot like an endpoint
  or shared-memory object. Frames stay owned by the allocating address
  space (freed on dma_free/teardown as before); the token carries a `dead`
  flag so a stale downstream handle can no longer bind a freed region.
- dma_alloc gains the dma_shareable flag: it returns a capability handle
  in r8 and every region is tracked in a registry. A task's own regions
  auto-bind into the devices it claims (its rings just work); foreign
  buffers are bound explicitly.
- dma_bind / dma_unbind / handle_close syscalls (51-53). dma_bind maps a
  held region (or shared-memory) capability into a claimed device's domain;
  it is idempotent. handle_close reclaims a table slot (raised 16 -> 32).
- dma_free and task death unmap a region from every domain and invalidate
  BEFORE its frames return to the allocator — the stale-IOTLB use-after-
  free window, closed structurally.

Protocols (flag-day): block gains attach, usb-transfer gains dma_attach —
each carries a region capability on the cap slot. fat allocates its bounce
buffer shareable and attaches it; usb-storage allocates its transport
buffers shareable, attaches them to the controller, and forwards fat's
capability downstream; usb-xhci-bus binds and closes; virtio-gpu binds its
shared scanout surface. The physical addresses on the wire are unchanged
(identity IOVA), so no register-programming code moved.

Cross-process DMA (fat -> usb-storage -> xHC) now flows only through
delegated capabilities. iommu-usb-storage / iommu-usb-hid / iommu-fault
all green under per-grant enforcement; 104/104 overall (fail-open paths
unchanged).
2026-07-26 18:31:27 +01:00

178 lines
7.1 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
//! mass-storage driver) says to the xHCI bus driver over its well-known
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
//!
//! The shape:
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
//! its own endpoint (for asynchronous interrupt reports) and its assigned
//! device id, and receives a `device_token` plus its interface's endpoints.
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
//! it completes. Control data travels inline (descriptors, HID/MSC class
//! requests are all small); bulk data travels by **physical address** — the
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
//! to cross the 256-byte IPC boundary.
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
//! interrupt endpoint; each report the device produces is then pushed to the
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
//! exactly how the input service delivers events.
//!
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
//! A multi-controller machine would need a per-controller endpoint (the device
//! manager handing each class driver the right one); noted, not built.
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
pub const message_maximum: usize = 256;
/// The largest inline control-transfer payload. Sized so a whole message
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
/// requests are all far smaller.
pub const max_inline_data: usize = 200;
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
/// 8 bytes, boot mouse reports 3–4.
pub const max_report_data: usize = 48;
/// Endpoints per interface reported back in an open reply (a boot HID interface
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
pub const max_reported_endpoints: usize = 4;
pub const Operation = enum(u32) {
open = 0,
control = 1,
interrupt_subscribe = 2,
bulk = 3,
/// dma_attach: a class driver hands the controller a DMA-region capability (riding
/// the call's cap slot) so the controller binds that buffer into its IOMMU domain
/// and may then DMA to the physical addresses inside it. Needed once per buffer the
/// class driver will name in a `bulk` transfer (its own, or one forwarded to it).
dma_attach = 4,
};
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
/// the bus driver already parsed during enumeration.
pub const Endpoint = extern struct {
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
address: u8,
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
transfer_type: u8,
max_packet_size: u16,
interval: u8,
reserved: [3]u8 = .{ 0, 0, 0 },
};
/// open: the class driver's receive endpoint rides as the call's capability, and
/// `device_id` is the interface's assigned id (its argv[1]).
pub const OpenRequest = extern struct {
operation: u32 = @intFromEnum(Operation.open),
reserved: u32 = 0,
device_id: u64,
};
/// The answer to open: a token scoping every later request to this device, the
/// interface's class triple (a sanity check), and its endpoints.
pub const OpenReply = extern struct {
status: i32,
endpoint_count: u32,
device_token: u64,
interface_class: u8,
interface_subclass: u8,
interface_protocol: u8,
interface_number: u8,
reserved2: u32 = 0,
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
};
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
/// reply carries up to `data_length` bytes back.
pub const ControlRequest = extern struct {
operation: u32 = @intFromEnum(Operation.control),
reserved: u32 = 0,
device_token: u64,
setup: [8]u8,
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
reserved2: u8 = 0,
data_length: u16,
reserved3: u32 = 0,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
pub const ControlReply = extern struct {
status: i32, // 0 success, negative on failure/stall
actual_length: u32,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
/// report the device returns is pushed to the caller's endpoint (handed over at
/// open) as an asynchronous `InterruptReport`.
pub const InterruptSubscribeRequest = extern struct {
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
reserved: u32 = 0,
device_token: u64,
endpoint_address: u8,
reserved2: u8 = 0,
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
};
pub const InterruptSubscribeReply = extern struct {
status: i32,
reserved: u32 = 0,
};
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
pub const BulkRequest = extern struct {
operation: u32 = @intFromEnum(Operation.bulk),
reserved: u32 = 0,
device_token: u64,
physical_address: u64,
length: u32,
endpoint_address: u8,
reserved2: u8 = 0,
reserved3: u16 = 0,
};
pub const BulkReply = extern struct {
status: i32,
actual_length: u32,
};
/// dma_attach: the region capability rides the call's cap slot; the body only carries
/// the device token (scoping) so the controller knows which caller is attaching.
pub const DmaAttachRequest = extern struct {
operation: u32 = @intFromEnum(Operation.dma_attach),
reserved: u32 = 0,
device_token: u64,
};
pub const DmaAttachReply = extern struct {
status: i32,
reserved: u32 = 0,
};
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
pub const InterruptReport = extern struct {
device_token: u64,
endpoint_address: u8,
length: u8,
reserved: u16 = 0,
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
};
comptime {
const std = @import("std");
// Every synchronous message must fit one IPC message; the async report must
// fit an ipc_send payload slot.
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
std.debug.assert(@sizeOf(InterruptReport) <= 64);
}