From fe69e8e47759b9d7eca3976041310cf094f5f977 Mon Sep 17 00:00:00 2001 From: Dan Berkowitz Date: Fri, 21 Aug 2026 09:45:45 -0400 Subject: [PATCH] nvme: fix controller init for Windows Server 2025 / Windows 11 24H2 The rewritten stornvme in Windows Server 2025 (26100) and Windows 11 24H2 could not see any NVMe disk; Setup reports "a media driver your computer needs is missing" while Server 2019/2022 and Windows 10 install fine. Two defects in the NVMe emulation combine to cause this: 1. AQA/ASQ/ACQ reads returned nothing while the controller was enabled. The spec restricts *writes* to these registers while CC.EN=1, but reads must always return the last-written value. stornvme reads ASQ back right after its first Identify Controller completes and treats the zero readback as a fatal adapter error (CM_PROB_FAILED_START, STATUS_ADAPTER_HARDWARE_ERROR), tearing the device down after exactly one successful admin command. Make the reads unconditional. 2. Async Event Requests were completed with Invalid Opcode + DNR, on the rationale that QEMU does the same; that comment is stale (modern QEMU implements AER). stornvme ignores DNR and immediately resubmits, producing a permanent ~8k cmd/s admin loop during which boot of the installed OS never proceeds. Park AER permits, retaining them until controller reset, as the spec prescribes; no events are ever posted. Verified on bhyve/Helios: Server 2025 installs end-to-end to the logon screen (4 parked AERs per controller at steady state), Windows 11 installs to OOBE, and Server 2022 still installs on the same build. Known limitation: parked AER permits are not preserved across live migration export/import. --- lib/propolis/src/hw/nvme/mod.rs | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/propolis/src/hw/nvme/mod.rs b/lib/propolis/src/hw/nvme/mod.rs index a9360383f..762a00a1c 100644 --- a/lib/propolis/src/hw/nvme/mod.rs +++ b/lib/propolis/src/hw/nvme/mod.rs @@ -200,6 +200,10 @@ struct NvmeCtrl { /// Doorbell Buffer Config state doorbell_buf: Option, + /// Async Event Requests parked until an event is posted or the + /// controller is reset (see the AsyncEventReq admin command handling). + parked_aers: Vec, + /// MSI-X Interrupt Handle to signal VM msix_hdl: Option, @@ -539,6 +543,12 @@ impl NvmeCtrl { self.ctrl.cc = Configuration(0); self.ctrl.csts = Status(0); + // Parked Async Event Requests are implicitly aborted by the reset; + // their queues are already torn down so there is nothing to complete. + for permit in self.parked_aers.drain(..) { + permit.abandon(); + } + // Other bits which are cleared on reset self.doorbell_buf = None; @@ -925,6 +935,7 @@ impl PciNvme { device_id: DeviceId::new(), ctrl: CtrlState { cap, vs, cc, csts, ..Default::default() }, doorbell_buf: None, + parked_aers: Vec::new(), msix_hdl: None, cqs: Default::default(), sqs: Default::default(), @@ -1044,22 +1055,21 @@ impl PciNvme { ro.write_u32(state.ctrl.csts.0); } CtrlrReg::AdminQueueAttr => { + // These registers may only be modified while the controller + // is disabled, but reads must always return the last value + // written (NVMe 1.0e Section 3.1; e.g. Windows Server 2025's + // stornvme reads ASQ back after enabling the controller and + // treats a mismatch as a fatal adapter error). let state = self.state.lock().unwrap(); - if !state.ctrl.cc.enabled() { - ro.write_u32(state.ctrl.aqa.0); - } + ro.write_u32(state.ctrl.aqa.0); } CtrlrReg::AdminSubQAddr => { let state = self.state.lock().unwrap(); - if !state.ctrl.cc.enabled() { - ro.write_u64(state.ctrl.admin_sq_base); - } + ro.write_u64(state.ctrl.admin_sq_base); } CtrlrReg::AdminCompQAddr => { let state = self.state.lock().unwrap(); - if !state.ctrl.cc.enabled() { - ro.write_u64(state.ctrl.admin_cq_base); - } + ro.write_u64(state.ctrl.admin_cq_base); } CtrlrReg::Reserved => { ro.fill(0); @@ -1361,16 +1371,14 @@ impl PciNvme { state.acmd_delete_io_sq(sqid, self) } AdminCmd::AsyncEventReq => { - // async event requests do not appear to be an optional - // feature but are not yet supported. The only - // command-specific error we could return is "async event - // limit exceeded". - // - // qemu's emulated NVMe also does not support async events - // but returns invalid opcode with the do-not-retry flag - // set. Do the same so that guest drivers that check for - // this can detect it and stop posting async events. - cmds::Completion::generic_err(bits::STS_INVAL_OPC).dnr() + // Async Event Requests remain outstanding until an event + // occurs (which we never post) or the controller is + // reset; they do not receive an immediate completion. + // Completing them with an error instead sends some + // guests (e.g. Windows Server 2025's stornvme) into a + // tight resubmit loop, despite the do-not-retry flag. + state.parked_aers.push(permit); + continue; } AdminCmd::DoorbellBufCfg(cmd) => { state.acmd_doorbell_buf_cfg(&cmd)