Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

NVMe Userspace Provider: Conditional Model B Doorbell/Notify DMA Validator

Operator Decision (2026-05-27)

The userspace NVMe-class storage provider (docs/proposals/cloud-driver-foundation-gap-analysis.md, NVMe child chain) selected Model B: provider-writes-everything, kernel-validates-on-notify for the direct-remapping userspace-driver lane. This was intended to override the kernel-mints-the-address model (Model A) that the gap analysis originally recommended for the storage chain and that the landed virtio-net TX provider uses.

The operator’s stated reason: capOS wants the genuine userspace-driver model, where the driver process — not the kernel — owns and writes the device-visible addresses it programs into the controller. Model A keeps device-address minting inside the kernel, which is safe but is not a real userspace driver: the provider only places a value the kernel already chose. Model B makes the provider a first-class driver and moves the kernel from address-author to address-validator.

Correction recorded later on 2026-05-27: Model B cannot be used on the current no-IOMMU run-pci-nvme or probed GCP bounce-buffer path without exporting host physical addresses to userspace. It remains valid for a verified direct-remapping/vIOMMU lane, or for a future synthetic device-address namespace that the manager translates before hardware sees it. The GCP/no-IOMMU path must use brokered bounce address publication instead.

The bounded admin-queue lane is implemented by nvme_model_b_provider_iova_proof: DMABuffer.info exports domain-scoped IOVAs, the provider authors AQA/ASQ/ACQ, and the live CC.EN selected write validates both queue windows before enabling the controller. The no-IOMMU controller-enable path remains brokered.

Model A vs Model B

DimensionBrokered address publication (kernel/device-manager materializes)Model B (provider-writes, kernel-validates)
Who writes the device-visible addressKernel or device manager writes queue-base/PRP/SGL values from live buffer authority. Provider submits typed requests or places opaque kernel-authored values only when that is safe.Provider writes the device-visible address itself into ASQ/ACQ/SQ/CQ bases and PRP/SGL entries.
Kernel roleAuthor of every device address; trivially correct by construction; no scan needed.Validator: on each doorbell/notify, scan the submitted descriptors/queue-base registers and reject any address outside the owner’s granted DMA window.
New kernel componentNone.A ring/queue-scan on-notify DMA validator (this proposal).
Driver authenticityProvider owns protocol choices but not raw device-address authorship. This is required when device-visible equals host physical.Provider is a real driver that owns its addresses.
Where it appliesNo-IOMMU brokered-bounce paths, including probed GCP shapes and the current no-IOMMU run-pci-nvme gate.Verified direct-remapping/vIOMMU paths, or a future synthetic address namespace.

The two models coexist. The existing virtio-net TX path keeps brokered/kernel- authored device addresses. The NVMe validator is retained for lanes where provider-written addresses are not host physical addresses. A DeviceMmio doorbell claim must declare which model is active; no-IOMMU claims must not accept provider-authored raw device addresses.

What Model B Requires: the On-Notify DMA Validator

The validator is a kernel component invoked on the doorbell/notify path of the NVMe provider’s DeviceMmio selected-write claim. Before the doorbell write reaches the device (i.e. before the controller can fetch the just-submitted descriptors or act on a just-programmed queue base), the kernel scans the device-visible addresses the provider wrote and fails closed if any address is not inside that owner’s granted DMA window(s).

Scan targets (what the validator reads)

  1. Queue-base registers, scanned when the doorbell/notify that arms a queue is rung (or on the controller-enable / CC.EN write that activates the admin queue): ASQ, ACQ, and the I/O SQ/CQ base addresses the provider programmed through its selected-write DeviceMmio claim.
  2. Submission-queue entries newly made visible by an SQ tail doorbell: the PRP1/PRP2 entries (and, where used, the PRP list pages and SGL descriptors) of each NVMe command between the last validated tail and the new tail. The validator follows one level of PRP-list indirection; deeper SGL/PRP-chain shapes are out of scope for the bounded proof and are rejected, not silently accepted.

The validator scans only on notify — not on every provider memory write. The provider may freely write into its own mapped DMA pages between doorbells; nothing device-reachable happens until a doorbell rings, and that is the single choke point the kernel guards. This bounds the validation cost to the descriptors a single doorbell newly publishes (one queue entry for a depth-1 admin proof, a small bounded batch otherwise), not to the whole address space.

