Commit cfb6c95706 for qemu.org

commit cfb6c95706998be9a6606d80e03f088c0f5b7b42
Author: mr-083 <matthieu@minio.io>
Date:   Wed Apr 15 19:38:52 2026 +0200

    hw/nvme: add namespace hotplug support

    Add hotplug support for nvme-ns devices on the NvmeBus. This enables
    NVMe namespace-level hot-add and hot-remove via device_add and
    device_del with proper Asynchronous Event Notification (AEN), so the
    guest kernel can react to namespace topology changes.

    Mark nvme-ns devices as hotpluggable and register the NvmeBus as a
    hotplug handler with proper plug and unplug callbacks:

    - plug: attach namespace to all started controllers and send an
      Asynchronous Event Notification (AEN) with NS_ATTR_CHANGED so
      the guest kernel rescans namespaces and adds the block device
    - unplug: drain in-flight I/O, detach from all controllers, send
      AEN, then unrealize the device. The guest kernel rescans and
      removes the block device.

    The plug handler skips controllers that haven't started yet
    (qs_created == false) to avoid interfering with boot-time namespace
    attachment in nvme_start_ctrl().

    The unplug handler drains in-flight I/O via nvme_ns_drain() before
    detaching the namespace from controllers, so pending requests can
    complete normally without touching freed state.

    For symmetry with nvme_ns_realize() which sets subsys->namespaces[nsid],
    nvme_ns_unrealize() now clears that slot too making the namespace
    lifecycle complete.

    Both the controller bus and subsystem bus are configured as hotplug
    handlers via qbus_set_bus_hotplug_handler() since nvme-ns devices
    may reparent to the subsystem bus during realize.

    Example hot-swap sequence using the NVMe subsystem model:

      # Boot with: -device nvme-subsys,id=subsys0
      #            -device nvme,id=ctrl0,subsys=subsys0
      #            -device nvme-ns,id=ns0,drive=drv0,bus=ctrl0,nsid=1

      device_del ns0             # guest receives AEN, removes /dev/nvme0n1
      drive_del drv0
      drive_add 0 file=disk.qcow2,format=qcow2,id=drv0,if=none
      device_add nvme-ns,id=ns0,drive=drv0,bus=ctrl0,nsid=1
                                  # guest receives AEN, adds /dev/nvme0n1

    Tested with Linux 6.1 guest (NVMe driver processes AEN and rescans
    namespace list automatically).

    Signed-off-by: Matthieu <matthieu@min.io>
    Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
    Reviewed-by: Klaus Jensen <k.jensen@samsung.com>
    Signed-off-by: Klaus Jensen <k.jensen@samsung.com>

diff --git a/hw/nvme/ctrl.c b/hw/nvme/ctrl.c
index 1fa3a24f76..a67e159889 100644
--- a/hw/nvme/ctrl.c
+++ b/hw/nvme/ctrl.c
@@ -9634,6 +9634,7 @@ static void nvme_realize(PCIDevice *pci_dev, Error **errp)
     }

     qbus_init(&n->bus, sizeof(NvmeBus), TYPE_NVME_BUS, dev, dev->id);
+    qbus_set_bus_hotplug_handler(BUS(&n->bus));

     if (nvme_init_subsys(n, errp)) {
         return;
@@ -10571,10 +10572,96 @@ static const TypeInfo nvme_info = {
     },
 };

