The Block Device Layer

July 16, 2026 · View on GitHub

Under any on-disk filesystem is a block device, and NØNOS presents one uniform block interface over three real backends: NVMe, AHCI, and virtio-blk. The kernel side is a thin dispatcher; the drivers themselves are signed capsules reached over IPC. This page documents the backend selection and the block operations. The code is under src/hardware/block_device/.

The backend

A Backend (backend.rs:18) names the three supported controllers, and selected (select.rs:23) picks one at first use by probing for capacity in a fixed order:

  selected():   // memoized in a Once
      if nvme_capsule::capacity() > 0:  Nvme
      if ahci_capsule::capacity() > 0:  Ahci
      else:                             VirtioBlk

NVMe is preferred, then AHCI, then virtio-blk as the fallback that a QEMU guest usually presents. The choice is made once and cached, so every later operation goes to the same backend. Each of the three is a real driver, not a stub: NVMe, AHCI, and virtio-blk are all implemented as capsules under src/hardware/, spawned at boot.

Block operations

The block interface is read, write, flush, capacity, and geometry, and each dispatches to the selected backend's capsule client. read (read.rs:24) is representative:

  read(lba, out):
      match selected():
          VirtioBlk => virtio_blk_capsule::read_blocks(lba, out)
          Ahci      => ahci_capsule::read_blocks(lba, out)
          Nvme      => nvme_capsule::read_blocks(lba, out)

The call reaches the driver capsule, which owns the controller through the hardware broker and performs the actual device I/O, then copies the sectors back. This is real hardware access, not an in-memory shim: the block layer is a dispatcher that forwards a logical block address and a buffer to whichever driver capsule owns the disk. The geometry is 512-byte sectors (geometry.rs), and the errors from each backend are normalized to one BlockDeviceError through the per-backend map_*_error functions so a caller sees one error type.

Where drivers live

The driver capsules are the same kind of ring-3 capsule as everything else: they claim their controller through the broker, map its registers and set up DMA rings for the queues, and serve block reads and writes over IPC. The block layer documented here is the kernel-side seam that a filesystem calls; the driver-side is a capsule. This keeps the disk driver out of the kernel proper, so a bug in an NVMe or AHCI driver is contained in a capsule rather than being a kernel fault.

Security analysis

A block driver is the most authority-heavy capsule in the storage stack, because unlike a HID report parser it does reach hardware directly. What contains it is that the authority is scoped to one device and minted through the broker, and the block layer above it never runs driver code in the kernel.

The driver holds only its own device's grants. Each driver capsule is spawned with a fixed capability mask (src/hardware/nvme_capsule/spawn.rs:51, ahci_capsule/spawn.rs, virtio_blk_capsule/spawn.rs): IPC, Memory, Driver, DeviceEnum, Mmio, Irq, and Dma, which decodes to 0xF8018 for NVMe and AHCI, and 0x1F8018 for virtio-blk because it additionally holds Pio for the legacy virtio I/O ports (bits from src/capabilities/types.rs:54). Those capabilities let it claim a device and mint MMIO, IRQ, and DMA grants, but the broker checks every grant against the claim epoch, so a driver can only map the BAR, bind the IRQ, and program the DMA of the one device it claimed. It cannot reach across to another controller's registers, and a second capsule that tried to claim an already-claimed device is refused. The NVMe setup shows the scoping directly: find_nvme (discover.rs:32) enumerates only CLASS_BLOCK devices, claim (setup/claim.rs) takes the epoch, and mmio::map (setup/mmio.rs) maps NVME_BAR_INDEX under that epoch and rolls the claim back if the map fails.

The DMA buffers are broker-zeroed and ceiling-bounded. The queues and data buffers a driver programs into the controller are mk_dma_map grants (dma/region.rs:28 for NVMe, setup/dma.rs for virtio-blk), which the broker allocates, scrubs, and hands back with a device-visible physical address. A block driver cannot request an unbounded region: the BLOCK class ceiling is 1024 pages (src/hardware/broker/dma/limits.rs:40), so an NVMe submission and completion queue pair (admin/queue/types.rs:20, the sq and cq DMA regions) plus its data buffers fit inside the ceiling and a runaway request is refused with BadLengthForClass before any RAM is allocated. Each DmaRegion unmaps itself on drop (dma/region.rs:46), so the frames return to the allocator when the driver exits or the region goes out of scope.

The block layer above the drivers runs no driver code. src/hardware/block_device/ is a dispatcher, not a driver: selected (select.rs:23) picks a backend once by probing capacity, and read / write / flush forward a logical block address and a buffer to the capsule client (read.rs:24). A filesystem calling read_all never executes NVMe or AHCI register code in the kernel; it sends an IPC request to the capsule that owns the disk. So a parsing or queue bug in a disk driver is a fault in a ring-3 capsule the kernel can tear down, not a kernel fault.

