# Proposal: Virtio-9p Host Directory Passthrough

A QEMU-hosted fixture driver mounts a host directory into a capOS guest as
`Directory`/`File` capabilities over the virtio-9p device, using the 9P2000.L
protocol. Read-only and structurally separate writable grant sources are
implemented under either the full `qemu` feature or the composable
`virtio_9p_host_fixture` feature. Builds with neither feature refuse both grant
sources: this is development infrastructure, never a production path.

## Motivation

Three concrete needs, in priority order:

1. **Dev-loop payload injection.** Today every guest-visible input — WASI
   payloads, Lua scripts, test corpora, fixture files — must be baked into the
   boot ISO or a disk image (`tools/mkstore-image`) before boot. Changing one
   test input costs a full ISO rebuild. A host-shared directory removes that
   rebuild from the inner loop.
2. **Artifact export from proofs (write slice).** QEMU proofs assert by
   grepping serial-log markers. With guest→host file export, a proof can write
   structured results (reports, dumps, benchmark tables) that host harnesses
   read directly — richer assertions than ordered-marker matching, and a
   natural output channel for a future declarative proof runner.
3. **Task-backend persistence shortcut.** The
   [Self-Hosted Task Backend](../backlog/self-hosted-task-backend.md) track
   aims to replace the host-side PostgreSQL task board with a capOS-served
   coordinator. Its durable serve loop is currently blocked on a spawn-grant
   authority gap for `BlockDevice` caps. The writable 9p `Directory` now has a
   single-writer child spawn-grant path, so it can give the coordinator
   restart-persistent, **host-inspectable** state files through an authority
   path that exists today.

## What Exists Today

`Directory`/`File`/`Store`/`Namespace` are established Cap'n Proto interfaces
with several backers: the RAM-backed kernel fixtures
(`kernel/src/cap/directory.rs`, `kernel/src/cap/file.rs`,
`kernel/src/cap/store.rs`, and `kernel/src/cap/namespace.rs`), the block-device
filesystems (`readonly_fs`, `fat_fs`, `persistent_store`, `writable_fs`), the
boot-ISO reader (`installable_image`), and the QEMU-hosted virtio-9p host-share
fixture described here. Disk-backed paths require an image prepared host-side;
virtio-9p instead exposes one dedicated host directory at QEMU launch.

The pure protocol layer in `capos-lib/src/ninep.rs` implements the bounded read
and write subsets. It encodes requests and decodes replies with negotiated
`msize`, frame-size, tag, reply-type, counted-payload, directory-entry, and
attribute-mask validation. Host tests cover the supported request/reply shapes
and hostile frames; `fuzz/fuzz_targets/ninep_reply_decode.rs` exercises
structured reply decoding. This codec has no transport I/O of its own.

The kernel-side fixture driver that carries it over the wire is implemented
(`Virtio9pDriver` in `kernel/src/virtio.rs`): it probes the
modern-PCI device, negotiates features, brings up a polled request virtqueue,
and completes the `Tversion`/`Tattach` session handshake against a QEMU
`-fsdev local` share, proven by `make test-virtio-9p-bringup`. Above it,
`kernel/src/cap/virtio_9p_fs.rs` serves the share as read-only
`Directory`/`File` capabilities through the fixture-gated `virtio_9p_root`
grant source, proven by `make test-virtio-9p-fs`. The separate
`virtio_9p_root_writable` source serves the bounded write subset, proven by
`make test-virtio-9p-write`; a read-only grant cannot be upgraded at runtime.
WASI payload reload consumes the read-only source today. Host-inspectable
task-backend persistence on the writable source is the active follow-on.

Two distinct transport surfaces remain mutually exclusive at the virtio-net
ownership boundary. The explicit `virtio_9p_host_fixture` feature is the narrow
exception for a different PCI function: it carries only the kernel-side 9p
subset into the userspace-network build without enabling the full `qemu`
surface or weakening the Nic guard.

- **Kernel-side split-ring machinery, available under `qemu` and for 9p under
  `virtio_9p_host_fixture`.**
  `kernel/src/virtio.rs` carries a generic virtqueue implementation
  (`Virtqueue`, `submit_request_chain`, `notify`, `poll_used`,
  `poll_used_within_ns`) over the device-agnostic modern-PCI surface
  `kernel/src/virtio_transport.rs`. `bring_up_blk_device` uses it to drive the
  virtio-blk fixture that backs `readonly_fs`, `fat_fs`, `persistent_store`,
  and `writable_fs`; those block-device and other full Device Driver
  Foundation (DDF) consumers remain
  `qemu`-only.
- **Userspace MMIO driver authority, available only under `not(qemu)`.** The
  `cloud_virtio_net_userspace_*_proof` and `cloud_nvme_controller_reset_proof`
  features admit narrow, selected-write userspace `DeviceMmio.write32` access.
  Each is documented in `kernel/Cargo.toml` as mutually exclusive with `qemu`,
  and they are staged from real cloud PCI enumeration, not from QEMU.

