Port I/O grants (io_read / io_write) + refresh stale driver docs
Ring 3 still has no direct in/out (no TSS I/O bitmap, IOPL never raised — a #GP), but a
driver no longer needs it: io_read(device_id, resource_index, offset, width) and
io_write(..., value) grant port access the same way mmio_map grants memory. The claim
plus the device's discovered io_port resource are the capability — resolveIoPort checks
the device is claimed by the caller, the resource is io_port, and [offset, offset+width)
stays inside it, then issues the in/out via architecture.pioRead/pioWrite. So a PS/2 or
16550 driver is now writable; the low-rate legacy hardware that needs port I/O is fine
with a syscall per access. io_port resources were recorded by discovery and ignored —
now they're used. Runtime: device.ioRead/ioWrite.
New `ioport` test claims QEMU's PS/2 controller (io_port 0x64, discovered via ACPI) and
checks the gate admits an in-range access, refuses over-wide / out-of-range / unclaimed,
and that the kernel actually reads the status port (0x1c). Suite 41/41 plus host tests.
Docs refreshed for the whole M13-M16 + port-I/O reality: drivers.md "Limits" no longer
lists port I/O, DMA memory, or barriers as missing (they exist) and its "what's next"
reflects that; the FSH doc's character-device and block-device sections are corrected
("cannot host a block driver at all" is no longer true — writable now, not yet memory-
safe pending IOMMU enforcement); device-interrupts.md unblocks the keyboard and narrows
the interrupt gap to MSI-X.
This commit is contained in:
@@ -160,6 +160,8 @@ fn system_call(state: *architecture.CpuState) void {
|
||||
.dma_alloc => systemDmaAlloc(state),
|
||||
.dma_free => systemDmaFree(state),
|
||||
.msi_bind => systemMsiBind(state),
|
||||
.io_read => systemIoRead(state),
|
||||
.io_write => systemIoWrite(state),
|
||||
_ => fail(state),
|
||||
}
|
||||
}
|
||||
@@ -271,6 +273,49 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
||||
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
||||
}
|
||||
|
||||
/// Resolve a port-I/O access against the caller's claims. The device must be claimed by
|
||||
/// `t`, `resource_index` must name one of its `io_port` resources, and the access
|
||||
/// `[offset, offset+width)` must fall wholly inside it. Returns the absolute 16-bit
|
||||
/// port, or null if the capability check fails. The claim plus the discovered `io_port`
|
||||
/// resource are the capability — exactly like `mmio_map` for memory, so a driver can
|
||||
/// only touch the ports its device actually owns, never a raw `in`/`out` to anywhere.
|
||||
pub fn resolveIoPort(t: *scheduler.Task, device_id: u64, resource_index: u64, offset: u64, width: u64) ?u16 {
|
||||
if (width != 1 and width != 2 and width != 4) return null;
|
||||
const owner = devices_broker.ownerOf(device_id) orelse return null;
|
||||
if (owner != t.id) return null; // not claimed by this process
|
||||
const r = devices_broker.resourceOf(device_id, resource_index) orelse return null;
|
||||
if (r.kind != @intFromEnum(device_abi.ResourceKind.io_port)) return null;
|
||||
if (offset + width > r.len) return null; // access escapes the claimed port range
|
||||
const port = r.start + offset;
|
||||
if (port + width > 0x1_0000) return null; // I/O ports are 16-bit
|
||||
return @intCast(port);
|
||||
}
|
||||
|
||||
/// io_read(device_id, resource_index, offset, width) -> value: read `width` bytes (1/2/4)
|
||||
/// from a port in a claimed device's `io_port` resource. Ring 3 has no direct `in`/`out`
|
||||
/// (no TSS I/O bitmap, IOPL never raised), so a legacy driver (PS/2, 16550 UART) reaches
|
||||
/// its ports through this claim-gated call — low-rate hardware, so a syscall per access
|
||||
/// is fine. See docs/drivers.md.
|
||||
fn systemIoRead(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const width = architecture.systemCallArg(state, 3);
|
||||
const port = resolveIoPort(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), width) orelse return fail(state);
|
||||
architecture.setSystemCallResult(state, architecture.pioRead(@intCast(width), port));
|
||||
}
|
||||
|
||||
/// io_write(device_id, resource_index, offset, width, value) -> 0: write `value` (low
|
||||
/// `width` bytes) to a port in a claimed device's `io_port` resource. Same capability
|
||||
/// gate as `io_read`.
|
||||
fn systemIoWrite(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const width = architecture.systemCallArg(state, 3);
|
||||
const port = resolveIoPort(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), width) orelse return fail(state);
|
||||
architecture.pioWrite(@intCast(width), port, @intCast(architecture.systemCallArg(state, 4)));
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): grant `len` bytes (rounded up to
|
||||
/// whole pages) of DMA-capable memory — physically contiguous, zeroed, pinned, and
|
||||
/// strong-uncacheable (coherent) — mapping it into the caller's DMA arena and handing
|
||||
|
||||
@@ -90,6 +90,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
msiTest();
|
||||
} else if (eql(case, "iommu")) {
|
||||
iommuTest();
|
||||
} else if (eql(case, "ioport")) {
|
||||
ioPortTest();
|
||||
} else if (eql(case, "smp")) {
|
||||
smpTest();
|
||||
} else if (eql(case, "affinity")) {
|
||||
@@ -1051,6 +1053,53 @@ fn iommuTest() void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// Port I/O grants: ring 3 has no `in`/`out`, so a legacy driver reaches its ports
|
||||
/// through `io_read`/`io_write`, gated by `device_claim` and the device's `io_port`
|
||||
/// resource exactly like `mmio_map` gates memory. Target the PS/2 controller's status
|
||||
/// port (0x64) — discovered on every PC and side-effect-free to read. Proves the
|
||||
/// capability gate (`resolveIoPort` admits an in-range access, refuses out-of-range,
|
||||
/// over-wide, and unclaimed) and that the kernel actually performs the `in`. The
|
||||
/// `io_read`/`io_write` syscalls wrap this with the same ring-3 dispatch every device
|
||||
/// driver already uses.
|
||||
fn ioPortTest() void {
|
||||
log("DANOS-TEST-BEGIN: ioport\n", .{});
|
||||
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||
|
||||
var found_id: ?u64 = null;
|
||||
var found_res: u64 = 0;
|
||||
outer: for (buffer[0..n]) |d| {
|
||||
for (0..d.resource_count) |ri| {
|
||||
const r = d.resources[ri];
|
||||
if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0x64 and r.len >= 1) {
|
||||
found_id = d.id;
|
||||
found_res = ri;
|
||||
break :outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
const id = found_id orelse {
|
||||
check("discovered the PS/2 status port (io_port 0x64)", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
check("discovered the PS/2 status port (io_port 0x64)", true);
|
||||
|
||||
const me = scheduler.current();
|
||||
check("claimed the io_port device", devices_broker.claim(id, me.id));
|
||||
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0, 1) == 0x64);
|
||||
check("an over-wide access is refused", process.resolveIoPort(me, id, found_res, 0, 2) == null);
|
||||
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 1, 1) == null);
|
||||
check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0, 1) == null);
|
||||
|
||||
// The kernel actually issues the `in`. Reaching this line at all proves it didn't
|
||||
// fault; a width-1 read must return a single byte.
|
||||
const status = architecture.pioRead(1, 0x64);
|
||||
check("reading the PS/2 status port returned a byte", status <= 0xFF);
|
||||
log("DANOS-IOPORT: PS/2 status = 0x{x}\n", .{status});
|
||||
result();
|
||||
}
|
||||
|
||||
var proc_worker_run: bool = true;
|
||||
var proc_worker_ran: bool = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user