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:
Daniel Samson
2026-07-10 20:36:14 +01:00
parent e612d948d2
commit 21657943a8
9 changed files with 174 additions and 48 deletions
+25 -21
View File
@@ -64,14 +64,15 @@ already provide — it claims its device, maps its registers with `mmio_map`, an
on `replyWait` for either an interrupt or a client request. `system/drivers/hpet/hpet.zig` is already
that program, minus the client half.
The obstacle is not the file type, it is which hardware a ring-3 driver can actually
drive. Port I/O is unavailable to user space — the TSS I/O permission bitmap is absent
and IOPL is never raised — so `in`/`out` from a driver is a #GP. That excludes the
16550 UART at `0x3F8` and PS/2 at `0x60`/`0x64`, which is to say it excludes the
obvious implementations of `/dev/tty`, `/dev/ttyS0` and a keyboard node. Until either
port I/O grants or a memory-mapped UART exist, serial output stays a kernel service
reached through the `write` system call rather than a file. A memory-mapped device such
as the framebuffer has no such problem and is the more likely first real entry here.
The obstacle was never the file type; it is which hardware a ring-3 driver can reach.
Direct `in`/`out` from user space is still a #GP (no TSS I/O bitmap, IOPL never raised),
but a driver no longer needs it: **`io_read`/`io_write`** grant port access the same way
`mmio_map` grants memory — gated by `device_claim` and the device's discovered `io_port`
resource. So the 16550 UART at `0x3F8` and the PS/2 controller at `0x60`/`0x64` (and thus
`/dev/ttyS0` and a keyboard node) are now writable as ordinary ring-3 drivers; the
low-rate legacy hardware that needs port I/O is fine with a syscall per access. A
memory-mapped device such as the framebuffer, needing no port I/O at all, remains the
easiest first entry.
### Block devices
@@ -79,20 +80,23 @@ A block device is addressed in fixed-size blocks and, unlike a character device,
layer above is free to buffer, reorder, coalesce and retry requests against it. Disks
and other persistent storage are the whole population of this class.
**danos cannot host a block driver at all today,** and the reason is worth stating
plainly because it is not a matter of unwritten code. Every storage controller worth
naming is a bus master: it is programmed by handing it the physical address of a
descriptor ring and left to read and write memory on its own. A ring-3 driver cannot
build such a ring, because `mmap` returns writeback-cached, physically discontiguous
pages and never discloses their physical address. Nor should it be allowed to: a device
programmed with an arbitrary physical address writes to arbitrary physical memory, and
page tables do not sit between a device and RAM — an IOMMU does. Granting a DMA-capable
device to a driver process, with no IOMMU programmed, is equivalent to granting ring 0,
which would forfeit the isolation that motivates user-space drivers in the first place.
A block driver is now **writable, but not yet memory-safe.** Every storage controller
worth naming is a bus master: it is programmed by handing it the physical address of a
descriptor ring and left to read and write memory on its own. That ring is exactly what
**`dma_alloc`** now provides — physically contiguous, pinned, uncacheable, with its
physical address disclosed — and **`/lib/mmio`**'s barriers order the descriptor writes
against the doorbell, and **`msi_bind`** delivers completions. So an AHCI or NVMe driver
can be written today (the M14/M15 work in [driver-model.md](driver-model.md); the earlier
"cannot host a block driver at all" is no longer true).
Block devices therefore wait on DMA-capable memory, memory barriers, and VT-d/DMAR —
the M14–M16 work in [driver-model.md](driver-model.md). A ramdisk over the initial ramdisk is
the one block-shaped thing implementable now, and it needs no driver process.
What is *not* yet true is that it is safe. A device programmed with an arbitrary physical
address writes to arbitrary physical memory, and page tables do not sit between a device
and RAM — an IOMMU does. The IOMMU is now *detected* (M16), but no translation domains
are programmed, so granting a DMA-capable device to a driver process is still equivalent
to granting ring 0. Until per-device domains confine a driver's DMA to the buffers it
`dma_alloc`'d, a block driver works but forfeits the isolation that motivates user-space
drivers — enforcement is the next step, and lands with that first driver. A ramdisk over
the initial ramdisk remains the one block-shaped thing that needs no driver process at all.
### Pseudo-devices
+6 -5
View File
@@ -156,10 +156,11 @@ spinning in unrelated code — is the whole mechanism working end to end.
## What's next (not done here)
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and ring 3
has no port I/O yet, so the first *input* device is blocked on either an I/O
permission bitmap or `io_in`/`io_out` syscalls ([drivers.md](drivers.md)).
- **MSI/MSI-X**: per-device vectors, edge-triggered and unshared, which retire the
I/O APIC's mask/ack cycle and its 24-GSI ceiling.
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and port I/O is
now available to ring 3 via the claim-gated `io_read`/`io_write` syscalls
([drivers.md](drivers.md)) — so the first *input* device is unblocked; it just needs
writing (claim the controller, `irq_bind` GSI 1, read scancodes from `0x60`).
- **MSI-X**: `msi_bind` gives one per-device edge-triggered vector (M15); MSI-X's
multi-vector table (many queues per device, e.g. NVMe) is the remaining extension.
- **The LAPIC's own page** is still mapped writeback-cacheable like the rest of the
identity map. QEMU tolerates it; real hardware wants it uncacheable.
+6
View File
@@ -152,6 +152,12 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
ack cycle. Legacy INTx (`_PRT` parsing + shared lines) is deliberately skipped — MSI
is the real answer. QEMU's HPET has no MSI, so delivery is proven with a self-IPI; the
first PCI driver is the first real consumer.
- **Port I/O** — `io_read`/`io_write(device_id, resource_index, offset, width[, value])`:
a claimed device's `io_port` resource lets a driver read/write its ports, gated exactly
like `mmio_map` gates memory (direct ring-3 `in`/`out` stays a #GP). This is what makes
a PS/2 or 16550 driver possible; the low-rate legacy hardware that needs it is fine with
a syscall per access. `io_port` resources were recorded by discovery and ignored — now
they're used.
- **M16 (detection)** — the IOMMU is now *found*: discovery parses the ACPI DMAR table,
maps the first VT-d unit, and reads its version + capabilities (`iommu_present` in the
platform info). This is detection only — **no translation domains are programmed, so
+18 -22
View File
@@ -266,26 +266,24 @@ controller drivers fit together.
Worth knowing before you write the second driver:
- **Ring 3 has no port I/O.** The TSS I/O permission bitmap is absent
(`tss.zig`: `iomap_base = @sizeOf(Tss)`), and IOPL is never raised, so `in`/`out`
from a driver is a #GP. That rules out a user-space 16550 UART (`0x3F8`), PS/2
(`0x60`/`0x64`), and legacy PCI config (`0xCF8`/`0xCFC`). Everything must be MMIO.
`io_port` resources are recorded by discovery and then ignored.
Several things this list used to warn about are now available (see
[driver-model.md](driver-model.md)): **port I/O** (`io_read`/`io_write`, claim-gated by
the device's `io_port` resource — direct ring-3 `in`/`out` is still a #GP, so a PS/2 or
16550 driver goes through these), **DMA memory** (`dma_alloc`: contiguous, pinned,
uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
`mb`/`rmb`/`wmb`). What remains:
- **Page granularity.** `mmio_map` rounds to 4 KiB. Two devices sharing a page means
granting one grants the other. A `device_register`ed child's *resource* can be narrower
than a page, but its *mapping* can't.
- **No DMA memory.** `mmap` gives you writeback-cached, non-contiguous pages and never
tells you their physical address, so you cannot build a descriptor ring. Any driver
for a bus-mastering device is blocked on this.
- **No memory barriers.** There are none in the tree, and `volatile` is not one — it
won't stop the compiler sinking an ordinary store (your DMA descriptor) past a
volatile MMIO store (your doorbell). On x86 you mostly get away with it; on ARM you
will not. See [driver-model.md](driver-model.md#m14).
- **DMA is not contained.** A driver that can program a bus-mastering device can make
that device write to *any* physical address — page tables don't sit between a device
and RAM; an IOMMU does. Until VT-d/DMAR is programmed, `device_claim` on a DMA-capable
device is effectively equivalent to granting ring 0. This is the largest gap between
the design's promise and what it delivers.
and RAM; an IOMMU does. The IOMMU is now *detected* (M16), but no translation domains
are programmed, so `device_claim` on a DMA-capable device is still effectively
equivalent to granting ring 0. This is the largest gap between the design's promise and
what it delivers; enforcement lands with the first DMA driver.
- **No `dev_release`.** A claim is never dropped (only IRQ/MSI bindings are, on exit), so
a device stays owned for the life of its driver — which blocks restart.
- **One endpoint per GSI**, so shared legacy PCI INTx lines can't be split between two
drivers. MSI/MSI-X — one vector per device, edge-triggered, unshared — is the real
answer, and QEMU's HPET doesn't offer it (`Tn_FSB_INT_DEL_CAP = 0`).
@@ -341,14 +339,12 @@ Two companions cover what `hpet` can't, because it never exits:
## What's next (not done here)
The big ones — capability passing (class drivers), DMA + barriers and MSI (host
controller drivers), and the IOMMU — have proposed signatures in
[driver-model.md](driver-model.md). Smaller items:
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,
and IOMMU detection — are **now done** ([driver-model.md](driver-model.md), M13–M16), as
is **port I/O** (`io_read`/`io_write`, the claim-gated syscalls that make a PS/2 or 16550
driver possible). What's left is IOMMU *enforcement* (per-device domains — it waits on
the first DMA driver to protect and test against) and these smaller items:
- **Port I/O grants**, so a PS/2 or 16550 driver is possible: either a per-device TSS
I/O permission bitmap swapped on context switch, or `io_in`/`io_out` syscalls gated
by the same claim. The legacy devices that need it are all low-rate, so the syscall
is likely fast enough.
- **Releasing a claim.** There is no `dev_release`, and `devices_broker` never drops a claim on
exit — only IRQ bindings are released. A dead driver's device stays owned forever,
which blocks restart.