Under `--features qemu`, userspace MMIO write authority exists but is confined
to **narrow per-register claims** staged by the grant source.
`device_manager/qemu_full.rs`'s `validate_devicemmio_write32_claim` admits only
three shapes: an exact pinned `(offset, value)` pair for the virtio claims (the
virtio-net TX notify doorbell, the virtio-rng MSI-X vector-control write); a
value-flexible NVMe `CC` write that is accepted only with `CC.EN` clear, i.e. a
controller reset; and brokered NVMe doorbells whose address-bearing fields the
kernel materializes from its own ledger. Any other offset is unclaimed and
fails closed.

None of those shapes let userspace program the virtio common-config
queue-address registers, set `queue_enable`, or drive the feature-negotiation
handshake — those offset constants are compiled only under the `not(qemu)`
`cloud_virtio_net_userspace_ownable_vring_proof` and
`cloud_virtio_net_userspace_queue_enable_driver_ok_proof` features, which took
a multi-slice ladder to land.

Separately, and on **both** axes, no capability exists for userspace-authored
descriptor chains or used-ring polling on a live queue. Even in the `not(qemu)`
NIC ladder the userspace driver handles only opaque `deviceIova` bounce tokens
that the kernel resolves; descriptor authoring and used-ring completion stay
kernel-mediated behind a typed `Nic` capability.

An earlier revision of this document claimed the userspace driver track had
already landed the groundwork this proposal needs. That was an overstatement:
it read `not(qemu)` production-path proofs as if they were available to a
`qemu` fixture. The Driver Boundary Decision below supersedes it.

## Driver Boundary Decision

**Decided: a kernel-side QEMU-hosted fixture driver (Option A).** virtio-9p is
implemented in `kernel/src/virtio.rs` reusing the existing generic split-ring
machinery exactly as `bring_up_blk_device` does for virtio-blk. The
`Directory`/`File` capabilities are served by a kernel cap module backed by
that driver, mirroring how `readonly_fs` and `fat_fs` back the same interfaces
over `BlockDevice`. Userspace consumers — shell `ls`/`cat`, `wasm-host`, the
task coordinator — see only the existing typed `Directory`/`File` interfaces
and need no new interface, which was the point of the userspace-driver framing
in the first place.

The original full fixture path remains inside `--features qemu`; the later
composable extraction exposes the same 9p subset through the explicit
`virtio_9p_host_fixture` proof feature. Both routes fail closed when neither
feature is selected, introduce no new authority class, and reuse the same
`Virtqueue`/`VirtqueueDma` seam governed by the settled DMA isolation design
(`docs/dma-isolation-design.md`), so they add no new DMA backend.

Two costs are real and belong to slice 1 rather than to the decision:
`submit_request_chain` is device-agnostic, but `Virtqueue` still carries
virtio-net-specific fields whose factor-out the code defers to "the first
block-device caller", and a 9p driver needs its own `VirtqueueDma`
implementation.

Rejected alternatives:

- **Option B — a new userspace MMIO authority class under `qemu`.** Would
  preserve the userspace-driver shape, but requires a `virtio_9p_*`
  selected-write policy for the common-config handshake and queue-address
  registers and `queue_enable` — none of which the qemu axis admits today —
  *plus* a capability for userspace-authored descriptor chains and used-ring
  polling on a live queue that exists nowhere in the tree. (The notify doorbell
  alone is not the gap: a pinned-claim virtio doorbell write already exists
  under `qemu`.)
  That re-implements the NIC-driver ladder on a third feature axis and extends
  past anything landed, for a development fixture. Cost is not justified by the
  benefit; if a userspace virtio driver authority class is wanted, it should be
  motivated by a production device, not by a dev-loop convenience, and
  decomposed as its own ladder.
- **Option C — relocate the proof to the `not(qemu)` production axis.** Would
  reuse the landed `cloud_virtio_net_userspace_*` authority chain, but it
  contradicts this proposal's core premise: virtio-9p passthrough is
  development infrastructure that must never be a production path. It would
  also stage a dev-loop fixture behind real cloud PCI enumeration, which
  defeats the dev-loop motivation entirely.

The accepted cost of Option A is that the driver is kernel-side rather than a
userspace driver process. That is the same trade already accepted for the
virtio-blk fixture, and it remains bounded because the code is selected only by
the full `qemu` feature or the explicit `virtio_9p_host_fixture` proof feature.
Neither is a default or implied cloud feature, and builds with neither fail
closed, so this does not grow the production kernel's device surface.

## Design

- **Device**: virtio-9p (`-fsdev local,path=<hostdir>,security_model=none,
  readonly=on -device virtio-9p-pci,mount_tag=...`). Fully emulated inside
  QEMU; no external daemon.