+static void nvme_ns_hot_plug(HotplugHandler *hotplug_dev, DeviceState *dev,
+                              Error **errp)
+{
+    NvmeNamespace *ns = NVME_NS(dev);
+    NvmeSubsystem *subsys = ns->subsys;
+    uint32_t nsid = ns->params.nsid;
+    int i;
+
+    /*
+     * Attach to all started controllers and notify via AEN.
+     * Skip controllers that haven't started yet (boot-time realize) —
+     * nvme_start_ctrl() will attach namespaces during controller init.
+     */
+    for (i = 0; i < NVME_MAX_CONTROLLERS; i++) {
+        NvmeCtrl *ctrl = nvme_subsys_ctrl(subsys, i);
+        if (!ctrl || !ctrl->qs_created) {
+            continue;
+        }
+
+        if (nvme_csi_supported(ctrl, ns->csi) && !ns->params.detached) {
+            nvme_attach_ns(ctrl, ns);
+            nvme_update_dsm_limits(ctrl, ns);
+
+            if (!test_and_set_bit(nsid, ctrl->changed_nsids)) {
+                nvme_enqueue_event(ctrl, NVME_AER_TYPE_NOTICE,
+                                   NVME_AER_INFO_NOTICE_NS_ATTR_CHANGED,
+                                   NVME_LOG_CHANGED_NSLIST);
+            }
+        }
+    }
+}
+
+static void nvme_ns_hot_unplug(HotplugHandler *hotplug_dev, DeviceState *dev,
+                               Error **errp)
+{
+    NvmeNamespace *ns = NVME_NS(dev);
+    NvmeSubsystem *subsys = ns->subsys;
+    uint32_t nsid = ns->params.nsid;
+    int i;
+
+    /*
+     * Drain in-flight I/O before tearing down the namespace.
+     * This must happen while the namespace is still attached to the
+     * controllers so any pending requests can complete normally.
+     */
+    nvme_ns_drain(ns);
+
+    /*
+     * Detach from all controllers and notify the guest via AEN.
+     * The guest kernel will rescan namespaces and remove the block device.
+     */
+    for (i = 0; i < NVME_MAX_CONTROLLERS; i++) {
+        NvmeCtrl *ctrl = nvme_subsys_ctrl(subsys, i);
+        if (!ctrl || !nvme_ns(ctrl, nsid)) {
+            continue;
+        }
+
+        nvme_detach_ns(ctrl, ns);
+        nvme_update_dsm_limits(ctrl, NULL);
+
+        if (!test_and_set_bit(nsid, ctrl->changed_nsids)) {
+            nvme_enqueue_event(ctrl, NVME_AER_TYPE_NOTICE,
+                               NVME_AER_INFO_NOTICE_NS_ATTR_CHANGED,
+                               NVME_LOG_CHANGED_NSLIST);
+        }
+    }
+
+    /*
+     * Unrealize: removes from subsystem (in nvme_ns_unrealize), flushes,
+     * cleans up structures, and removes from QOM.
+     */
+    qdev_unrealize(dev);
+}
+
+static void nvme_bus_class_init(ObjectClass *klass, const void *data)
+{
+    HotplugHandlerClass *hc = HOTPLUG_HANDLER_CLASS(klass);
+    hc->plug = nvme_ns_hot_plug;
+    hc->unplug = nvme_ns_hot_unplug;
+}
+
 static const TypeInfo nvme_bus_info = {
     .name = TYPE_NVME_BUS,
     .parent = TYPE_BUS,
     .instance_size = sizeof(NvmeBus),
+    .class_init = nvme_bus_class_init,
+    .interfaces = (const InterfaceInfo[]) {
+        { TYPE_HOTPLUG_HANDLER },
+        { }
+    },
 };

 static void nvme_register_types(void)
diff --git a/hw/nvme/ns.c b/hw/nvme/ns.c
index 4caab59097..7f0f9ac766 100644
--- a/hw/nvme/ns.c
+++ b/hw/nvme/ns.c
@@ -720,10 +720,17 @@ void nvme_ns_cleanup(NvmeNamespace *ns)
 static void nvme_ns_unrealize(DeviceState *dev)
 {
     NvmeNamespace *ns = NVME_NS(dev);
+    NvmeSubsystem *subsys = ns->subsys;
+    uint32_t nsid = ns->params.nsid;

     nvme_ns_drain(ns);
     nvme_ns_shutdown(ns);
     nvme_ns_cleanup(ns);
+
+    /* Symmetric with nvme_ns_realize() which sets subsys->namespaces[nsid]. */
+    if (subsys && nsid && subsys->namespaces[nsid] == ns) {
+        subsys->namespaces[nsid] = NULL;
+    }
 }

 void nvme_ns_atomic_configure_boundary(bool dn, uint16_t nabsn,
@@ -1100,6 +1107,7 @@ static void nvme_ns_class_init(ObjectClass *oc, const void *data)
     dc->bus_type = TYPE_NVME_BUS;
     dc->realize = nvme_ns_realize;
     dc->unrealize = nvme_ns_unrealize;
+    dc->hotpluggable = true;
     dc->vmsd = &nvme_vmstate_ns;
     device_class_set_props(dc, nvme_ns_props);
     dc->desc = "Virtual NVMe namespace";
diff --git a/hw/nvme/subsys.c b/hw/nvme/subsys.c
index 777e1c620f..fa35055d3c 100644
--- a/hw/nvme/subsys.c
+++ b/hw/nvme/subsys.c
@@ -9,6 +9,7 @@
 #include "qemu/osdep.h"
 #include "qemu/units.h"
 #include "qapi/error.h"
+#include "hw/core/qdev.h"

 #include "nvme.h"

@@ -205,6 +206,7 @@ static void nvme_subsys_realize(DeviceState *dev, Error **errp)
     NvmeSubsystem *subsys = NVME_SUBSYS(dev);

     qbus_init(&subsys->bus, sizeof(NvmeBus), TYPE_NVME_BUS, dev, dev->id);
+    qbus_set_bus_hotplug_handler(BUS(&subsys->bus));

     nvme_subsys_setup(subsys, errp);
 }