The block protocol gains define_range (appended, numbers hold): confine the process named by `badge` to blocks [base, base+count). usb-storage keeps a per-badge range table and, in read/write, translates volume-relative LBAs (base added) and refuses any transfer past the volume end. geometry returns the confined size, so a filesystem mounts against what it may actually touch. The security seam (decision 4, settled): the clamp lives at the PROVIDER, so a channel carries exactly the authority it grants — handing a filesystem the whole disk plus a base offset would let it reach the neighbouring partition. The gate: a confined caller may NOT call define_range, so a filesystem cannot widen its own range or confine anyone; only an unconfined party (the volume manager, whole-device) may. The volume manager defines a filesystem's range before handing it the channel, so the ordering holds by construction. Default (no range for a badge) is the whole device — behaviour-neutral for a single-volume boot and what the volume manager itself uses to probe partitions. The range table is declared through bounds.md as a runaway detector (ours, refuse at limit), not a real-partition cap. Neutral: fat-mount, usb-storage, iommu-usb-storage green. The discrimination fixture (a confined process reads past its range and is refused) follows next.
97 lines
4.9 KiB
Zig
97 lines
4.9 KiB
Zig
//! Block-device client: the helper a filesystem uses to read and write a block
|
|
//! device (a USB stick, via usb-storage) without hand-rolling the block-protocol
|
|
//! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like
|
|
//! `runtime.usb` over the transfer protocol.
|
|
//!
|
|
//! Transfers name a caller-owned DMA buffer by physical address (from
|
|
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
|
//! limit — the same handoff usb-storage uses toward the controller.
|
|
|
|
const envelope = @import("envelope");
|
|
const ipc = @import("ipc");
|
|
const block_protocol = @import("block-protocol");
|
|
|
|
const Protocol = block_protocol.Protocol;
|
|
|
|
pub const Geometry = struct { block_size: u32, block_count: u64 };
|
|
|
|
pub const Device = struct {
|
|
endpoint: ipc.Handle,
|
|
|
|
/// The device's block size and total block count.
|
|
pub fn geometry(self: Device) ?Geometry {
|
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
|
const answered = self.call(.geometry, {}, null, &reply) orelse return null;
|
|
const result = Protocol.decodeReply(.geometry, answered) orelse return null;
|
|
return .{ .block_size = result.block_size, .block_count = result.block_count };
|
|
}
|
|
|
|
/// Hand the block server a DMA-region capability (`handle` — from a `shareable`
|
|
/// dma_alloc) so it forwards it to the controller and the buffer's physical
|
|
/// addresses become reachable by the device. Call once per buffer before naming it
|
|
/// in `read`/`write`. Harmless success when no IOMMU is enforcing.
|
|
pub fn attach(self: Device, handle: ipc.Handle) bool {
|
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
|
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;
|
|
return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
|
|
}
|
|
|
|
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
|
|
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
|
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
|
return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
|
|
}
|
|
|
|
/// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
|
|
/// prior writes survive a power-off. A filesystem calls this before the machine
|
|
/// goes down; no data transfer, so the buffer arguments are unused.
|
|
pub fn flush(self: Device) bool {
|
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
|
return self.call(.flush, {}, null, &reply) != null;
|
|
}
|
|
|
|
/// Confine the process `badge` to blocks `[base_lba, base_lba + block_count)`
|
|
/// on this device — the volume manager's per-volume grant to a filesystem.
|
|
/// The caller must itself be unconfined (whole-device); a confined caller is
|
|
/// refused, so a filesystem cannot widen its own range. See `DefineRange`.
|
|
pub fn defineRange(self: Device, badge: u32, base_lba: u64, block_count: u64) bool {
|
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
|
return self.call(.define_range, .{ .badge = badge, .base_lba = base_lba, .block_count = block_count }, null, &reply) != null;
|
|
}
|
|
|
|
/// One request at the driver. `target` is always 0: one endpoint per device, so
|
|
/// there is no object within the peer to address.
|
|
fn call(
|
|
self: Device,
|
|
comptime operation: Protocol.Operation,
|
|
request: Protocol.RequestOf(operation),
|
|
capability: ?ipc.Handle,
|
|
reply: []u8,
|
|
) ?[]u8 {
|
|
var packet: [block_protocol.message_maximum]u8 = undefined;
|
|
const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
|
|
const answer = ipc.callCap(self.endpoint, framed, reply, capability) catch return null;
|
|
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
|
|
if (status.status != 0) return null;
|
|
return reply[0..answer.len];
|
|
}
|
|
};
|
|
|
|
// There is deliberately no open-by-name here: `block` is not a registry name.
|
|
// One storage process serves each volume, and a consumer receives its volume's
|
|
// channel from the device manager (establishment by lineage, communication.md
|
|
// "Establishment: two planes"), then wraps it: `block.Device{ .endpoint = c }`.
|