- **Protocol**: 9P2000.L, minimal client subset. Read path:
  `Tversion`/`Tattach`/`Twalk`/`Tlopen`/`Tread`/`Treaddir`/`Tgetattr`/
  `Tclunk`. Write slice adds `Tlcreate`/`Twrite`/`Tfsync`/`Trename`/
  `Tunlinkat`. Message codec is pure `no_std` logic in `capos-lib` style:
  host-tested, fuzzable, bounded, fail-closed on malformed replies.
- **Driver shape**: a kernel-side QEMU-hosted fixture driver over the generic
  split-ring machinery in `kernel/src/virtio.rs` (see the Driver Boundary
  Decision above), **polled** — no MSI-X dependency, matching the virtio-blk
  fixture. A dev fixture has no latency requirement that justifies interrupt
  plumbing.
- **Capability surface**: a kernel cap module serves the existing
  `Directory`/`File` interfaces over the 9p client, the way `readonly_fs` and
  `fat_fs` serve them over `BlockDevice`. Consumers (shell `ls`/`cat`,
  `wasm-host`, the task coordinator) need no new interface. Read-only exports
  serve a `Directory` whose mutating methods fail closed.
- **Manifest gating**: the grant sources require either `qemu` or the explicit
  `virtio_9p_host_fixture` proof feature and fail closed with neither.
  Write-enabled exports additionally require a distinct manifest source so a
  read-only share cannot be silently upgraded.

### Why 9p and not virtiofs

virtiofs needs an external `virtiofsd` daemon, vhost-user shared-memory
plumbing, and a guest FUSE client — an order of magnitude more machinery whose
payoff is performance, which is irrelevant for a development fixture.
virtio-9p is built into QEMU, its transport is plain virtqueues over the
already-landed modern-PCI surface, and the 9P2000.L request/reply messages map
nearly 1:1 onto the `Directory`/`File` capability methods. Other rejected
alternatives: `fw_cfg` (single small blobs, no directory semantics) and
keeping the disk-image-only workflow (remains correct for production-shaped
storage proofs, but leaves the dev loop and the spawn-grant shortcut on the
table).

## Task-Backend Shortcut: Honest Trade-offs

The `CAPOSRS1` WAL record store over `BlockDevice` already provides durable,
crash-recovering coordinator persistence (`make test-task-coordinator-persist`)
— 9p adds no new durability capability. What it adds:

- **A grant path that works now.** `Directory` spawn-grants are landed; the
  `BlockDevice` spawn-grant gap blocks the durable Endpoint serve loop.
- **Host-inspectable state.** Record-per-file layout (one file per task,
  current/temp/backup publication) makes the board state a plain host
  directory: greppable, diffable, backed up or versioned with ordinary tools,
  no image mounting. Task keys use canonical lowercase-hex UTF-8 filenames, so
  arbitrary valid keys remain reversible without becoming paths.
- **Operational simplicity for self-hosting.** A capOS-in-QEMU task backend
  whose state survives VM restarts in a host directory is a realistic
  dogfooding deployment shape for the multi-agent workflow.

Limits to state plainly: durability depends on the host filesystem and QEMU's
`Tfsync` handling, weaker than the fenced `CAPOSRS1` frame guarantees — the
WAL-over-`BlockDevice` path remains the reference persistence design, and the
9p path is a pragmatic parallel track, not a replacement. Single writer only.
QEMU-hosted by construction; a production deployment still needs the NVMe/
`writable_fs` path or the future userspace storage service. The implementation
accepts at most 20 tasks and 60 directory entries, rejects malformed, stray,
duplicate, oversized, or ambiguous recovery state, and keeps the previously
published record for every pre-publication crash window. A boot validates the
complete restored graph before serving. It preserves fencing generations but
invalidates restored leases because boot-relative monotonic expiry instants are
not comparable across restarts.

## Decomposition

Tracked as loopyard tasks; each behavior slice carries its QEMU proof and the
driver slices maintain a `docs/devices/virtio-9p.md` provenance map as part of
the same change:

1. [virtio-9p kernel fixture bring-up](https://tasks.cap-os.dev/p/capos/t/virtio-9p-fixture-bringup) —
   **Landed:** kernel-side modern-PCI probe, feature negotiation
   (`VIRTIO_F_VERSION_1` + `VIRTIO_9P_F_MOUNT_TAG`), and polled request-virtqueue
   setup over `virtio_transport` in `kernel/src/virtio.rs`, following
   `bring_up_blk_device`; `Tversion`/`Tattach` handshake driven through the
   landed `capos_lib::ninep` codec. `make test-virtio-9p-bringup` asserts the
   negotiated `msize`/version, the mount tag read back from device config, and
   the attach qid's directory type bit from a kernel-side serial diagnostic,
   following the `diagnose_virtio_blk_transport` precedent. That slice granted
   no userspace authority on its own: the `Directory`/`File` export and its
   grant source arrived in slice 3 below. No userspace `DeviceMmio` write
   authority is involved. Provenance map:
   [`docs/devices/virtio-9p.md`](../devices/virtio-9p.md).
2. [9P2000.L client core](https://tasks.cap-os.dev/p/capos/t/virtio-9p-client-core) — **Landed:** host-tested
   `no_std` read-subset message codec, bounded fail-closed reply decode, and
   the `ninep_reply_decode` fuzz target.
3. [Directory/File caps over 9p](https://tasks.cap-os.dev/p/capos/t/virtio-9p-directory-file-caps) —
   **Landed:** read-only `Directory`/`File` served by `kernel/src/cap/virtio_9p_fs.rs`
   over the driver's bounded read subset
   (`Twalk`/`Tlopen`/`Tgetattr`/`Treaddir`/`Tread`/`Tclunk`), alongside
   `readonly_fs`/`fat_fs`, behind the fixture-gated `virtio_9p_root` grant source.
   `make test-virtio-9p-fs` has a guest consumer list the host share, read exact
   host bytes whole and at an offset, and observe every mutating and traversing
   method fail closed. A kernel built with neither `qemu` nor
   `virtio_9p_host_fixture` compiles no 9p cap module and resolves the source to
   ``kernel source `virtio_9p_root` requires the qemu feature (or the composable
   virtio_9p_host_fixture feature)``.
4. [Write support](https://tasks.cap-os.dev/p/capos/t/virtio-9p-write-support) — **Landed:** the write-path
   messages (`Tlcreate`/`Twrite`/`Tfsync`/`Trename`/`Tunlinkat`) in the codec
   and a separate driver write façade, served by a second, structurally
   attenuated cap-type pair (`Virtio9pWritableDirectoryCap` /
   `Virtio9pWritableFileCap`) behind the fixture-gated `virtio_9p_root_writable`
   grant source. Naming that source in the manifest **is** the explicit write
   flag: there is no rights flag and no method that upgrades a read-only cap,
   so the choice is fixed at spawn. One fixture-gated raw spawn grant may mint
   the writable root for a child; the root and writable `File` results are
   nontransferable, while read-only results retain same-session copy
   delegation. `make test-virtio-9p-write` runs four stages: a read/write share
   whose bytes are verified on the host, a `readonly=on` share where the server
   refuses write intent with `EROFS` while reads keep working, the single-writer
   child-grant proof, and a two-boot forced-QEMU-poweroff `Tfsync` stage with a
   pre-sync negative control. The first stage covers multi-chunk `Twrite`
   resubmission, but server-reported short writes and zero-count `WriteStalled`
   handling remain unproven. The device provenance map is authoritative for the
   fourth stage's bounded outcome and for the remaining `Tfsync` and non-atomic
   `Trename` residuals:
   [`docs/devices/virtio-9p.md`](../devices/virtio-9p.md).
5. [Task-coordinator 9p persistence](https://tasks.cap-os.dev/p/capos/t/task-backend-9p-persistence) —
   **Landed:** the Endpoint-serving coordinator optionally consumes the
   spawn-granted writable 9p `Directory`, writes one canonical record per task,
   and recovers the bounded current/temp/backup publication windows before
   serving. `make test-task-coordinator-9p` proves state reload across two VM
   boots, host-visible clean state files, preserved lease generations with
   restored leases invalidated, and a post-restart mutation. This path remains
   weaker than the fenced `CAPOSRS1` WAL and is not production storage
   durability.
6. [WASI payload hot-reload](https://tasks.cap-os.dev/p/capos/t/wasm-payload-hot-reload-9p) — **Landed:**
   `wasm-host` reads a bounded, single-component payload through an explicitly
   granted read-only `Directory`, while manifests without that grant retain the
   embedded-payload path. `make test-wasi-9p-reload` builds one payload-free ISO,
   rejects missing, oversized, malformed, and truncated host-share payloads,
   and executes two distinct host-swapped payloads during one success boot
   without rebuilding the ISO. Read-only 9p `File` capabilities bind reads to
   the qid and metadata captured at open, so an atomic host replacement cannot
   substitute bytes under a stale size snapshot; the synchronized replacement
   proof is part of `make test-virtio-9p-fs`. The completed implementation is
   commit `1eb399e2` (`2026-07-20 05:28 UTC`). Preview 1 `proc_exit` now
   terminates only the current wasm instance and returns its status to the
   host loop. The focused proof runs two distinct exit-zero payloads across
   host replacements, records each exit code, and keeps `wasm-host` alive
   between instances. Every fixture payload opens and retains a File result
   capability and attenuates its borrowed preopen rights before terminating;
   the same QEMU transcript proves teardown releases one File, clears its fd
   slot and argv/environment buffers, and restores the preopen rights before a
   successor starts.
   The same boot distinguishes a non-zero exit from a runtime trap and does
   not label either outcome as a successful reload.
   Single-shot payloads still map `proc_exit(code)` back to the wasm-host
   process status. This remains a bounded four-instance QEMU development
   proof, not a general restart-policy or long-running module supervisor:
   wasmi 1.0.9 exposes no instance-removal API, so completed instances and
   linear memories remain allocated in the shared Store until wasm-host exits.
7. [Composable fixture feature](https://tasks.cap-os.dev/p/capos/t/virtio-9p-composable-fixture-implementation) —
   **Implemented (this document, Composable Fixture Feature).** Extracts the 9p
   fixture from the monolithic `qemu` axis into a `virtio_9p_host_fixture`
   feature that also builds in a `not(qemu)` cloud kernel, so the writable-9p
   persistence path and the Phase C userspace network stack co-boot for the
   host-accessible task backend. The `not(qemu)` fixture build routes device
   claim through the minimal `device_manager::virtio_9p_fixture_claim` surface
   (the qemu-only DDF ledger, net/blk/rng drivers, `prove_qemu_*` harness, and
   IOMMU diagnostic stay `#[cfg(feature = "qemu")]`), and PCI enumeration takes
   the dedicated `pci::diagnose_virtio_9p_host_fixture` startup route. Co-residency
   is proven by `make test-virtio-9p-net-coboot`: one `not(qemu)` boot serving
   both the Phase C userspace `Nic` path and a writable virtio-9p `Directory`,
   with the writable-9p write proof verified on the host side of the share.

## Security Considerations

The guest→host boundary is the new surface. Mitigations: the share is scoped
to one dedicated host directory; QEMU-level `readonly=on` mirrors the
capability-level read-only export (defense in depth); `security_model=none`
maps all access to the QEMU process's own uid — the guest can never act as
another host principal; write-enabled shares are opt-in per manifest and
should point only at dedicated scratch/state directories. The 9p client treats
the QEMU server as untrusted input: all replies are bounded and fail closed,
and reply parsing is host-fuzzed like the other mount/wire parsers.

## Composable Fixture Feature (`virtio_9p_host_fixture`)

### Why the `qemu` axis is not enough

The [Host-Accessible Persistent Task Backend](../backlog/self-hosted-task-backend.md)
milestone needs one boot that serves **both** the writable-9p persistence path
(host-inspectable task state) **and** the Phase C userspace HTTP/JSON network
topology (the host-API surface, `task-backend-9p-api-persistence`). Those two
capabilities sit on **disjoint feature axes** today:

- Writable 9p is reachable only under `--features qemu`
  (`KernelCapSource::Virtio9pRootWritable` → `cap::virtio_9p_fs`, the real
  `mod virtio` in `kernel/src/main.rs`).
- The Phase C userspace network stack builds under the `not(qemu)`
  `cloud_virtio_net_userspace_sustained_receive_pool_proof` chain, whose
  transitive `cloud_virtio_net_userspace_features_ok_proof` trips an explicit
  `compile_error!` against `qemu` (`kernel/src/cap/mod.rs`): the `qemu` DDF
  backend and the proof-only selected-write virtio-net common-config grant
  would race the same virtio-net BDF.

Directly composing the two feature worlds is therefore rejected by
construction, and it should stay rejected: virtio-net ownership must not be
shared between the kernel DDF fixture and the userspace driver. The chosen
direction keeps userspace networking unchanged and instead makes **virtio-9p**
— a different PCI device on a different BDF, with no virtio-net contention —
composable into the `not(qemu)` build behind its own feature. This does not
weaken the `Nic × qemu` guard, does not restore kernel TCP/socket ownership,
and adds no networking surface.

### Dependency cut (empirically derived)

A compile probe re-gated the minimal 9p surface — `mod virtio` (real) and `mod
iommu` in `main.rs`, `mod virtio_9p_fs` plus the `Virtio9pRoot` /
`Virtio9pRootWritable` grant-source arms in `cap/mod.rs`, and the
`Writable9pSpawnReservation` spawn arm in `cap/process_spawner.rs` — onto
`any(feature = "qemu", feature = "virtio_9p_host_fixture")` and built the kernel
`--no-default-features --features virtio_9p_host_fixture --target
x86_64-unknown-none`. (The kernel declares no `default` feature, so
`--no-default-features` is a no-op; it is spelled out only for reproducibility.)

The build stopped with **67 distinct unresolved-symbol errors** (46× `E0425`,
21× `E0433`), **all located in `kernel/src/virtio.rs`** and **zero reported**
from `cap/virtio_9p_fs.rs`, `device_dma.rs`, `virtio_transport.rs`, or
`capos_lib::ninep`. No `compile_error!` fired. This is partial dependency-cut
evidence, not a successful build: unresolved names can prevent later items from
being type-checked and can hide additional diagnostics.

Adding the network chain (`virtio_9p_host_fixture,
cloud_virtio_net_userspace_sustained_receive_pool_proof`) produced the
**byte-identical** 67-error set (delta 0). That shows only that the incomplete
probe exposed no additional diagnostic before the same failure point. It makes
the feature composition plausible because the existing mutual-exclusion targets
`qemu`, but it neither proves that the two features coexist successfully nor
rules out errors hidden behind the unresolved names. Successful builds of all
five matrix cells and the co-boot proof below are the buildability evidence.

**Bucket A — required 9p infrastructure (must be available in `not(qemu)`).**

- *Surfaced by the probe as unresolved edges inside the 9p bring-up, currently
  `qemu`-gated, mechanically re-gatable to `not(qemu)`:*
  - `pci::DmaRemappingRequesterBinding` and its builder
    `pci::dma_remapping_requester_binding_for_device` — threaded through
    `virtio::diagnose_virtio_9p_transport`; built from the always-available
    `capos_lib::device_authority` types plus `acpi::DmarDiscovery`. The 9p
    driver carries this binding value; it never calls into `iommu.rs`.
  - `device_manager::claim_pci_function` and
    `device_manager::attach_dmapool_record_with_remapping` — today defined only
    in `device_manager/qemu_full.rs`. These are the **same** device-ownership +
    DMA-pool-attach functions the virtio-blk fixture uses, i.e. genuine shared
    single-queue-fixture infrastructure, not 9p-bespoke. Making 9p composable
    requires these two (plus `DeviceOwner::Virtio9p`) to exist in the
    `not(qemu)` surface — extracted into the always-built `device_manager`
    surface / a shared submodule, or provided by the `stub` backend, not the
    whole `qemu_full` DDF backend.
- *Also `qemu`-gated and required for a live cloud wire-up, but not surfaced as
  an error because its caller is itself still gated:* the module-private
  `diagnose_qemu_virtio_9p` in `pci.rs`. `pci::enumerate` does **not** call this
  helper today. The call is nested at the end of
  `pci::diagnose_qemu_virtio_net`, which is itself reached only from the
  `#[cfg(feature = "qemu")]` diagnostics block in `main.rs`. The composable
  path therefore needs the independent startup route defined below; merely
  re-gating the helper cannot bind a fixture-only device.
- *Already resolved in `not(qemu)` — no change needed, listed so the cut is
  complete:* `DeviceOwner::Virtio9p` (always-built `device_manager/handles.rs`);
  the `device_dma` single-queue 9p pool (`begin_virtio_9p_pool`,
  `register_virtio_9p_queue`, `allocate_virtio_9p_page`, `free_virtio_9p_page`,
  and the `VIRTIO_9P_*` budgets, in always-built `device_dma.rs`); the generic
  split-ring machinery (`Virtqueue`, the `VirtqueueDma` trait, `DmaPage`,
  `VirtqueueDescriptorTracker`, `submit_request_chain`, `poll_used`,
  `discover_modern_transport`); the always-built `virtio_transport` modern-PCI
  surface; `pci::is_virtio_9p` and the `VIRTIO_9P_*_DEVICE_ID` constants; and
  the `capos_lib::ninep` codec.

**Bucket B — unrelated `qemu`/DDF surface, stays `qemu`-only (must NOT be pulled
into the fixture build).** The remaining ~54 edges are the net/blk/rng drivers
and the DDF proof harness sharing one `virtio.rs` and one
`device_manager/qemu_full.rs`:

- the `prove_qemu_*` DDF hostile-smoke family (ownership, teardown, and the
  devicemmio/dmapool/dmabuffer/interrupt cap-release, driver-crash,
  reset-disable, and stale-DMA-completion hooks) and its `*ProofError` types;
- the userspace-provider grant-source modules
  `cap::{devicemmio,interrupt,dmapool}_grant_source`;
- `crate::iommu` — reached **only** via the virtio-**rng** `IommuRngDmaVehicle`
  proof (`iommu.rs` itself carries an inner `#![cfg(feature = "qemu")]`), so
  VT-d IOMMU is confirmed **not** a 9p dependency;
- `sched::current_cpu_lease_nohz_active`, the net-provider helpers
  (`provider_notify_doorbell_write_count`,
  `provider_route_device_handle_for_bdf_owner`), and the net/rng bring-up
  device-manager functions `attach_devicemmio_record`, `attach_interrupt_source`,
  and `select_first_decoded_memory_bar_region`.

The polled 9p driver claims no MSI-X route (`device_manager/qemu_full.rs` already
returns `None` for `DeviceOwner::Virtio9p`), so none of the interrupt/MSI-X
surface in Bucket B is needed.

### Independent 9p startup route

The `not(qemu)` fixture path must not enter the QEMU net diagnostic wrapper.
The implementation extracts a dedicated PCI entry point such as
`pci::diagnose_virtio_9p_host_fixture(dmar)`, gated for
`virtio_9p_host_fixture`, and calls it from `main.rs` after ACPI discovery and
`pci::record_mcfg_probe` under
`all(feature = "virtio_9p_host_fixture", not(feature = "qemu"))`. That entry
point enumerates PCI once for its path and passes the resulting device list to
the existing 9p-only bind helper. It selects only `pci::is_virtio_9p`, claims
only `DeviceOwner::Virtio9p`, and never invokes the virtio-net/blk/rng binders,
the IOMMU diagnostic, or DDF proof entry points.

The full-`qemu` path keeps its current single enumeration and 9p-helper call
inside `pci::diagnose_qemu_virtio_net`; `main.rs` must not call the new fixture
entry point in that build, which avoids a duplicate 9p claim. An absent 9p
function still takes the existing not-found path, leaving both grant sources
unable to mount.

### The `virtio.rs` split

The crux is that `virtio.rs` is monolithic but **structurally separable**. The
failed probe exposed no diagnostics outside `virtio.rs`, and its visible
unresolved edges distinguish the shared ring/9p dependencies from the
net/blk/rng drivers and `prove_qemu_*` harness. It did not compile either subset
successfully, so the matrix builds remain the check on this classification.

The implementation gates `virtio.rs` at item granularity so a
`virtio_9p_host_fixture` build compiles the generic scaffold and 9p driver
(`Virtio9pDriver`, its `VirtqueueDma` implementation,
`bring_up_9p_device`, `diagnose_virtio_9p_transport`, and the `VIRTIO_9P_*`
constants) while excluding the **real QEMU** net/blk/rng drivers and DDF proof
items. The qemu implementation items stay `#[cfg(feature = "qemu")]`; the
shared scaffold and 9p driver use
`any(feature = "qemu", feature = "virtio_9p_host_fixture")`.

Selecting that real 9p module cannot discard the current non-QEMU typed-negative
network façade. `cap::network` is always compiled and consumes the handle,
error, configuration, TCP, UDP, and polling symbols currently provided by
`virtio_stub.rs`. The split therefore moves that façade into a non-QEMU
submodule (or equivalently includes and re-exports it) whenever
`not(feature = "qemu")`, including fixture-only and net-plus-fixture builds.
Those functions keep returning the same fail-closed results; they are
compatibility types, not a kernel networking implementation and not Bucket B
authority.

Splitting `virtio.rs` into a `virtio/` module directory (`scaffold`, `blk`,
`net`, `network_stub`, `rng`, `ninep`, `proofs`) is the recommended shape, but
an equivalent façade/re-export layout is acceptable if all three configurations
remain type-correct: full `qemu`, fixture with the real 9p subset plus the
negative network façade, and neither feature with the negative façade alone.
The `virtio.rs` scaffold factor-out already flagged in the Driver Boundary
Decision (the virtio-net-specific fields on `Virtqueue`) is adjacent to this
split and should land with it.

### Feature definition and fail-closed cfg rules

```
# kernel/Cargo.toml
virtio_9p_host_fixture = []   # no implied features; does NOT imply qemu
```

- The feature is **additive and orthogonal to `qemu`**: `qemu` continues to
  imply the whole fixture, so every existing `--features qemu` path is
  unchanged. `virtio_9p_host_fixture` alone brings the 9p subset into a
  `not(qemu)` build.
- Every 9p code gate becomes `any(feature = "qemu", feature =
  "virtio_9p_host_fixture")`. Bucket B stays `#[cfg(feature = "qemu")]`.
- **Writable 9p stays opt-in and fail-closed.** The manifest gate is unchanged:
  `KernelCapSource::Virtio9pRootWritable` binds only when either `qemu` or
  `virtio_9p_host_fixture` is compiled in, and naming
  `virtio_9p_root_writable` in the manifest remains the sole write opt-in (no
  runtime upgrade of a read-only cap). In a build with **neither** feature,
  both `virtio_9p_root` and `virtio_9p_root_writable` resolve to a
  ``kernel source ... requires ...`` refusal — the fixture is never a production
  authority. `capos_config::virtio_9p_writable_source_refusal` keeps rejecting
  the writable source in non-fixture manifests. The current refusal string names
  only the `qemu` feature; the implementation slice broadens the wording (and
  the refusal helper's build-condition input) to name `qemu` **or**
  `virtio_9p_host_fixture`, so the diagnostic stays accurate in the new build
  shape.
- The feature must **not** be a default and must **not** be implied by any
  cloud/production feature; it is dev/proof infrastructure, selected explicitly
  by the co-boot proof target only.

### Feature matrix

| Build | virtio-9p read-only | virtio-9p writable | Phase C userspace net | Full DDF (`qemu_full`) |
|---|---|---|---|---|
| default (neither) | fail closed | fail closed | no | no (stub) |
| `qemu` | yes (kernel fixture) | yes (opt-in manifest) | no (`compile_error!` on the NIC chain) | yes |
| Phase C net only (`cloud_virtio_net_userspace_sustained_receive_pool_proof`) | fail closed | fail closed | yes | no (stub) |
| fixture only (`virtio_9p_host_fixture`) | yes | yes (opt-in manifest) | no | no (stub) |
| **net + fixture** (`…sustained_receive_pool_proof, virtio_9p_host_fixture`) | **yes** | **yes (opt-in manifest)** | **yes** | no (stub) |

The last row is the target milestone cell. The implementation builds all five
matrix cells and `make test-virtio-9p-net-coboot` boots the last row, proving the
Phase C `Nic` path and a writable virtio-9p `Directory` both serve in one
`not(qemu)` boot (host-verified). The `qemu` row still cannot host the NIC chain
— that guard is intentional and untouched.

### Invariants (device identity, DMA, teardown, single-owner)

- **Device identity.** virtio-9p is a distinct PCI device
  (`VIRTIO_9P_MODERN_DEVICE_ID` `0x1049` / transitional `0x1009`), selected by
  `pci::is_virtio_9p`, and claimed for `DeviceOwner::Virtio9p` via
  `device_manager::claim_pci_function`. Because it is a different BDF from
  virtio-net, no ownership arbitration against the userspace-NIC chain is
  needed; the fixture must still refuse to bind if the 9p function is absent
  (`pci: virtio-9p device not found` → grant source fails closed).
- **DMA.** Unchanged from the landed fixture: kernel-owned, single-request-queue
  bounce accounting through the `device_dma` `VIRTIO_9P_DMA_POOL`
  (generation-checked page handles, scrub-before-free, five-page budget). This
  is the settled DMA-isolation backend (`../dma-isolation-design.md`); the
  composable feature introduces **no new DMA backend** and does not touch VT-d
  (`iommu.rs` stays `qemu`-only and unreferenced by 9p). The
  `DmaRemappingRequesterBinding` threaded into the driver is descriptive
  requester identity, not a remapping-table owner.
- **Teardown.** Unchanged: the driver's reset-before-free `Drop` quiesces the
  device before its DMA frames are freed. The composable build compiles the
  same driver, so the same teardown path applies.
- **Single owner.** The writable root remains single-writer, non-transferable,
  minted at most once per boot through the qemu/fixture-gated raw spawn grant;
  read-only results retain same-session copy delegation. The composable feature
  changes only *when the code compiles*, never the grant/ownership semantics.

### Verification gates

- **Design record:** `make workflow-check`, `make docs`, and
  `scripts/check-md-links.py` cover the dependency-cut documentation.
- **Implemented extraction:** the existing 9p proofs
  (`make test-virtio-9p-bringup`, `make test-virtio-9p-fs`,
  `make test-virtio-9p-write`) cover the unchanged full-`qemu` path. All five
  feature-matrix rows build, including the negative default and Phase C-only
  configurations. The co-boot proof builds
  `--features cloud_virtio_net_userspace_sustained_receive_pool_proof,
  virtio_9p_host_fixture`, boots with both a virtio-9p device and the userspace
  network stack, and asserts the typed `Nic` path and a writable 9p `Directory`
  both serve in one boot. Task-coordinator persistence and the HTTP/JSON task API
  remain the dependent `task-backend-9p-api-persistence` task's scope. The
  feature/cfg audit and proof also establish that this path neither compiles nor
  invokes the real QEMU
  net/blk/rng drivers, IOMMU diagnostic, or DDF proof entry points, while the
  kernel network façade continues to return its non-QEMU typed-negative
  results. `make test-virtio-9p-net-coboot` is the Makefile smoke target for
  that cell, and `docs/devices/virtio-9p.md` records the composable build path.

### Implemented extraction

The bounded implementation ([`virtio-9p-composable-fixture-implementation`](https://tasks.cap-os.dev/p/capos/t/virtio-9p-composable-fixture-implementation))
adds (1) the `virtio_9p_host_fixture` feature; (2) the independent
non-`qemu` 9p PCI startup route; (3) re-gate the Bucket A edges to
`any(qemu, virtio_9p_host_fixture)` and extract `claim_pci_function` +
`attach_dmapool_record_with_remapping` into the `not(qemu)`-reachable
`device_manager` surface; (4) split/gate `virtio.rs` so the fixture build keeps
the non-QEMU typed-negative network façade while excluding the real net/blk/rng
drivers and the `prove_qemu_*` harness; (5) confirm Bucket B stays `qemu`-only;
(6) build all five matrix rows and add the co-boot proof plus its Makefile smoke
entry; and (7) refresh `docs/devices/virtio-9p.md`.