Invariants (fail-closed on any violation)

  • Bounds. Every scanned device-visible address, and the full extent of the region it names (queue size × entry size for a queue base; transfer length for a PRP/SGL data pointer), must lie wholly within a DMA window granted to the owning provider. An address at the window edge whose region runs past the window end fails closed. Unaligned queue-base or PRP addresses (NVMe requires page-aligned PRP1 for the first entry, dword-aligned queue bases) fail closed.
  • Owner-scoping. The window set checked is exactly the set granted to the provider that owns the DeviceMmio doorbell claim being rung. An address that is valid for another owner’s window is rejected for this owner: no aliasing into a different owner’s DMA region, no host-physical address, no out-of-any-window address. The validator resolves “owner” from the doorbell claim’s grant identity, not from the address value.
  • No host-physical / no out-of-window. The provider-written value must be a domain-scoped IOVA or synthetic device address, never a host physical address. On the current no-IOMMU bounce path this invariant cannot be satisfied by provider-authored queue-base/PRP values, because device-visible equals host physical and userspace export is disabled.
  • Stale-completion / generation. The validator binds its accept decision to the live grant generation of the owner’s DMA window and doorbell claim. A doorbell rung after revoke/reset/regrant against a stale generation fails closed even if the byte value would have been in-window for the prior grant. Completions are accepted only against the issue/generation that was live at submission scan time, matching the existing stale-completion gate on the virtio-net path; a completion whose submission was never validated (or was validated under a now-retired generation) does not wake a waiter.
  • On-notify timing. The scan completes and either accepts or rejects before the doorbell write is allowed to take effect on the device. A rejected scan does not write the doorbell, returns a fail-closed error to the provider’s DeviceMmio write, and records the rejection; the device never sees the descriptor batch. There is no window in which the controller can fetch an unvalidated descriptor.
  • Quiesce/teardown. On release/reset/driver-death, in-flight doorbell scans are quiesced, the owner’s windows are removed from the validator’s accepted set, backing pages are scrubbed before frame reuse, and any subsequently rung doorbell against the retired grant fails closed.

Where it hooks

The validator hooks the NVMe provider’s selected-write DeviceMmio doorbell claim in the kernel capability layer — the same selected-write claim the bring-up slice scopes to the NVMe enable/admin-queue-base/doorbell registers (mirroring the virtio-net notify-write claim). Concretely:

  • The doorbell/queue-base DeviceMmio.write* path (kernel/src/cap/device_mmio.rs) gains a pre-write validation step for the NVMe doorbell/queue-arm register subset.
  • The scan reads the provider’s mapped SQ pages and queue-base register shadow through the manager-owned DMA window records (kernel/src/device_dma.rs), checking containment against the owner’s granted window descriptors. It does not gain a generic memory-read authority over the provider; it reads only the descriptor/queue-base bytes the doorbell newly publishes, via the manager’s record of the owner’s DMA pages.
  • Generation/owner identity comes from the grant ledger (kernel/src/device_dma.rs / the *_grant_source records), not from provider-supplied metadata.

This is a kernel-side, capability-scoped, on-notify check — not a new ambient syscall and not a per-write trap on all provider memory.

Performance note

The validator runs only on the notify/doorbell path, not on the data path and not on every provider write. Its cost is O(descriptors newly published by this doorbell) — one entry for the depth-1 admin/IDENTIFY proof, a small bounded batch for the I/O queue. Steady-state provider memory writes between doorbells are uninstrumented. This keeps the genuine-driver model without a per-access trap and without copying the data path through the kernel.

No-IOMMU Correction And Brokered Bounce Path

On GCE shapes without a usable guest IOMMU, and on the current no-IOMMU make run-pci-nvme gate, the labeled bounce-buffer backend does not provide a provider-visible IOVA namespace. The device-visible value a real NVMe controller consumes is the host physical or bus address of a manager-owned page. Publishing that value to userspace would violate the reviewed no-host-physical-exposure invariant.

Therefore the no-IOMMU storage path must be brokered:

  • The provider receives buffer capabilities, queue ownership handles, and typed NVMe command intent, not raw queue-base or PRP addresses.
  • The kernel or device manager allocates/pins the bounce pages and writes AQA/ASQ/ACQ, I/O queue-base, and PRP/SGL fields from the live ledger.
  • The selected DeviceMmio claim gates CC.EN, queue-arm, and doorbell writes on the brokered ledger state, not on provider-supplied numeric addresses.
  • Teardown still quiesces outstanding DMA, blocks stale completions, scrubs pages before reuse, and keeps hostile_hardware_isolation=not-claimed.