The honest boundary is the same IOMMU gap as every DMA grant. The device_addr the broker returns to a block driver is a raw physical address, because the IOMMU backend is behind the nonos-arch-iommu feature and is not engaged in shipping builds (see the DMA grants page). The broker bounds what the driver capsule may allocate and program, but it does not bound what the controller does once the driver has handed it a descriptor: a compromised or buggy storage controller can in principle DMA to any physical address regardless of the grant. Block-driver DMA safety therefore rests on the per-device grant scoping and the zero-scrub plus the assumption of non-malicious controller hardware, and enabling the IOMMU backend is the path to closing that last gap.

Debugging the block layer

A disk that never appears has one of a small number of causes, and because the driver is a capsule reached over the broker and IPC, each cause shows up as a different line rather than a silent hang. On a machine with a serial port the boot log carries it; a NONOS_FBCONSOLE=1 build mirrors the same log to the framebuffer (src/sys/boot_log/).

Did the driver capsule spawn. Each driver is spawned through capsule_boot::boot (src/userspace/init/capsule_boot/run.rs:29), which prints capsule spawned on success and boot_log::error(...) with the spawn reason on failure, tagged with the driver's prefix ([DRIVER-NVME], [DRIVER-AHCI], [DRIVER-VIRTIO-BLK] from spawn_plan/drivers_storage.rs and drivers_virtio_io.rs). An absent [DRIVER-NVME] capsule spawned line means the capsule's ELF failed signature verification or its manifest asked for a capability outside policy, and no driver ran.

Did the driver find its device, or fail a grant. A spawned driver that still serves no capacity exited during setup, and the exit code names the stage. For NVMe those codes are explicit (error/types.rs:30): DeviceNotFound (30) means mk_device_list(CLASS_BLOCK, ...) (discover.rs:34) returned nothing, so the firmware never enumerated an NVMe controller into the broker table (src/hardware/broker/table/list.rs); ClaimFailed (31) means another capsule already holds the device; and BrokerCallFailed (32) means an MMIO or DMA grant was refused. A DMA grant refused over the class ceiling narrates itself on the broker side as [DMA] validate bad-length-class (BadLengthForClass, dma/map/mod.rs:76), and a grant against a lapsed claim as [DMA] validate stale-epoch, using the same marker vocabulary as the DMA grants page. So a driver that dies at DMA setup tells you whether it over-requested or raced a claim release.

Is the IRQ the problem, and does it matter. For NVMe the answer is that it usually does not: irq::bind (setup/irq.rs) treats the MSI-X bind as best effort and, on a failed bind, continues in polling mode with a zero grant, because the command path polls every completion rather than waiting on the interrupt. So an NVMe controller that comes up but delivers no interrupt still serves reads. AHCI and virtio-blk take their own IRQ grants under the claim epoch, and a silent-but-bound interrupt on those is the same GSI-to-vector routing question as any other device (the IOAPIC).

Driver-spawn failure versus device-enumeration failure. These are the two failures that look alike from the desktop, and the log tells them apart. A missing [DRIVER-*] capsule spawned line is a spawn-side failure: verification or capability policy stopped the capsule before it ran, and no amount of correct hardware helps. A spawned driver that exits with DeviceNotFound is an enumeration-side failure: the capsule ran but the broker device table held no matching controller, which is a firmware/PCI problem rather than a driver bug, because find_nvme filters on pci_class == PCI_CLASS_STORAGE, pci_subclass == PCI_SUBCLASS_NVM, and pci_progif == PCI_PROGIF_NVME (discover.rs:49) and none matched. The first is fixed in the capsule or its signature; the second is fixed in what the firmware exposes.

Source map

  src/hardware/block_device/backend.rs   the Backend enum
  src/hardware/block_device/select.rs    the NVMe -> AHCI -> virtio probe
  src/hardware/block_device/read.rs, write.rs, flush.rs   the dispatched operations
  src/hardware/block_device/geometry.rs, capacity.rs      512-byte geometry and size
  src/hardware/nvme_capsule/, ahci_capsule/, virtio_blk_capsule/   the kernel-side embeds and spawns
  userland/capsule_driver_nvme/          the NVMe driver: discover.rs, setup/ (claim, mmio, irq),
                                         dma/region.rs, admin/queue/ (sq/cq), nvm/, error/types.rs
  userland/capsule_driver_ahci/          the AHCI driver: setup/, controller/scan_ports.rs, server/
  userland/capsule_driver_virtio_blk/    the virtio-blk driver: setup/dma.rs, queue/, io/, server/
  src/hardware/broker/dma/limits.rs      the BLOCK class DMA ceiling (1024 pages)
  src/hardware/broker/table/list.rs      list_by_class, the broker device table drivers enumerate
  src/capabilities/types.rs              the capability bits the driver masks decode to
  src/userspace/init/spawn_plan/         drivers_storage.rs, drivers_virtio_io.rs (the spawn markers)

Every reference above is verified against those trees. The grant syscalls the drivers rest on are specified on the hardware broker pages, the DMA ceiling and its markers on the DMA grants page, and the filesystems that sit above the block layer on the VFS routing page.