Model B can be reintroduced for NVMe when the proof gate is a verified direct-remapping/vIOMMU shape where the provider-visible value is a domain-scoped IOVA, or after capOS implements a synthetic address namespace that is translated by trusted code before the controller observes it.

Direct-Remapping Lane: Platform Precondition Measured (2026-07-25)

Reintroducing Model B on a direct-remapping lane rests on a platform fact that had never been measured: does an emulated NVMe controller actually honor VT-d translation? The virtio lane is the reason that cannot be assumed — a virtio device bypasses the platform IOMMU and consumes host-physical addresses unless the driver negotiates VIRTIO_F_ACCESS_PLATFORM (QEMU iommu_platform=on), so a “successful DMA” there proves nothing about translation. NVMe has no equivalent feature negotiation, so the question had to be settled by measurement, not by analogy.

make test-nvme-iommu-translation settles it. On -machine q35 -device intel-iommu,aw-bits=39 -device nvme (QEMU 8.2.2), the kernel programs a VT-d domain for the NVMe requester and drives a minimal admin queue (kernel/src/nvme_iommu_proof.rs, driven by iommu::diagnose_nvme_iommu_translation) with ASQ, ACQ, and the IDENTIFY payload PRP1 set to programmed IOVAs only:

  • Translation applies. The controller enables (CSTS.RDY=1) and completes IDENTIFY CONTROLLER, delivering a payload whose vendor id matches the enumerated PCI vendor id and whose serial-number field matches the serial the QEMU device was created with. The IOVAs are deliberately distinct from the pages’ host-physical addresses, so the controller could only have reached those pages by walking the second-level tables. No iommu_platform-style opt-in exists or is needed.
  • Negative control 1 — unmapped IOVA. The same command with PRP1 at an unmapped IOVA records a VT-d fault against this controller’s source id at that address. Note the device behavior this exposed: QEMU’s nvme model posts a success completion for the payload write the IOMMU dropped, the same way its virtio-rng model still advances the used ring for a faulting descriptor. The completion status is therefore a diagnostic on this shape, never the enforcement gate.
  • Negative control 2 — host-physical-as-IOVA. Because a fault record alone cannot distinguish “the IOMMU refused the write” from “the controller bypassed the IOMMU”, PRP1 is also offered the payload page’s own host-physical address. A controller ignoring translation would deposit IDENTIFY data into exactly that page; instead the transfer faults and the scrubbed page comes back untouched. This is the control that excludes a bypass.
  • Negative control 3 — revoked target. After the ACQ and payload mappings are removed, invalidated, and their frames freed, the same still-enabled controller is re-driven from the still-mapped ASQ. Its DMA faults instead of landing in the freed frames: no completion appears and both pages still read back as the scrub zeros.
  • Teardown under an enabled controller. The controller is disabled (CC.EN=0 until CSTS.RDY=0) and PCI bus-master cleared before any VT-d table frame is returned to the allocator, on top of the existing translation-disabled-before-table-free ordering.

What this does not establish. The kernel is the driver in this proof: it owns the pages, authors every device-visible value, and rings every doorbell. Nothing here is provider authority, no address is published to userspace (host_physical_user_visible=0, provider_authored_addresses=none), and the live on-notify validator is not on this path. What it removes is the reason the Model-B lane could not be attempted at all.

Provider-authority continuation. The production not(qemu) proof build now uses the dedicated kernel/src/nvme_model_b_iommu.rs domain instead of reusing the QEMU diagnostic module. A provider DMAPool allocation installs the leaf and makes DMABuffer.info’s existing deviceIova/iovaExport ABI return the domain-scoped address. The selected-write path stages each low dword without MMIO, validates the completed 64-bit ASQ/ACQ, and performs a second owner/generation-scoped validate_doorbell_scan before CC.EN=1. The shared DeviceMmio cap-release/process-exit path disables an enabled controller, waits for CSTS.RDY=0, and clears PCI bus mastering before it detaches the claim or releases either queue pin. A queue-cap release that arrives first records deferred cleanup on the parked buffer; MMIO release replays it after quiescence, including leaf removal, invalidation, page scrub, and slot reclamation. Admin activation requires distinct live SQ and CQ buffer handles; using one domain IOVA for both queues fails before the CC.EN write. A failed leaf-install invalidation instead leaves the allocation parked and the possible cached translation quarantined; the page is not scrubbed or returned to the allocator, the IOVA is not exported as active, and domain teardown stays blocked until translation is disabled. test-nvme-model-b-provider-iova proves the accepted and rejected cases, enabled-controller release quiescence, and reuse of both released queue slots with fresh generations. What remains is extending this bounded admin-queue authority to provider-authored command PRPs and steady-state SQ doorbells.

Brokered Alternative For No-IOMMU

The brokered model is no longer a rejected storage alternative for no-IOMMU targets. It is the required GCP/no-IOMMU design until a safe non-host-physical device-address namespace exists. Its tradeoff is narrower driver authenticity: userspace owns NVMe protocol state and command construction, but trusted kernel or manager code remains the author of raw device addresses.

Implementing Slices

  • nvme-doorbell-dma-validator (landed 2026-05-27 08:56 UTC): the kernel on-notify DMA validator mechanism (kernel/src/cap/nvme_doorbell_validator.rs, validate_doorbell_scan / completion_wakes_waiter) and its invariants, proven by the bounded cfg(qemu) hostile-scan self-test (prove_qemu_on_notify_scan_contract) that make run-pci-nvme asserts: out-of-window, host-physical, cross-owner-alias, region-overrun, unaligned, deeper-PRP-chain, and stale-generation all fail closed with no doorbell write and no waiter wake. Synthetic owner windows stand in for the live grant ledger; the live DeviceMmio doorbell-path wiring is the bring-up slice below. This is the kernel component Model B requires; the controller bring-up slice depends on it. Provenance map: NVMe.
  • nvme-no-iommu-brokered-controller-enable (landed 2026-05-27 21:38 UTC, commit 11b86568): no-IOMMU replacement for the blocked provider-written enable task; brokered admin queue-base materialization with no host-physical export.
  • nvme-model-b-iova-validation-on-iommu-lane (landed 2026-07-25): make test-nvme-iommu-translation measures that QEMU’s emulated NVMe controller honors VT-d translation, with the three negative controls above. make test-nvme-model-b-provider-iova then boots the production capability bundle behind VT-d, exports only domain-scoped IOVAs, rejects an unmapped provider-authored ASQ and an aliased ASQ/ACQ pair with no MMIO side effect, accepts the live owner/generation-scoped ASQ/ACQ scan, reaches CSTS.RDY=1, and releases both queue capabilities while enabled, then quiesces on MMIO release and replays their deferred leaf removal, context-cache/IOTLB invalidation, scrub, and free before proving both slots reusable with fresh generations. The bounded proof covers admin queue activation; provider-authored command PRPs and steady-state SQ doorbells remain later work.
  • nvme-userspace-bind-and-controller-bringup: its bind and bring-up headline shipped through the brokered no-IOMMU chain; what remained of it was the Model-B provider-authored lane, now tracked by the task above.
  • nvme-admin-queue-identify (landed 2026-05-27 22:34 UTC, commit cede5257) closes the no-IOMMU admin command.
  • nvme-admin-interrupt-delivery (landed 2026-05-27 23:07 UTC, commit 18fd25c7) closes the admin completion wake.
  • nvme-io-queue-and-read is the ready brokered I/O/read continuation. It inherits the same split: provider-written PRPs require direct remapping or a synthetic namespace; no-IOMMU GCP planning requires brokered PRP materialization.

Design Grounding

  • docs/proposals/cloud-driver-foundation-gap-analysis.md (the foundation map and the original Model A recommendation this overrides for storage)
  • docs/dma-isolation-design.md (Cloud DMA Backend; bounce-buffer fallback; IOVA/window discipline; teardown/scrub ordering)
  • docs/proposals/dma-assurance-model-proposal.md
  • ddf-provider-virtio-net-driver-closeout (the Model A virtio-net TX provider that this leaves unchanged)
  • kernel/src/cap/device_mmio.rs (the selected-write claim the validator hooks), kernel/src/device_dma.rs (owner DMA window records / grant generation), kernel/src/cap/{dma_pool,dma_buffer,interrupt}_grant_source.rs, kernel/src/pci.rs (NVMe enumeration today)