From 882049362d2967c40ba22956cd24b3d3b09e98df Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 16 Apr 2026 22:44:58 +0200 Subject: [PATCH 01/19] vmspawn: add QmpClient userdata and VmspawnQmpBridge.setup_done flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add qmp_client_set_userdata()/qmp_client_get_userdata() accessors mirroring the sd_varlink API, and wire up the VmspawnQmpBridge as userdata on the QmpClient so that command callbacks can retrieve it. Add a setup_done flag to VmspawnQmpBridge, set by on_cont_complete() when the VM has booted and all boot-time device setup is finished. This lets command callbacks differentiate boot-time errors (fatal — exit event loop) from runtime errors (recoverable — log and continue). Signed-off-by: Christian Brauner (Amutable) --- src/shared/qmp-client.c | 17 +++++++++++++++++ src/shared/qmp-client.h | 3 +++ src/vmspawn/vmspawn-qmp.c | 4 ++++ src/vmspawn/vmspawn-qmp.h | 1 + src/vmspawn/vmspawn-varlink.c | 1 + 5 files changed, 26 insertions(+) diff --git a/src/shared/qmp-client.c b/src/shared/qmp-client.c index cc51259cd1..ad8f8bb24a 100644 --- a/src/shared/qmp-client.c +++ b/src/shared/qmp-client.c @@ -49,6 +49,8 @@ struct QmpClient { QmpClientState state; sd_json_variant *current; /* most recently parsed message, pending dispatch */ + + void *userdata; }; static void qmp_slot_hash_func(const QmpSlot *p, struct siphash *state) { @@ -508,6 +510,21 @@ bool qmp_client_is_disconnected(QmpClient *c) { return c->state == QMP_CLIENT_DISCONNECTED; } +void* qmp_client_set_userdata(QmpClient *c, void *userdata) { + void *old; + + assert(c); + + old = c->userdata; + c->userdata = userdata; + return old; +} + +void* qmp_client_get_userdata(QmpClient *c) { + assert(c); + return c->userdata; +} + /* Map our state to the transport phase used for POLLIN / salvage / timeout decisions. */ static JsonStreamPhase qmp_client_phase(void *userdata) { QmpClient *c = ASSERT_PTR(userdata); diff --git a/src/shared/qmp-client.h b/src/shared/qmp-client.h index 7a477f7e4f..8f78473128 100644 --- a/src/shared/qmp-client.h +++ b/src/shared/qmp-client.h @@ -54,6 +54,9 @@ bool qmp_client_is_idle(QmpClient *c); /* True iff the connection is dead. Stable terminal state — once set, it stays set. */ bool qmp_client_is_disconnected(QmpClient *c); +void* qmp_client_set_userdata(QmpClient *c, void *userdata); +void* qmp_client_get_userdata(QmpClient *c); + /* Async send. Returns 0 on send (callback will fire later), negative errno on failure. If * ret_slot is non-NULL, returns a reference to a QmpSlot which can be used to cancel the call * (by unreffing it before the reply arrives). */ diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index f69d2a5e7c..f0f987082c 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -1042,6 +1042,8 @@ static int on_cont_complete( int error, void *userdata) { + VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); + assert(client); if (error < 0) { @@ -1049,6 +1051,8 @@ static int on_cont_complete( return sd_event_exit(qmp_client_get_event(client), error); } + /* VM is running — all boot-time device setup has completed. */ + bridge->setup_done = true; return 0; } diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 8f8c26fb03..15949c40a5 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -28,6 +28,7 @@ typedef struct VmspawnQmpBridge { QmpClient *qmp; Hashmap *pending_jobs; /* job_id (string, owned) -> PendingJob* */ VmspawnQmpFeatureFlags features; + bool setup_done; } VmspawnQmpBridge; VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b); diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 4a11b6fd4e..98d3fc73e9 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -387,6 +387,7 @@ int vmspawn_varlink_setup( ctx->bridge = bridge; qmp_client_bind_event(ctx->bridge->qmp, on_qmp_event, ctx); qmp_client_bind_disconnect(ctx->bridge->qmp, on_qmp_disconnect, ctx); + qmp_client_set_userdata(ctx->bridge->qmp, ctx->bridge); log_debug("Varlink control server listening on %s", listen_address); From a5afc0f1071b6e371eda486064bc295f5289f67c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 16 Apr 2026 23:17:25 +0200 Subject: [PATCH 02/19] vmspawn: rename on_qmp_setup_complete() to on_qmp_complete() Pure rename, no functional change. Prepares for making this callback handle both boot-time (fatal) and runtime (non-fatal) errors based on the bridge setup_done flag. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index f0f987082c..32af5e4e3e 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -85,7 +85,7 @@ void machine_config_done(MachineConfig *c) { /* Generic async QMP setup-completion callback. The userdata argument carries the * command name (as a string literal) for logging. On failure, request a clean * event loop exit so vmspawn shuts down instead of running a VM with missing devices. */ -static int on_qmp_setup_complete( +static int on_qmp_complete( QmpClient *client, sd_json_variant *result, const char *error_desc, @@ -127,7 +127,7 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { return -ENOMEM; r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "add-fd", QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd)), - on_qmp_setup_complete, (void*) "add-fd"); + on_qmp_complete, (void*) "add-fd"); if (r < 0) return r; @@ -221,7 +221,7 @@ static int qmp_add_file_node(QmpClient *qmp, const QmpFileNodeParams *p) { if (r < 0) return r; - return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), on_qmp_setup_complete, (void*) "blockdev-add"); + return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), on_qmp_complete, (void*) "blockdev-add"); } /* Get the virtual size of an image from the fd directly. For raw images the virtual size @@ -320,7 +320,7 @@ static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { if (r < 0) return log_error_errno(r, "Failed to build overlay format JSON for '%s': %m", ctx->node_name); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return r; @@ -339,7 +339,7 @@ static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { if (r < 0) return log_error_errno(r, "Failed to build device_add JSON for '%s': %m", ctx->node_name); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return r; @@ -408,7 +408,7 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(base_fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(base_fmt_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for base format '%s': %m", drive->path); @@ -424,7 +424,7 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(overlay_file_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(overlay_file_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for overlay file '%s': %m", drive->path); @@ -482,7 +482,7 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D TAKE_PTR(ectx); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-create", QMP_CLIENT_ARGS(cmd_args), on_qmp_setup_complete, (void*) "blockdev-create"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-create", QMP_CLIENT_ARGS(cmd_args), on_qmp_complete, (void*) "blockdev-create"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-create for '%s': %m", drive->path); @@ -532,7 +532,7 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_setup_complete, (void*) "blockdev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return log_error_errno(r, "Failed to send blockdev-add format for '%s': %m", drive->path); @@ -542,7 +542,7 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send device_add for '%s': %m", drive->path); @@ -585,7 +585,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { return log_error_errno(r, "Failed to build getfd JSON: %m"); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "getfd", QMP_CLIENT_ARGS_FD(getfd_args, TAKE_FD(network->fd)), - on_qmp_setup_complete, (void*) "getfd"); + on_qmp_complete, (void*) "getfd"); if (r < 0) return log_error_errno(r, "Failed to send getfd for TAP fd: %m"); } @@ -606,7 +606,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { if (r < 0) return log_error_errno(r, "Failed to build netdev_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "netdev_add", QMP_CLIENT_ARGS(netdev_args), on_qmp_setup_complete, (void*) "netdev_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "netdev_add", QMP_CLIENT_ARGS(netdev_args), on_qmp_complete, (void*) "netdev_add"); if (r < 0) return log_error_errno(r, "Failed to send netdev_add: %m"); @@ -623,7 +623,7 @@ int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { if (r < 0) return log_error_errno(r, "Failed to build NIC device_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send NIC device_add: %m"); @@ -658,7 +658,7 @@ static int vmspawn_qmp_setup_one_virtiofs(QmpClient *qmp, const VirtiofsInfo *vf if (r < 0) return log_error_errno(r, "Failed to build chardev-add JSON for '%s': %m", vfs->id); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "chardev-add", QMP_CLIENT_ARGS(chardev_args), on_qmp_setup_complete, (void*) "chardev-add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "chardev-add", QMP_CLIENT_ARGS(chardev_args), on_qmp_complete, (void*) "chardev-add"); if (r < 0) return log_error_errno(r, "Failed to send chardev-add '%s': %m", vfs->id); @@ -675,7 +675,7 @@ static int vmspawn_qmp_setup_one_virtiofs(QmpClient *qmp, const VirtiofsInfo *vf if (r < 0) return log_error_errno(r, "Failed to build virtiofs device_add JSON for '%s': %m", vfs->id); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send virtiofs device_add '%s': %m", vfs->id); @@ -720,7 +720,7 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { return log_error_errno(r, "Failed to build getfd JSON for VSOCK: %m"); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "getfd", QMP_CLIENT_ARGS_FD(getfd_args, TAKE_FD(vsock->fd)), - on_qmp_setup_complete, (void*) "getfd"); + on_qmp_complete, (void*) "getfd"); if (r < 0) return log_error_errno(r, "Failed to send getfd for VSOCK fd: %m"); @@ -736,7 +736,7 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { if (r < 0) return log_error_errno(r, "Failed to build VSOCK device_add JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send VSOCK device_add: %m"); @@ -766,7 +766,7 @@ static int qmp_setup_scsi_controller(QmpClient *qmp, const char *pcie_port) { if (r < 0) return log_error_errno(r, "Failed to build SCSI controller JSON: %m"); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), on_qmp_setup_complete, (void*) "device_add"); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), on_qmp_complete, (void*) "device_add"); if (r < 0) return log_error_errno(r, "Failed to send SCSI controller device_add: %m"); @@ -910,7 +910,7 @@ static int on_io_uring_probe_add_reply( return r; return qmp_client_invoke(c, /* ret_slot= */ NULL, "blockdev-del", QMP_CLIENT_ARGS(del_args), - on_io_uring_probe_del_reply, bridge); + on_io_uring_probe_del_reply, bridge); } static int probe_io_uring(QmpClient *c, VmspawnQmpBridge *bridge) { @@ -931,7 +931,7 @@ static int probe_io_uring(QmpClient *c, VmspawnQmpBridge *bridge) { return r; return qmp_client_invoke(c, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(args), - on_io_uring_probe_add_reply, bridge); + on_io_uring_probe_add_reply, bridge); } static int on_probe_schema_reply( @@ -966,7 +966,7 @@ static int probe_schema(QmpClient *c, VmspawnQmpBridge *bridge) { assert(bridge); return qmp_client_invoke(c, /* ret_slot= */ NULL, "query-qmp-schema", QMP_CLIENT_ARGS(NULL), - on_probe_schema_reply, bridge); + on_probe_schema_reply, bridge); } int vmspawn_qmp_init(VmspawnQmpBridge **ret, int fd, sd_event *event) { From c697634ae220a84a8fcbb103bfcb812f31cdafd9 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 17 Apr 2026 00:23:20 +0200 Subject: [PATCH 03/19] vmspawn: heap-allocate each DriveInfo individually Change DriveInfos from a contiguous array of DriveInfo structs to an array of pointers to individually heap-allocated entries. Make all DriveInfo string fields owned (strdup'd) and add drive_info_new()/ drive_info_free() constructors matching the cleanup pattern. This prepares for the block device hotplug work where drives need to be handed off to a hashmap via TAKE_PTR without copying. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 38 +++++++++++++++++------ src/vmspawn/vmspawn-qmp.h | 26 +++++++++------- src/vmspawn/vmspawn.c | 65 ++++++++++++++++++++++----------------- 3 files changed, 80 insertions(+), 49 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 32af5e4e3e..d467d68a89 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -25,19 +25,37 @@ DEFINE_PRIVATE_HASH_OPS_FULL( char, string_hash_func, string_compare_func, free, PendingJob, pending_job_free); -void drive_info_done(DriveInfo *info) { - assert(info); - info->serial = mfree(info->serial); - info->node_name = mfree(info->node_name); - info->pcie_port = mfree(info->pcie_port); - info->fd = safe_close(info->fd); - info->overlay_fd = safe_close(info->overlay_fd); +DriveInfo* drive_info_new(void) { + DriveInfo *d = new(DriveInfo, 1); + if (!d) + return NULL; + + *d = (DriveInfo) { + .fd = -EBADF, + .overlay_fd = -EBADF, + }; + return d; +} + +DriveInfo* drive_info_free(DriveInfo *d) { + if (!d) + return NULL; + + free(d->path); + free(d->format); + free(d->disk_driver); + free(d->serial); + free(d->node_name); + free(d->pcie_port); + safe_close(d->fd); + safe_close(d->overlay_fd); + return mfree(d); } void drive_infos_done(DriveInfos *infos) { assert(infos); FOREACH_ARRAY(d, infos->drives, infos->n_drives) - drive_info_done(d); + drive_info_free(*d); infos->drives = mfree(infos->drives); infos->n_drives = 0; infos->scsi_pcie_port = mfree(infos->scsi_pcie_port); @@ -748,7 +766,7 @@ static bool drives_need_scsi_controller(DriveInfos *drives) { assert(drives); FOREACH_ARRAY(d, drives->drives, drives->n_drives) - if (STR_IN_SET(d->disk_driver, "scsi-hd", "scsi-cd")) + if (STR_IN_SET((*d)->disk_driver, "scsi-hd", "scsi-cd")) return true; return false; @@ -792,7 +810,7 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives) { } FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - r = qmp_setup_drive(bridge, qmp, d); + r = qmp_setup_drive(bridge, qmp, *d); if (r < 0) return r; } diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 15949c40a5..35cc029ea7 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -65,23 +65,27 @@ typedef enum QmpDriveFlags { QMP_DRIVE_DISCARD_NO_UNREF = 1u << 6, /* qcow2 only */ } QmpDriveFlags; -/* Drive info for QMP-based drive setup */ +/* Drive info for QMP-based drive setup. All string fields are owned. + * Each DriveInfo is individually heap-allocated so it can be handed off + * to the block device registry via TAKE_PTR. */ typedef struct DriveInfo { - const char *path; /* kept for logging only — not passed to QEMU */ - const char *format; /* "raw" or "qcow2" */ - const char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ - char *serial; /* owned */ - char *node_name; /* owned */ - char *pcie_port; /* owned: pcie-root-port id for device_add bus (NULL on non-PCIe) */ - int fd; /* pre-opened image fd (owned, -EBADF if unused) */ - int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (owned, -EBADF if unused) */ + char *path; /* original path (for logging; not passed to QEMU) */ + char *format; /* "raw" or "qcow2" */ + char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ + char *serial; + char *node_name; + char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ + int fd; /* pre-opened image fd (-EBADF if unused) */ + int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-EBADF if unused) */ QmpDriveFlags flags; } DriveInfo; -void drive_info_done(DriveInfo *info); +DriveInfo* drive_info_new(void); +DriveInfo* drive_info_free(DriveInfo *d); +DEFINE_TRIVIAL_CLEANUP_FUNC(DriveInfo *, drive_info_free); typedef struct DriveInfos { - DriveInfo *drives; + DriveInfo **drives; /* array of individually heap-allocated entries */ size_t n_drives; char *scsi_pcie_port; /* owned: pcie-root-port id for SCSI controller (NULL if no SCSI or non-PCIe) */ } DriveInfos; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index e0ddcee9b1..01325bf9eb 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2303,6 +2303,8 @@ static int qemu_config_add_qmp_monitor(FILE *config_file, int bridge_fds[2], int } static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *info) { + const char *driver; + size_t serial_max; int r; assert(filename); @@ -2310,34 +2312,34 @@ static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *inf switch (dt) { case DISK_TYPE_VIRTIO_BLK: - info->disk_driver = "virtio-blk-pci"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_VIRTIO_BLK, &info->serial); - if (r < 0) - return r; + driver = "virtio-blk-pci"; + serial_max = DISK_SERIAL_MAX_LEN_VIRTIO_BLK; break; case DISK_TYPE_VIRTIO_SCSI: - info->disk_driver = "scsi-hd"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_SCSI, &info->serial); - if (r < 0) - return r; + driver = "scsi-hd"; + serial_max = DISK_SERIAL_MAX_LEN_SCSI; break; case DISK_TYPE_NVME: - info->disk_driver = "nvme"; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_NVME, &info->serial); - if (r < 0) - return r; + driver = "nvme"; + serial_max = DISK_SERIAL_MAX_LEN_NVME; break; case DISK_TYPE_VIRTIO_SCSI_CDROM: - info->disk_driver = "scsi-cd"; + driver = "scsi-cd"; + serial_max = DISK_SERIAL_MAX_LEN_SCSI; info->flags |= QMP_DRIVE_READ_ONLY; - r = disk_serial(filename, DISK_SERIAL_MAX_LEN_SCSI, &info->serial); - if (r < 0) - return r; break; default: assert_not_reached(); } + info->disk_driver = strdup(driver); + if (!info->disk_driver) + return log_oom(); + + r = disk_serial(filename, serial_max, &info->serial); + if (r < 0) + return r; + return 0; } @@ -2355,8 +2357,9 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { if (r < 0) return log_error_errno(r, "Failed to extract filename from path '%s': %m", arg_image); - DriveInfo *d = &drives->drives[drives->n_drives++]; - *d = (DriveInfo) { .fd = -EBADF, .overlay_fd = -EBADF }; + _cleanup_(drive_info_freep) DriveInfo *d = drive_info_new(); + if (!d) + return log_oom(); r = resolve_disk_driver(arg_image_disk_type, image_fn, d); if (r < 0) @@ -2376,10 +2379,10 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { if (r < 0) return log_error_errno(r, "Expected regular file or block device for image: %s", arg_image); - d->path = arg_image; - d->format = image_format_to_string(arg_image_format); + d->path = strdup(arg_image); + d->format = strdup(ASSERT_PTR(image_format_to_string(arg_image_format))); d->node_name = strdup("vmspawn"); - if (!d->node_name) + if (!d->path || !d->format || !d->node_name) return log_oom(); d->fd = TAKE_FD(image_fd); if (S_ISBLK(st.st_mode)) @@ -2406,6 +2409,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { d->flags |= QMP_DRIVE_NO_FLUSH; } + drives->drives[drives->n_drives++] = TAKE_PTR(d); return 0; } @@ -2423,8 +2427,9 @@ static int prepare_extra_drives(DriveInfos *drives) { DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type; - DriveInfo *d = &drives->drives[drives->n_drives++]; - *d = (DriveInfo) { .fd = -EBADF, .overlay_fd = -EBADF }; + _cleanup_(drive_info_freep) DriveInfo *d = drive_info_new(); + if (!d) + return log_oom(); r = resolve_disk_driver(dt, drive_fn, d); if (r < 0) @@ -2445,8 +2450,10 @@ static int prepare_extra_drives(DriveInfos *drives) { "Block device '%s' cannot be used with 'qcow2' format, only 'raw' is supported.", drive->path); - d->path = drive->path; - d->format = image_format_to_string(drive->format); + d->path = strdup(drive->path); + d->format = strdup(ASSERT_PTR(image_format_to_string(drive->format))); + if (!d->path || !d->format) + return log_oom(); d->fd = TAKE_FD(drive_fd); if (S_ISBLK(drive_st.st_mode)) d->flags |= QMP_DRIVE_BLOCK_DEVICE; @@ -2454,6 +2461,8 @@ static int prepare_extra_drives(DriveInfos *drives) { if (asprintf(&d->node_name, "vmspawn_extra_%zu", extra_idx++) < 0) return log_oom(); + + drives->drives[drives->n_drives++] = TAKE_PTR(d); } return 0; @@ -2477,11 +2486,11 @@ static int assign_pcie_ports(MachineConfig *c) { /* Drives: non-SCSI drives get individual ports, SCSI controller gets one port */ bool need_scsi = false; FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - if (STR_IN_SET(d->disk_driver, "scsi-hd", "scsi-cd")) { + if (STR_IN_SET((*d)->disk_driver, "scsi-hd", "scsi-cd")) { need_scsi = true; continue; } - if (asprintf(&d->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) + if (asprintf(&(*d)->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) return log_oom(); } if (need_scsi) @@ -2513,7 +2522,7 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) { /* Build drive info for QMP-based setup. vmspawn opens all image files and * passes fds to QEMU via add-fd — QEMU never needs filesystem access. */ - drives->drives = new0(DriveInfo, 1 + arg_extra_drives.n_drives); + drives->drives = new0(DriveInfo*, 1 + arg_extra_drives.n_drives); if (!drives->drives) return log_oom(); From 258c66784527bdffc8df0a3ad8437926bda5894c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 20 Apr 2026 09:28:54 +0200 Subject: [PATCH 04/19] shared: extract disk-spec parsing into machine-util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the ImageFormat / DiskType enums and their string tables out of vmspawn's private settings header into a new src/shared/machine-util, and add parse_disk_spec() — the colon-prefix loop that turns "[FORMAT:][DISKTYPE:]PATH" into the two enums plus a normalized path. No behavior change for vmspawn. A follow-up machinectl attach-disk change accepts the same syntax and consumes the shared helper. Signed-off-by: Christian Brauner (Amutable) --- src/shared/machine-util.c | 102 +++++++++++++++++++++++++++++++++ src/shared/machine-util.h | 32 +++++++++++ src/shared/meson.build | 1 + src/vmspawn/vmspawn-settings.c | 16 ------ src/vmspawn/vmspawn-settings.h | 19 +----- src/vmspawn/vmspawn.c | 34 +---------- 6 files changed, 138 insertions(+), 66 deletions(-) create mode 100644 src/shared/machine-util.c create mode 100644 src/shared/machine-util.h diff --git a/src/shared/machine-util.c b/src/shared/machine-util.c new file mode 100644 index 0000000000..fa5e46ace1 --- /dev/null +++ b/src/shared/machine-util.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "machine-util.h" +#include "parse-argument.h" +#include "string-table.h" + +static const char *const image_format_table[_IMAGE_FORMAT_MAX] = { + [IMAGE_FORMAT_RAW] = "raw", + [IMAGE_FORMAT_QCOW2] = "qcow2", +}; + +DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat); + +static const char *const disk_type_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio-blk", + [DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType); + +/* Wire value for the io.systemd.VirtualMachineInstance.BlockDriver IDL enum. */ +static const char *const block_driver_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio_blk", + [DISK_TYPE_VIRTIO_SCSI] = "scsi_hd", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi_cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(block_driver, DiskType); + +/* QEMU -device driver name (e.g. "virtio-blk-pci"). */ +static const char *const qemu_device_driver_table[_DISK_TYPE_MAX] = { + [DISK_TYPE_VIRTIO_BLK] = "virtio-blk-pci", + [DISK_TYPE_VIRTIO_SCSI] = "scsi-hd", + [DISK_TYPE_NVME] = "nvme", + [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", +}; + +DEFINE_STRING_TABLE_LOOKUP(qemu_device_driver, DiskType); + +int parse_disk_spec( + const char *arg, + ImageFormat *format, + DiskType *disk_type, + char **ret_path) { + + int r; + + assert(arg); + assert(format); + assert(disk_type); + assert(ret_path); + + ImageFormat parsed_format = *format; + DiskType parsed_disk_type = *disk_type; + const char *dp = arg; + + /* Format and disk-type vocabularies don't overlap, so prefixes may appear in any order. */ + for (;;) { + _cleanup_free_ char *word = NULL; + const char *save = dp; + + r = extract_first_word(&dp, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0 || !dp) { + /* No ':' remained after this word — rest is the path. */ + dp = save; + break; + } + + ImageFormat f = image_format_from_string(word); + if (f >= 0) { + parsed_format = f; + continue; + } + + DiskType dt = disk_type_from_string(word); + if (dt >= 0) { + parsed_disk_type = dt; + continue; + } + + /* Unknown prefix — rewind, remainder is the path. */ + dp = save; + break; + } + + _cleanup_free_ char *path = NULL; + r = parse_path_argument(dp, /* suppress_root= */ false, &path); + if (r < 0) + return r; + + *format = parsed_format; + *disk_type = parsed_disk_type; + *ret_path = TAKE_PTR(path); + return 0; +} diff --git a/src/shared/machine-util.h b/src/shared/machine-util.h new file mode 100644 index 0000000000..3937ce1703 --- /dev/null +++ b/src/shared/machine-util.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +typedef enum ImageFormat { + IMAGE_FORMAT_RAW, + IMAGE_FORMAT_QCOW2, + _IMAGE_FORMAT_MAX, + _IMAGE_FORMAT_INVALID = -EINVAL, +} ImageFormat; + +typedef enum DiskType { + DISK_TYPE_VIRTIO_BLK, + DISK_TYPE_VIRTIO_SCSI, + DISK_TYPE_NVME, + DISK_TYPE_VIRTIO_SCSI_CDROM, + _DISK_TYPE_MAX, + _DISK_TYPE_INVALID = -EINVAL, +} DiskType; + +DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); +DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType); +DECLARE_STRING_TABLE_LOOKUP(block_driver, DiskType); +DECLARE_STRING_TABLE_LOOKUP(qemu_device_driver, DiskType); + +/* Parse "[FORMAT:][DISKTYPE:]PATH"; *format and *disk_type are in-out. */ +int parse_disk_spec( + const char *arg, + ImageFormat *format, + DiskType *disk_type, + char **ret_path); diff --git a/src/shared/meson.build b/src/shared/meson.build index 741dbb60a4..56b823c50c 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -133,6 +133,7 @@ shared_sources = files( 'machine-credential.c', 'machine-id-setup.c', 'machine-register.c', + 'machine-util.c', 'macvlan-util.c', 'main-func.c', 'metrics.c', diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 9382172e2c..502189e7be 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -3,22 +3,6 @@ #include "string-table.h" #include "vmspawn-settings.h" -static const char *const image_format_table[_IMAGE_FORMAT_MAX] = { - [IMAGE_FORMAT_RAW] = "raw", - [IMAGE_FORMAT_QCOW2] = "qcow2", -}; - -DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat); - -static const char *const disk_type_table[_DISK_TYPE_MAX] = { - [DISK_TYPE_VIRTIO_BLK] = "virtio-blk", - [DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi", - [DISK_TYPE_NVME] = "nvme", - [DISK_TYPE_VIRTIO_SCSI_CDROM] = "scsi-cd", -}; - -DEFINE_STRING_TABLE_LOOKUP(disk_type, DiskType); - void extra_drive_context_done(ExtraDriveContext *ctx) { assert(ctx); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index f02b499201..596a66cecd 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -1,24 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "machine-util.h" #include "shared-forward.h" -typedef enum ImageFormat { - IMAGE_FORMAT_RAW, - IMAGE_FORMAT_QCOW2, - _IMAGE_FORMAT_MAX, - _IMAGE_FORMAT_INVALID = -EINVAL, -} ImageFormat; - -typedef enum DiskType { - DISK_TYPE_VIRTIO_BLK, - DISK_TYPE_VIRTIO_SCSI, - DISK_TYPE_NVME, - DISK_TYPE_VIRTIO_SCSI_CDROM, - _DISK_TYPE_MAX, - _DISK_TYPE_INVALID = -EINVAL, -} DiskType; - typedef struct ExtraDrive { char *path; ImageFormat format; @@ -69,6 +54,4 @@ typedef enum SettingsMask { DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport); -DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType); DECLARE_STRING_TABLE_LOOKUP(firmware, Firmware); -DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 01325bf9eb..1b696f7014 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -752,39 +752,9 @@ static int parse_argv(int argc, char *argv[]) { OPTION_LONG("extra-drive", "[FORMAT:][DISKTYPE:]PATH", "Adds an additional disk to the VM"): { ImageFormat format = IMAGE_FORMAT_RAW; DiskType extra_disk_type = _DISK_TYPE_INVALID; - const char *dp = arg; - - /* Parse optional colon-separated prefixes. The format and disk type - * value sets don't overlap, so they can appear in any order. */ - for (;;) { - const char *colon = strchr(dp, ':'); - if (!colon) - break; - - _cleanup_free_ char *prefix = strndup(dp, colon - dp); - if (!prefix) - return log_oom(); - - ImageFormat f = image_format_from_string(prefix); - if (f >= 0) { - format = f; - dp = colon + 1; - continue; - } - - DiskType dt = disk_type_from_string(prefix); - if (dt >= 0) { - extra_disk_type = dt; - dp = colon + 1; - continue; - } - - /* Not a recognized prefix, treat the rest as the path */ - break; - } - _cleanup_free_ char *drive_path = NULL; - r = parse_path_argument(dp, /* suppress_root= */ false, &drive_path); + + r = parse_disk_spec(arg, &format, &extra_disk_type, &drive_path); if (r < 0) return r; From 74a07cb774cd9c8372a5e6868bdfdad9961dbee5 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 20 Apr 2026 11:07:38 +0200 Subject: [PATCH 05/19] vmspawn: use qemu_device_driver_to_string() in resolve_disk_driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the inline DiskType → QEMU device driver switch and call the shared helper instead. serial_max and the CD-ROM read-only flag stay inline since they are vmspawn-local. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 1b696f7014..7ed21af77b 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2273,7 +2273,6 @@ static int qemu_config_add_qmp_monitor(FILE *config_file, int bridge_fds[2], int } static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *info) { - const char *driver; size_t serial_max; int r; @@ -2282,19 +2281,15 @@ static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *inf switch (dt) { case DISK_TYPE_VIRTIO_BLK: - driver = "virtio-blk-pci"; serial_max = DISK_SERIAL_MAX_LEN_VIRTIO_BLK; break; case DISK_TYPE_VIRTIO_SCSI: - driver = "scsi-hd"; serial_max = DISK_SERIAL_MAX_LEN_SCSI; break; case DISK_TYPE_NVME: - driver = "nvme"; serial_max = DISK_SERIAL_MAX_LEN_NVME; break; case DISK_TYPE_VIRTIO_SCSI_CDROM: - driver = "scsi-cd"; serial_max = DISK_SERIAL_MAX_LEN_SCSI; info->flags |= QMP_DRIVE_READ_ONLY; break; @@ -2302,7 +2297,7 @@ static int resolve_disk_driver(DiskType dt, const char *filename, DriveInfo *inf assert_not_reached(); } - info->disk_driver = strdup(driver); + info->disk_driver = strdup(ASSERT_PTR(qemu_device_driver_to_string(dt))); if (!info->disk_driver) return log_oom(); From 4caa73563aca1c46d8da55b800a369d15e3e638c Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:17:21 +0200 Subject: [PATCH 06/19] qmp-client: widen next_fdset_id to uint64_t The fdset id is a monotonic counter; an unsigned int is more than wide enough today, but uint64_t matches the type of other QMP-internal counters (e.g. job ids) and avoids any worry about wraparound on long-running hosts. Update the storage in struct QmpClient, the qmp_client_next_fdset_id() return type, and the corresponding caller in qmp_fdset_add(), switching the sprintf format from %u to PRIu64. No behavioural change. Signed-off-by: Christian Brauner (Amutable) --- src/shared/qmp-client.c | 4 ++-- src/shared/qmp-client.h | 2 +- src/vmspawn/vmspawn-qmp.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shared/qmp-client.c b/src/shared/qmp-client.c index ad8f8bb24a..41b0c6dd57 100644 --- a/src/shared/qmp-client.c +++ b/src/shared/qmp-client.c @@ -45,7 +45,7 @@ struct QmpClient { qmp_disconnect_callback_t disconnect_callback; void *disconnect_userdata; - unsigned next_fdset_id; /* monotonic fdset-id allocator for add-fd */ + uint64_t next_fdset_id; /* monotonic fdset-id allocator for add-fd */ QmpClientState state; sd_json_variant *current; /* most recently parsed message, pending dispatch */ @@ -893,7 +893,7 @@ sd_event* qmp_client_get_event(QmpClient *c) { return json_stream_get_event(&c->stream); } -unsigned qmp_client_next_fdset_id(QmpClient *c) { +uint64_t qmp_client_next_fdset_id(QmpClient *c) { assert(c); return c->next_fdset_id++; } diff --git a/src/shared/qmp-client.h b/src/shared/qmp-client.h index 8f78473128..7dcd53355d 100644 --- a/src/shared/qmp-client.h +++ b/src/shared/qmp-client.h @@ -82,7 +82,7 @@ void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *us void qmp_client_bind_disconnect(QmpClient *c, qmp_disconnect_callback_t callback, void *userdata); int qmp_client_set_description(QmpClient *c, const char *description); sd_event* qmp_client_get_event(QmpClient *c); -unsigned qmp_client_next_fdset_id(QmpClient *client); +uint64_t qmp_client_next_fdset_id(QmpClient *client); DECLARE_TRIVIAL_REF_UNREF_FUNC(QmpClient, qmp_client); DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClient *, qmp_client_unref); diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index d467d68a89..6c1277f082 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -128,7 +128,7 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; _cleanup_close_ int fd = fd_consume; _cleanup_free_ char *path = NULL; - unsigned id; + uint64_t id; int r; assert(qmp); @@ -141,7 +141,7 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { if (r < 0) return r; - if (asprintf(&path, "/dev/fdset/%u", id) < 0) + if (asprintf(&path, "/dev/fdset/%" PRIu64, id) < 0) return -ENOMEM; r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "add-fd", QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd)), From 6920dfc0f03f191ec12cfc482d78d9a5f05168d5 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:18:27 +0200 Subject: [PATCH 07/19] vmspawn: move VMSPAWN_PCIE_HOTPLUG_SPARES to vmspawn-qmp.h Pure code motion, in preparation for the bridge-side hotplug machinery that needs the same constant to size its hotplug_port_owner[] array. The unsigned-suffix on the literal is dropped: the only consumer that compares against unsigned (vmspawn.c's pcie-port assert) is happy with a plain integer literal. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.h | 2 ++ src/vmspawn/vmspawn.c | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 35cc029ea7..0bc73c90ab 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -5,6 +5,8 @@ #include "shared-forward.h" +#define VMSPAWN_PCIE_HOTPLUG_SPARES 10 + /* Pending job continuation — called when a QMP background job reaches "concluded" state. * Used by blockdev-create to chain remaining drive setup after the job completes. */ typedef int (*pending_job_callback_t)(QmpClient *qmp, void *userdata); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 7ed21af77b..029ae4eacd 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -102,9 +102,6 @@ #define DISK_SERIAL_MAX_LEN_NVME 20 #define DISK_SERIAL_MAX_LEN_VIRTIO_BLK 20 -/* Spare pcie-root-ports reserved for future runtime hotplug beyond the pre-wired devices. */ -#define VMSPAWN_PCIE_HOTPLUG_SPARES 10u - /* An enum controlling how auxiliary state for the VM are maintained, i.e. the TPM state and the EFI variable * NVRAM. */ typedef enum StateMode { From b34413db8e462a51feff98e427268a682ce9c7f7 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:19:20 +0200 Subject: [PATCH 08/19] vmspawn-varlink: use error < 0 in async QMP completion callbacks The QMP client always passes either 0 or a negative errno; the != 0 check flagged values that cannot occur. Switch to the < 0 idiom used elsewhere in the tree, and reorder on_qmp_simple_complete so the error path is the first branch (the more conventional shape for callbacks). Equivalent in behaviour. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-varlink.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 98d3fc73e9..a9b64667c5 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -51,10 +51,10 @@ static int on_qmp_simple_complete( assert(client); - if (error == 0) - (void) sd_varlink_reply(link, NULL); - else + if (error < 0) (void) qmp_error_to_varlink(link, error_desc, error); + else + (void) sd_varlink_reply(link, NULL); sd_varlink_unref(link); return 0; @@ -118,7 +118,7 @@ static int on_qmp_describe_complete( assert(client); - if (error != 0) { + if (error < 0) { (void) qmp_error_to_varlink(link, error_desc, error); return 0; } From 34d12197633924eeb2d43a6725abe70ce0ff5fb0 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:19:48 +0200 Subject: [PATCH 09/19] vmspawn-varlink: simplify on_qmp_describe_complete result extraction Lift the running/status extraction out of the inline ternaries inside SD_JSON_BUILD_PAIR_*() into named local variables with explicit defaults. Pure readability change. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-varlink.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index a9b64667c5..94198923e0 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -123,13 +123,18 @@ static int on_qmp_describe_complete( return 0; } - sd_json_variant *running = sd_json_variant_by_key(result, "running"); - sd_json_variant *status = sd_json_variant_by_key(result, "status"); + sd_json_variant *running_v = sd_json_variant_by_key(result, "running"); + sd_json_variant *status_v = sd_json_variant_by_key(result, "status"); + + bool running = running_v ? sd_json_variant_boolean(running_v) : false; + + const char *status = status_v && sd_json_variant_is_string(status_v) ? + sd_json_variant_string(status_v) : "unknown"; (void) sd_varlink_replybo( link, - SD_JSON_BUILD_PAIR_BOOLEAN("running", running ? sd_json_variant_boolean(running) : false), - SD_JSON_BUILD_PAIR_STRING("status", status && sd_json_variant_is_string(status) ? sd_json_variant_string(status) : "unknown")); + SD_JSON_BUILD_PAIR_BOOLEAN("running", running), + SD_JSON_BUILD_PAIR_STRING("status", status)); return 0; } From 5b3a9f20a6992c6f048154a6d8ca3f1db0481eb8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:20:24 +0200 Subject: [PATCH 10/19] vmspawn-varlink: extract notify_event_subscribers from on_qmp_event Pure refactor: factor the subscriber-notification body out of on_qmp_event into a static helper. on_qmp_event keeps the JOB_STATUS_CHANGE short-circuit and otherwise delegates to the new helper. No behaviour change. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-varlink.c | 38 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 94198923e0..d9beb5bfe3 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -263,31 +263,21 @@ static int dispatch_pending_job(VmspawnQmpBridge *bridge, sd_json_variant *data) return 1; } -static int on_qmp_event( - QmpClient *client, - const char *event, - sd_json_variant *data, - void *userdata) { - - VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); +static int notify_event_subscribers(VmspawnVarlinkContext *ctx, const char *event_name, sd_json_variant *data) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *notification = NULL; sd_varlink *link; char **filter; int r; - assert(client); - assert(event); - - /* Dispatch job status changes to pending continuations (e.g. blockdev-create) */ - if (streq(event, "JOB_STATUS_CHANGE")) - return dispatch_pending_job(ctx->bridge, data); + assert(ctx); + assert(event_name); if (hashmap_isempty(ctx->subscribed)) return 0; r = sd_json_buildo( ¬ification, - SD_JSON_BUILD_PAIR_STRING("event", event), + SD_JSON_BUILD_PAIR_STRING("event", event_name), SD_JSON_BUILD_PAIR_CONDITION(!!data, "data", SD_JSON_BUILD_VARIANT(data))); if (r < 0) { log_warning_errno(r, "Failed to build event notification, ignoring: %m"); @@ -295,7 +285,7 @@ static int on_qmp_event( } HASHMAP_FOREACH_KEY(filter, link, ctx->subscribed) { - if (filter && !strv_contains(filter, event)) + if (filter && !strv_contains(filter, event_name)) continue; r = sd_varlink_notify(link, notification); @@ -306,6 +296,24 @@ static int on_qmp_event( return 0; } +static int on_qmp_event( + QmpClient *client, + const char *event, + sd_json_variant *data, + void *userdata) { + + VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); + + assert(client); + assert(event); + + /* Dispatch job status changes to pending continuations (e.g. blockdev-create) */ + if (streq(event, "JOB_STATUS_CHANGE")) + return dispatch_pending_job(ctx->bridge, data); + + return notify_event_subscribers(ctx, event, data); +} + /* Free all subscriber entries — varlink_subscriber_hash_ops handles * close + unref for each key and strv_free for each value. */ static void drain_event_subscribers(Hashmap **subscribed) { From 9ed1af2bd1638038033b774c17fd173dc089078a Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:21:00 +0200 Subject: [PATCH 11/19] vmspawn-varlink: treat empty event subscription filter as catch-all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A client supplying "filter": [] previously matched no events at all, because filter is checked with strv_contains() — an unintuitive corner case. Treat an empty filter strv identically to a NULL filter (deliver all events) by freeing the empty strv before it lands in the subscription map. Brings the API closer to least-surprise. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-varlink.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index d9beb5bfe3..8df234a9bb 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -159,6 +159,10 @@ static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *paramet if (r != 0) return r; + /* Treat [] identically to null: deliver all events. */ + if (strv_isempty(filter)) + filter = strv_free(filter); + sd_varlink_ref(link); r = hashmap_ensure_put(&ctx->subscribed, &varlink_subscriber_hash_ops, link, filter); From 06816dfee39d8419b9200448d8cc7656405a818f Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:21:43 +0200 Subject: [PATCH 12/19] vmspawn-qmp: pass bridge to on_cont_complete via invoke userdata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The callback already has the bridge available — but it was reaching for it via qmp_client_get_userdata() instead of through its own userdata parameter. Pass the bridge directly from vmspawn_qmp_start() so the callback can read its argument the way the rest of the file does. No behaviour change. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 6c1277f082..702e2e658e 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -1060,10 +1060,10 @@ static int on_cont_complete( int error, void *userdata) { - VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); - assert(client); + VmspawnQmpBridge *bridge = ASSERT_PTR(userdata); + if (error < 0) { log_error_errno(error, "Failed to resume QEMU execution: %s", strna(error_desc)); return sd_event_exit(qmp_client_get_event(client), error); @@ -1077,5 +1077,5 @@ static int on_cont_complete( int vmspawn_qmp_start(VmspawnQmpBridge *bridge) { assert(bridge); - return qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "cont", /* args= */ NULL, on_cont_complete, /* userdata= */ NULL); + return qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "cont", /* args= */ NULL, on_cont_complete, bridge); } From f0c1dc50ef0fcc38af438f445ceaed3cefb242d1 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:23:02 +0200 Subject: [PATCH 13/19] vmspawn-qmp: convert DriveInfo to a refcounted object In preparation for runtime block-device hotplug, where in-flight QMP callbacks need to keep a slot reference on the DriveInfo while the bridge also holds it in its block-device registry. Today each DriveInfo has exactly one owner; switch the API from drive_info_free() / drive_info_freep to drive_info_ref() / drive_info_unref() / drive_info_unrefp so future code can take additional refs without the caller losing track of ownership. drive_info_new() initialises n_ref to 1 (one ref for the caller). The existing drive_infos_done() and the prepare_*_drive() callers in vmspawn.c are switched to the unref form. No behaviour change: each DriveInfo still has exactly one ref at every point in this commit. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 10 ++++++---- src/vmspawn/vmspawn-qmp.h | 5 +++-- src/vmspawn/vmspawn.c | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 702e2e658e..23ae4f73f8 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -31,15 +31,15 @@ DriveInfo* drive_info_new(void) { return NULL; *d = (DriveInfo) { + .n_ref = 1, .fd = -EBADF, .overlay_fd = -EBADF, }; return d; } -DriveInfo* drive_info_free(DriveInfo *d) { - if (!d) - return NULL; +static DriveInfo* drive_info_free(DriveInfo *d) { + assert(d); free(d->path); free(d->format); @@ -52,10 +52,12 @@ DriveInfo* drive_info_free(DriveInfo *d) { return mfree(d); } +DEFINE_TRIVIAL_REF_UNREF_FUNC(DriveInfo, drive_info, drive_info_free); + void drive_infos_done(DriveInfos *infos) { assert(infos); FOREACH_ARRAY(d, infos->drives, infos->n_drives) - drive_info_free(*d); + drive_info_unref(*d); infos->drives = mfree(infos->drives); infos->n_drives = 0; infos->scsi_pcie_port = mfree(infos->scsi_pcie_port); diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 0bc73c90ab..391ac2d5bd 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -71,6 +71,7 @@ typedef enum QmpDriveFlags { * Each DriveInfo is individually heap-allocated so it can be handed off * to the block device registry via TAKE_PTR. */ typedef struct DriveInfo { + unsigned n_ref; char *path; /* original path (for logging; not passed to QEMU) */ char *format; /* "raw" or "qcow2" */ char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ @@ -83,8 +84,8 @@ typedef struct DriveInfo { } DriveInfo; DriveInfo* drive_info_new(void); -DriveInfo* drive_info_free(DriveInfo *d); -DEFINE_TRIVIAL_CLEANUP_FUNC(DriveInfo *, drive_info_free); +DECLARE_TRIVIAL_REF_UNREF_FUNC(DriveInfo, drive_info); +DEFINE_TRIVIAL_CLEANUP_FUNC(DriveInfo *, drive_info_unref); typedef struct DriveInfos { DriveInfo **drives; /* array of individually heap-allocated entries */ diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 029ae4eacd..e927812fe7 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2319,7 +2319,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { if (r < 0) return log_error_errno(r, "Failed to extract filename from path '%s': %m", arg_image); - _cleanup_(drive_info_freep) DriveInfo *d = drive_info_new(); + _cleanup_(drive_info_unrefp) DriveInfo *d = drive_info_new(); if (!d) return log_oom(); @@ -2389,7 +2389,7 @@ static int prepare_extra_drives(DriveInfos *drives) { DiskType dt = drive->disk_type >= 0 ? drive->disk_type : arg_image_disk_type; - _cleanup_(drive_info_freep) DriveInfo *d = drive_info_new(); + _cleanup_(drive_info_unrefp) DriveInfo *d = drive_info_new(); if (!d) return log_oom(); From a57fbf4e6cf538855b5c17c8ce74e42a6acf65eb Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:26:52 +0200 Subject: [PATCH 14/19] vmspawn-qmp: derive QMP node and device ids from a bridge counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the caller-supplied DriveInfo.node_name with two QMP-internal strings generated at setup time from a monotonic per-bridge counter: qmp_node_name = "vmspawn--storage" (blockdev node-name) qmp_device_id = "vmspawn--disk" (qdev id) This is the naming scheme the upcoming runtime-hotplug add path needs: unique across the lifetime of the VM, decoupled from any user-visible id, and stable across the four QMP commands that make up an add (add-fd, blockdev-add, remove-fd, device_add). The boot-time setups don't care about uniqueness, but switching them now means the hotplug path can share qmp_build_device_add() and EphemeralDriveCtx without a parallel naming scheme. vmspawn.c stops assigning node_name in prepare_primary_drive (which used the literal "vmspawn") and prepare_extra_drives (which counted "vmspawn_extra_%zu"); both are replaced by the bridge counter at the point the drive is actually pushed into QEMU. Ephemeral helper-node names follow the same vmspawn--{base-file,base-fmt,overlay-file} convention; the blockdev-create job-id becomes vmspawn--overlay-create. EphemeralDriveCtx grows a qmp_device_id field (the qdev id is independent of the format node-name now) and renames node_name → qmp_node_name to match. No external behaviour change other than the new internal names. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 88 +++++++++++++++++++++------------------ src/vmspawn/vmspawn-qmp.h | 6 ++- src/vmspawn/vmspawn.c | 7 +--- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 23ae4f73f8..bb7c9e06a3 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -45,7 +45,8 @@ static DriveInfo* drive_info_free(DriveInfo *d) { free(d->format); free(d->disk_driver); free(d->serial); - free(d->node_name); + free(d->qmp_node_name); + free(d->qmp_device_id); free(d->pcie_port); safe_close(d->fd); safe_close(d->overlay_fd); @@ -213,16 +214,17 @@ static int qmp_build_blockdev_add_format(const QmpFormatNodeParams *p, sd_json_v SD_JSON_BUILD_PAIR_CONDITION(!!p->backing, "backing", SD_JSON_BUILD_STRING(p->backing))); } -/* Build device_add JSON arguments for a drive */ static int qmp_build_device_add(const DriveInfo *drive, sd_json_variant **ret) { assert(drive); + assert(drive->qmp_node_name); + assert(drive->qmp_device_id); assert(ret); return sd_json_buildo( ret, SD_JSON_BUILD_PAIR_STRING("driver", drive->disk_driver), - SD_JSON_BUILD_PAIR_STRING("drive", drive->node_name), - SD_JSON_BUILD_PAIR_STRING("id", drive->node_name), + SD_JSON_BUILD_PAIR_STRING("drive", drive->qmp_node_name), + SD_JSON_BUILD_PAIR_STRING("id", drive->qmp_device_id), SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(drive->flags, QMP_DRIVE_BOOT), "bootindex", SD_JSON_BUILD_INTEGER(1)), SD_JSON_BUILD_PAIR_CONDITION(!!drive->serial, "serial", SD_JSON_BUILD_STRING(drive->serial)), SD_JSON_BUILD_PAIR_CONDITION(STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd"), @@ -292,23 +294,23 @@ static int get_image_virtual_size(int fd, const char *format, bool is_block_devi return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported image format '%s'", format); } -/* Ephemeral drive continuation — fired when the blockdev-create job concludes. - * Completes the drive setup by adding the overlay format node and the device. */ +/* Continuation state for on_ephemeral_create_concluded: overlay format + device_add. */ typedef struct EphemeralDriveCtx { - char *node_name; /* overlay format node name (= drive node name) */ + char *qmp_node_name; /* overlay format node name, "vmspawn--storage" */ + char *qmp_device_id; /* qdev id, "vmspawn--disk" */ char *overlay_file_node; char *base_fmt_node; - /* Fields for device_add */ char *disk_driver; - char *serial; /* NULL if unset */ - char *pcie_port; /* pcie-root-port bus for device_add (NULL on non-PCIe) */ - QmpDriveFlags flags; /* subset: QMP_DRIVE_DISCARD, QMP_DRIVE_DISCARD_NO_UNREF, QMP_DRIVE_BOOT */ + char *serial; + char *pcie_port; /* NULL on non-PCIe */ + QmpDriveFlags flags; /* subset forwarded to device_add */ } EphemeralDriveCtx; static EphemeralDriveCtx* ephemeral_drive_ctx_free(EphemeralDriveCtx *ctx) { if (!ctx) return NULL; - free(ctx->node_name); + free(ctx->qmp_node_name); + free(ctx->qmp_device_id); free(ctx->overlay_file_node); free(ctx->base_fmt_node); free(ctx->disk_driver); @@ -330,7 +332,7 @@ static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { /* Open formatted overlay as qcow2 with backing reference */ QmpFormatNodeParams overlay_fmt_params = { - .node_name = ctx->node_name, + .node_name = ctx->qmp_node_name, .format = "qcow2", .file_node_name = ctx->overlay_file_node, .backing = ctx->base_fmt_node, @@ -338,32 +340,32 @@ static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { }; r = qmp_build_blockdev_add_format(&overlay_fmt_params, &fmt_args); if (r < 0) - return log_error_errno(r, "Failed to build overlay format JSON for '%s': %m", ctx->node_name); + return log_error_errno(r, "Failed to build overlay format JSON for '%s': %m", ctx->qmp_node_name); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_complete, (void*) "blockdev-add"); if (r < 0) return r; - /* device_add: attach to virtual hardware. Build a temporary DriveInfo as a - * read-only view into the continuation context to reuse qmp_build_device_add(). */ + /* Temporary DriveInfo view so we can reuse qmp_build_device_add(). */ const DriveInfo tmp = { - .disk_driver = ctx->disk_driver, - .node_name = ctx->node_name, - .serial = ctx->serial, - .pcie_port = ctx->pcie_port, - .flags = ctx->flags & QMP_DRIVE_BOOT, - .fd = -EBADF, - .overlay_fd = -EBADF, + .disk_driver = ctx->disk_driver, + .serial = ctx->serial, + .pcie_port = ctx->pcie_port, + .flags = ctx->flags & QMP_DRIVE_BOOT, + .fd = -EBADF, + .overlay_fd = -EBADF, + .qmp_node_name = ctx->qmp_node_name, + .qmp_device_id = ctx->qmp_device_id, }; r = qmp_build_device_add(&tmp, &device_args); if (r < 0) - return log_error_errno(r, "Failed to build device_add JSON for '%s': %m", ctx->node_name); + return log_error_errno(r, "Failed to build device_add JSON for '%s': %m", ctx->qmp_device_id); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); if (r < 0) return r; - log_debug("Queued ephemeral drive completion for '%s'", ctx->node_name); + log_debug("Queued ephemeral drive completion for '%s'", ctx->qmp_device_id); return 0; } @@ -379,11 +381,14 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D assert(drive->fd >= 0); assert(drive->overlay_fd >= 0); - /* Node names: -base-file, -base-fmt, -overlay-file, */ - _cleanup_free_ char *base_file_node = strjoin(drive->node_name, "-base-file"); - _cleanup_free_ char *base_fmt_node = strjoin(drive->node_name, "-base-fmt"); - _cleanup_free_ char *overlay_file_node = strjoin(drive->node_name, "-overlay-file"); - if (!base_file_node || !base_fmt_node || !overlay_file_node) + uint64_t counter = bridge->next_block_counter++; + + _cleanup_free_ char *base_file_node = NULL, *base_fmt_node = NULL, *overlay_file_node = NULL; + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", counter) < 0 || + asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", counter) < 0 || + asprintf(&base_file_node, "vmspawn-%" PRIu64 "-base-file", counter) < 0 || + asprintf(&base_fmt_node, "vmspawn-%" PRIu64 "-base-fmt", counter) < 0 || + asprintf(&overlay_file_node, "vmspawn-%" PRIu64 "-overlay-file", counter) < 0) return log_oom(); /* Read virtual size before passing the fd to QEMU (TAKE_FD consumes it) */ @@ -459,8 +464,8 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to build blockdev-create options: %m"); - _cleanup_free_ char *job_id = strjoin("create-", drive->node_name); - if (!job_id) + _cleanup_free_ char *job_id = NULL; + if (asprintf(&job_id, "vmspawn-%" PRIu64 "-overlay-create", counter) < 0) return log_oom(); _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd_args = NULL; @@ -481,7 +486,8 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D ectx_flags |= QMP_DRIVE_DISCARD_NO_UNREF; *ectx = (EphemeralDriveCtx) { - .node_name = strdup(drive->node_name), + .qmp_node_name = strdup(drive->qmp_node_name), + .qmp_device_id = strdup(drive->qmp_device_id), .overlay_file_node = strdup(overlay_file_node), .base_fmt_node = strdup(base_fmt_node), .disk_driver = strdup(drive->disk_driver), @@ -489,9 +495,9 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D .pcie_port = drive->pcie_port ? strdup(drive->pcie_port) : NULL, .flags = ectx_flags, }; - if (!ectx->node_name || !ectx->overlay_file_node || !ectx->base_fmt_node || - !ectx->disk_driver || (drive->serial && !ectx->serial) || - (drive->pcie_port && !ectx->pcie_port)) + if (!ectx->qmp_node_name || !ectx->qmp_device_id || !ectx->overlay_file_node || + !ectx->base_fmt_node || !ectx->disk_driver || + (drive->serial && !ectx->serial) || (drive->pcie_port && !ectx->pcie_port)) return log_oom(); r = vmspawn_qmp_bridge_register_job(bridge, job_id, @@ -519,9 +525,11 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri assert(drive); assert(drive->fd >= 0); - /* Node names: -file, */ - _cleanup_free_ char *file_node_name = strjoin(drive->node_name, "-file"); - if (!file_node_name) + uint64_t counter = bridge->next_block_counter++; + _cleanup_free_ char *file_node_name = NULL; + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", counter) < 0 || + asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", counter) < 0 || + asprintf(&file_node_name, "vmspawn-%" PRIu64 "-file", counter) < 0) return log_oom(); _cleanup_free_ char *fdset_path = NULL; @@ -542,7 +550,7 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri return log_error_errno(r, "Failed to send blockdev-add for '%s': %m", drive->path); QmpFormatNodeParams fmt_params = { - .node_name = drive->node_name, + .node_name = drive->qmp_node_name, .format = drive->format, .file_node_name = file_node_name, .flags = drive->flags & (QMP_DRIVE_READ_ONLY|QMP_DRIVE_DISCARD), diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index 391ac2d5bd..a58dfae3be 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -28,7 +28,8 @@ typedef enum VmspawnQmpFeatureFlags { typedef struct VmspawnQmpBridge { QmpClient *qmp; - Hashmap *pending_jobs; /* job_id (string, owned) -> PendingJob* */ + Hashmap *pending_jobs; /* blockdev-create continuations */ + uint64_t next_block_counter; /* monotonic counter feeding internal QMP names (vmspawn--*) */ VmspawnQmpFeatureFlags features; bool setup_done; } VmspawnQmpBridge; @@ -76,7 +77,8 @@ typedef struct DriveInfo { char *format; /* "raw" or "qcow2" */ char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ char *serial; - char *node_name; + char *qmp_node_name; /* "vmspawn--storage" */ + char *qmp_device_id; /* "vmspawn--disk" */ char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ int fd; /* pre-opened image fd (-EBADF if unused) */ int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-EBADF if unused) */ diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index e927812fe7..3f99d0547c 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2343,8 +2343,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { d->path = strdup(arg_image); d->format = strdup(ASSERT_PTR(image_format_to_string(arg_image_format))); - d->node_name = strdup("vmspawn"); - if (!d->path || !d->format || !d->node_name) + if (!d->path || !d->format) return log_oom(); d->fd = TAKE_FD(image_fd); if (S_ISBLK(st.st_mode)) @@ -2380,7 +2379,6 @@ static int prepare_extra_drives(DriveInfos *drives) { assert(drives); - size_t extra_idx = 0; FOREACH_ARRAY(drive, arg_extra_drives.drives, arg_extra_drives.n_drives) { _cleanup_free_ char *drive_fn = NULL; r = path_extract_filename(drive->path, &drive_fn); @@ -2421,9 +2419,6 @@ static int prepare_extra_drives(DriveInfos *drives) { d->flags |= QMP_DRIVE_BLOCK_DEVICE; d->flags |= QMP_DRIVE_NO_FLUSH; - if (asprintf(&d->node_name, "vmspawn_extra_%zu", extra_idx++) < 0) - return log_oom(); - drives->drives[drives->n_drives++] = TAKE_PTR(d); } From b75b6041e014c721132258af85727fcabd383c58 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:28:51 +0200 Subject: [PATCH 15/19] vmspawn-qmp: pipeline remove-fd after each blockdev-add QEMU keeps a monitor-side fd alive until either an explicit remove-fd arrives or the fdset's last duplicate is closed. Today vmspawn issues add-fd but never the matching remove-fd, so each fdset stays around for the lifetime of the VM even after the consuming blockdev is torn down. Pipelining a remove-fd directly after the blockdev-add that consumed the fd hands ownership entirely to the blockdev: the fdset auto-disposes when raw_close runs at blockdev-del time. This is the shape needed by hotplug, where blockdev-del must clean up everything without further coordination. Mechanically: - qmp_fdset_add() takes a callback/userdata pair (so callers control failure handling) and an optional out-param for the numeric fdset id. All boot-time callers keep using on_qmp_complete with a label. - A new qmp_fdset_remove() helper sends remove-fd with caller-supplied callback/userdata. - qmp_setup_ephemeral_drive captures both fdset ids and fires remove-fd immediately after each base/overlay file blockdev-add. - qmp_setup_regular_drive does the same for its single file blockdev-add. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 69 +++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index bb7c9e06a3..cfa887e122 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -125,9 +125,15 @@ static int on_qmp_complete( return 0; } -/* Send add-fd via SCM_RIGHTS; return /dev/fdset/N. Allocations run before invoke so a late - * OOM cannot orphan an fdset on QEMU's side; *ret_path is only written on full success. */ -static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { +/* Send add-fd via SCM_RIGHTS; return /dev/fdset/N and the numeric fdset id. */ +static int qmp_fdset_add( + QmpClient *qmp, + int fd_consume, + qmp_command_callback_t callback, + void *userdata, + char **ret_path, + uint64_t *ret_fdset_id) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; _cleanup_close_ int fd = fd_consume; _cleanup_free_ char *path = NULL; @@ -136,6 +142,7 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { assert(qmp); assert(fd_consume >= 0); + assert(callback); assert(ret_path); id = qmp_client_next_fdset_id(qmp); @@ -148,14 +155,39 @@ static int qmp_fdset_add(QmpClient *qmp, int fd_consume, char **ret_path) { return -ENOMEM; r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "add-fd", QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd)), - on_qmp_complete, (void*) "add-fd"); + callback, userdata); if (r < 0) return r; *ret_path = TAKE_PTR(path); + if (ret_fdset_id) + *ret_fdset_id = id; return 0; } +/* Issue remove-fd for an fdset whose dup is now held by a blockdev. The fdset + * persists until the dup is closed (in raw_close at blockdev-del time) — see + * QEMU's monitor/fds.c:177-181 on the fds/dup_fds split. */ +static int qmp_fdset_remove( + QmpClient *qmp, + uint64_t fdset_id, + qmp_command_callback_t callback, + void *userdata) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + int r; + + assert(qmp); + assert(callback); + + r = sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", fdset_id)); + if (r < 0) + return r; + + return qmp_client_invoke(qmp, /* ret_slot= */ NULL, "remove-fd", QMP_CLIENT_ARGS(args), + callback, userdata); +} + typedef struct QmpFileNodeParams { const char *node_name; const char *filename; @@ -399,12 +431,16 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D /* Step 1-2: Pass both fds to QEMU */ _cleanup_free_ char *base_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), &base_path); + uint64_t base_fdset_id; + r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), + on_qmp_complete, (void*) "add-fd", &base_path, &base_fdset_id); if (r < 0) return log_error_errno(r, "Failed to send add-fd for base image '%s': %m", drive->path); _cleanup_free_ char *overlay_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->overlay_fd), &overlay_path); + uint64_t overlay_fdset_id; + r = qmp_fdset_add(qmp, TAKE_FD(drive->overlay_fd), + on_qmp_complete, (void*) "add-fd", &overlay_path, &overlay_fdset_id); if (r < 0) return log_error_errno(r, "Failed to send add-fd for overlay of '%s': %m", drive->path); @@ -421,6 +457,12 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for base file '%s': %m", drive->path); + /* The base file node now holds a dup of the fd; release the monitor's + * original so the fdset auto-frees when raw_close runs at teardown. */ + r = qmp_fdset_remove(qmp, base_fdset_id, on_qmp_complete, (void*) "remove-fd"); + if (r < 0) + return log_error_errno(r, "Failed to send remove-fd for base image '%s': %m", drive->path); + /* Step 4: Base image format node (read-only) */ QmpFormatNodeParams base_fmt_params = { .node_name = base_fmt_node, @@ -453,6 +495,11 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for overlay file '%s': %m", drive->path); + /* Same as for base: the overlay file node has the dup. */ + r = qmp_fdset_remove(qmp, overlay_fdset_id, on_qmp_complete, (void*) "remove-fd"); + if (r < 0) + return log_error_errno(r, "Failed to send remove-fd for overlay of '%s': %m", drive->path); + /* Step 6: Fire blockdev-create to format the overlay */ _cleanup_(sd_json_variant_unrefp) sd_json_variant *create_options = NULL; r = sd_json_buildo(&create_options, @@ -533,7 +580,9 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri return log_oom(); _cleanup_free_ char *fdset_path = NULL; - r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), &fdset_path); + uint64_t fdset_id; + r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), + on_qmp_complete, (void*) "add-fd", &fdset_path, &fdset_id); if (r < 0) return log_error_errno(r, "Failed to send add-fd for '%s': %m", drive->path); @@ -549,6 +598,12 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, Dri if (r < 0) return log_error_errno(r, "Failed to send blockdev-add for '%s': %m", drive->path); + /* The file node now holds a dup of the fd; release the monitor's + * original so the fdset auto-frees when raw_close runs at teardown. */ + r = qmp_fdset_remove(qmp, fdset_id, on_qmp_complete, (void*) "remove-fd"); + if (r < 0) + return log_error_errno(r, "Failed to send remove-fd for '%s': %m", drive->path); + QmpFormatNodeParams fmt_params = { .node_name = drive->qmp_node_name, .format = drive->format, From 1dbfab132ae10fed5c91c6e09a3c3450e01ef261 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:29:37 +0200 Subject: [PATCH 16/19] vmspawn-qmp: keep the event loop running on post-setup QMP failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on_qmp_complete tears the event loop down on any QMP error, which is the right behaviour while we're still building the VM (a missing device means we'd boot a half-configured guest). Once the boot-time setup is finished and the guest is running, killing the event loop on a QMP error means a single failed runtime command (e.g. a hotplug device_add that the guest rejects) takes the whole VM down. Consult bridge->setup_done — already flipped at the end of boot setup — and skip the event-loop exit once it's set; logging is sufficient post-setup. The bridge is fetched from the qmp client userdata, the same pointer the rest of the file uses. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index cfa887e122..7eff45f5e9 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -103,9 +103,7 @@ void machine_config_done(MachineConfig *c) { vsock_info_done(&c->vsock); } -/* Generic async QMP setup-completion callback. The userdata argument carries the - * command name (as a string literal) for logging. On failure, request a clean - * event loop exit so vmspawn shuts down instead of running a VM with missing devices. */ +/* Generic completion callback; userdata is a string literal label. Exits the event loop on boot-time failures. */ static int on_qmp_complete( QmpClient *client, sd_json_variant *result, @@ -113,12 +111,17 @@ static int on_qmp_complete( int error, void *userdata) { - const char *label = ASSERT_PTR(userdata); - assert(client); + VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); + const char *label = ASSERT_PTR(userdata); + if (error < 0) { log_error_errno(error, "%s failed: %s", label, strna(error_desc)); + + if (bridge->setup_done) + return 0; + return sd_event_exit(qmp_client_get_event(client), error); } From 1d0a8e5dbd267c803e100d9030d70d327eddf8b1 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 00:34:50 +0200 Subject: [PATCH 17/19] vmspawn-qmp: add the hotplug-capable block-device add machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the bulk of the runtime block-device hotplug feature. The boot-time qmp_setup_regular_drive() path is rewritten on top of a new vmspawn_qmp_add_block_device() that owns the DriveInfo, drives a four QMP-command pipeline (add-fd → blockdev-add → remove-fd → device_add) through staged ref-counted callbacks, and registers the drive in a per-bridge block-device registry on success. New on the bridge: - block_devices: user-id → DriveInfo* registry (owned ref). - hotplug_port_owner[VMSPAWN_PCIE_HOTPLUG_SPARES]: per-port owner string for the spare pcie-root-ports, allocated/released through vmspawn_qmp_bridge_{allocate,release_pcie_port_by_idx}(). - scsi_controller_port_idx / scsi_controller_created: track the on-demand virtio-scsi-pci controller so the first SCSI hotplug creates it (against an allocated spare port) and subsequent ones just attach. - next_block_counter: already in place from the previous commit, now actually consumed by add_block_device(). New on DriveInfo: - bridge (weak), id (varlink-visible — caller-supplied or auto-set to qmp_device_id), disk_type (for List replies later), counter, qmp_node_name / qmp_device_id (already added), fdset_path, pcie_port_idx (the hotplug port reserved by this drive — stays set across the add pipeline so drive_info_free releases it when the registry ref drops at DEVICE_DELETED time), rollback_mask (BlockDeviceAddStage bits of completed stages — plus a FAILED sentinel that suppresses cascading errors), and link (NULL ⇒ boot-time, sd_event_exit on fail; non-NULL ⇒ hotplug, varlink reply on fail). Helpers: - drive_info_unref() now releases any reserved hotplug port through the bridge and unrefs link. - drive_info_add_fail(): single failure entry point — fires the teardown for completed stages, sets the FAILED bit, and sends either the varlink error or sd_event_exit. Boot-time failures (link == NULL) always exit the loop, so late-arriving ephemeral continuation replies don't get silenced after cont. - vmspawn_qmp_block_device_teardown(): post-hoc blockdev-del when blockdev-add succeeded but a later stage failed. - reply_qmp_error: varlink reply helper — disconnect errors map to io.systemd.MachineInstance.NotConnected, everything else goes through sd_varlink_error_errno. - on_add_observe_stage / on_add_blockdev_stage / on_add_device_add_complete: the staged callbacks that drive the add pipeline, each holding one slot ref on the DriveInfo. The ephemeral blockdev-create continuation reuses the latter two so its post-cont replies go through drive_info_add_fail instead of the generic on_qmp_complete (which would silently log under setup_done). - on_scsi_controller_complete: handles the SCSI controller setup (releases the reserved port on failure, propagates the boot-time fatal error policy). - qmp_build_blockdev_add_inline(): single blockdev-add JSON that creates the file+format pair as one node, used by the hotplug path (the boot-time helpers stay separate so they can stack with the ephemeral path's base/overlay format nodes). EphemeralDriveCtx is trimmed down to a DriveInfo ref plus the two ephemeral-local scratch node names (overlay-file, base-fmt). The copies of disk_driver/serial/pcie_port/flags/qmp_node_name/ qmp_device_id go away — the continuation reads them straight off the ref'd drive. qmp_setup_ephemeral_drive now sets drive->bridge / drive->id / drive->counter up front (matching the hotplug path) and folds the feature-dependent DISCARD_NO_UNREF into drive->flags so qmp_build_blockdev_add_format picks it up. vmspawn_qmp_bridge_free() unrefs the qmp client first — its pending callbacks may still reach for the bridge's hotplug port table when they drop their last DriveInfo ref — then tears down the hashmaps and the port owner strings. The boot-time qmp_setup_regular_drive() collapses to a thin wrapper that asserts "caller hasn't pre-set drive->id", takes ownership and calls vmspawn_qmp_add_block_device(); the dispatcher in vmspawn_qmp_setup_drives() now hands ownership over with TAKE_PTR. The previous qmp_setup_drive() dispatcher disappears (its body is inlined into the loop). vmspawn_qmp_init() initialises scsi_controller_port_idx to -1. The cosmetic (*d) → DriveInfo *drive = *d; locals in drives_need_scsi_controller (vmspawn-qmp.c) and assign_pcie_ports (vmspawn.c) ride along — they live in code touched by this commit and would otherwise produce churn. vmspawn_qmp_remove_block_device() — the symmetric remove API — is added in the next commit so this one stays focused on the add path. Signed-off-by: Christian Brauner (Amutable) --- src/vmspawn/vmspawn-qmp.c | 619 +++++++++++++++++++++++++++++--------- src/vmspawn/vmspawn-qmp.h | 39 ++- src/vmspawn/vmspawn.c | 14 +- 3 files changed, 507 insertions(+), 165 deletions(-) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 7eff45f5e9..9158d5b10a 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -6,9 +6,11 @@ #include "sd-event.h" #include "sd-json.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "blockdev-util.h" +#include "errno-util.h" #include "ether-addr-util.h" #include "fd-util.h" #include "hashmap.h" @@ -19,12 +21,18 @@ #include "string-util.h" #include "strv.h" #include "vmspawn-qmp.h" +#include "vmspawn-util.h" DEFINE_PRIVATE_HASH_OPS_FULL( pending_job_hash_ops, char, string_hash_func, string_compare_func, free, PendingJob, pending_job_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + block_devices_hash_ops, + char, string_hash_func, string_compare_func, + DriveInfo, drive_info_unref); + DriveInfo* drive_info_new(void) { DriveInfo *d = new(DriveInfo, 1); if (!d) @@ -34,20 +42,68 @@ DriveInfo* drive_info_new(void) { .n_ref = 1, .fd = -EBADF, .overlay_fd = -EBADF, + .pcie_port_idx = -1, }; return d; } +static int vmspawn_qmp_bridge_allocate_pcie_port( + VmspawnQmpBridge *bridge, + const char *owner_id, + char **ret_name, + int *ret_idx) { + + assert(bridge); + assert(owner_id); + assert(ret_name); + assert(ret_idx); + + for (int i = 0; i < VMSPAWN_PCIE_HOTPLUG_SPARES; i++) { + if (bridge->hotplug_port_owner[i]) + continue; + + _cleanup_free_ char *owner = strdup(owner_id), *name = NULL; + if (!owner || asprintf(&name, "vmspawn-hotplug-pci-root-port-%d", i) < 0) + return -ENOMEM; + + bridge->hotplug_port_owner[i] = TAKE_PTR(owner); + *ret_name = TAKE_PTR(name); + *ret_idx = i; + return 0; + } + + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), + "No free PCIe hotplug port available for owner '%s'.", + owner_id); +} + +static void vmspawn_qmp_bridge_release_pcie_port_by_idx(VmspawnQmpBridge *bridge, int idx) { + assert(bridge); + + if (idx < 0) + return; + + assert(idx < VMSPAWN_PCIE_HOTPLUG_SPARES); + + bridge->hotplug_port_owner[idx] = mfree(bridge->hotplug_port_owner[idx]); +} + static DriveInfo* drive_info_free(DriveInfo *d) { assert(d); + if (d->bridge) + vmspawn_qmp_bridge_release_pcie_port_by_idx(d->bridge, d->pcie_port_idx); + free(d->path); free(d->format); free(d->disk_driver); free(d->serial); + free(d->pcie_port); + free(d->id); free(d->qmp_node_name); free(d->qmp_device_id); - free(d->pcie_port); + free(d->fdset_path); + sd_varlink_unref(d->link); safe_close(d->fd); safe_close(d->overlay_fd); return mfree(d); @@ -61,7 +117,6 @@ void drive_infos_done(DriveInfos *infos) { drive_info_unref(*d); infos->drives = mfree(infos->drives); infos->n_drives = 0; - infos->scsi_pcie_port = mfree(infos->scsi_pcie_port); } void network_info_done(NetworkInfo *info) { @@ -269,6 +324,43 @@ static int qmp_build_device_add(const DriveInfo *drive, sd_json_variant **ret) { "bus", SD_JSON_BUILD_STRING(drive->pcie_port))); } +/* Inline form: one blockdev-add creates format+file; one blockdev-del tears + * down the whole tree. Used for regular boot drives and hotplug. */ +static int qmp_build_blockdev_add_inline( + const char *node_name, + const char *format, + const char *filename, + const char *file_driver, + QmpDriveFlags flags, + VmspawnQmpFeatureFlags features, + sd_json_variant **ret) { + + bool use_io_uring = FLAGS_SET(features, VMSPAWN_QMP_FEATURE_IO_URING); + bool use_discard_no_unref = FLAGS_SET(flags, QMP_DRIVE_DISCARD_NO_UNREF); + + assert(node_name); + assert(format); + assert(filename); + assert(file_driver); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("node-name", node_name), + SD_JSON_BUILD_PAIR_STRING("driver", format), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_READ_ONLY), "read-only", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_DISCARD), "discard", JSON_BUILD_CONST_STRING("unmap")), + SD_JSON_BUILD_PAIR_CONDITION(use_discard_no_unref, "discard-no-unref", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR("file", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_STRING("driver", file_driver), + SD_JSON_BUILD_PAIR_STRING("filename", filename), + SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(flags, QMP_DRIVE_READ_ONLY), "read-only", SD_JSON_BUILD_BOOLEAN(true)), + SD_JSON_BUILD_PAIR_CONDITION(use_io_uring, "aio", JSON_BUILD_CONST_STRING("io_uring")), + SD_JSON_BUILD_PAIR("cache", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_BOOLEAN("direct", false), + SD_JSON_BUILD_PAIR_BOOLEAN("no-flush", FLAGS_SET(flags, QMP_DRIVE_NO_FLUSH))))))); +} + /* Issue blockdev-add for a file node. */ static int qmp_add_file_node(QmpClient *qmp, const QmpFileNodeParams *p) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; @@ -329,28 +421,27 @@ static int get_image_virtual_size(int fd, const char *format, bool is_block_devi return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported image format '%s'", format); } +/* Forward declarations — on_ephemeral_create_concluded routes failures through + * the shared block-device add callbacks defined further below. */ +static int drive_info_add_fail(DriveInfo *d, int error, const char *error_desc); +static int on_add_blockdev_stage(QmpClient *client, sd_json_variant *result, + const char *error_desc, int error, void *userdata); +static int on_add_device_add_complete(QmpClient *client, sd_json_variant *result, + const char *error_desc, int error, void *userdata); + /* Continuation state for on_ephemeral_create_concluded: overlay format + device_add. */ typedef struct EphemeralDriveCtx { - char *qmp_node_name; /* overlay format node name, "vmspawn--storage" */ - char *qmp_device_id; /* qdev id, "vmspawn--disk" */ + DriveInfo *drive; /* ref */ char *overlay_file_node; char *base_fmt_node; - char *disk_driver; - char *serial; - char *pcie_port; /* NULL on non-PCIe */ - QmpDriveFlags flags; /* subset forwarded to device_add */ } EphemeralDriveCtx; static EphemeralDriveCtx* ephemeral_drive_ctx_free(EphemeralDriveCtx *ctx) { if (!ctx) return NULL; - free(ctx->qmp_node_name); - free(ctx->qmp_device_id); + drive_info_unref(ctx->drive); free(ctx->overlay_file_node); free(ctx->base_fmt_node); - free(ctx->disk_driver); - free(ctx->serial); - free(ctx->pcie_port); return mfree(ctx); } @@ -363,50 +454,48 @@ static void ephemeral_drive_ctx_free_void(void *p) { static int on_ephemeral_create_concluded(QmpClient *qmp, void *userdata) { _cleanup_(ephemeral_drive_ctx_freep) EphemeralDriveCtx *ctx = ASSERT_PTR(userdata); _cleanup_(sd_json_variant_unrefp) sd_json_variant *fmt_args = NULL, *device_args = NULL; + _cleanup_(drive_info_unrefp) DriveInfo *slot_ref = NULL; + DriveInfo *drive = ctx->drive; int r; + assert(qmp); + /* Open formatted overlay as qcow2 with backing reference */ QmpFormatNodeParams overlay_fmt_params = { - .node_name = ctx->qmp_node_name, + .node_name = drive->qmp_node_name, .format = "qcow2", .file_node_name = ctx->overlay_file_node, .backing = ctx->base_fmt_node, - .flags = ctx->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_DISCARD_NO_UNREF), + .flags = drive->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_DISCARD_NO_UNREF), }; r = qmp_build_blockdev_add_format(&overlay_fmt_params, &fmt_args); if (r < 0) - return log_error_errno(r, "Failed to build overlay format JSON for '%s': %m", ctx->qmp_node_name); + return drive_info_add_fail(drive, r, NULL); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_complete, (void*) "blockdev-add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), + on_add_blockdev_stage, slot_ref); if (r < 0) - return r; + return drive_info_add_fail(drive, r, NULL); + TAKE_PTR(slot_ref); - /* Temporary DriveInfo view so we can reuse qmp_build_device_add(). */ - const DriveInfo tmp = { - .disk_driver = ctx->disk_driver, - .serial = ctx->serial, - .pcie_port = ctx->pcie_port, - .flags = ctx->flags & QMP_DRIVE_BOOT, - .fd = -EBADF, - .overlay_fd = -EBADF, - .qmp_node_name = ctx->qmp_node_name, - .qmp_device_id = ctx->qmp_device_id, - }; - r = qmp_build_device_add(&tmp, &device_args); + r = qmp_build_device_add(drive, &device_args); if (r < 0) - return log_error_errno(r, "Failed to build device_add JSON for '%s': %m", ctx->qmp_device_id); + return drive_info_add_fail(drive, r, NULL); - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), + on_add_device_add_complete, slot_ref); if (r < 0) - return r; + return drive_info_add_fail(drive, r, NULL); + TAKE_PTR(slot_ref); - log_debug("Queued ephemeral drive completion for '%s'", ctx->qmp_device_id); + log_debug("Queued ephemeral drive completion for '%s'", drive->qmp_device_id); return 0; } -/* Set up an ephemeral drive: base image (read-only) + anonymous qcow2 overlay (read-write). - * The final steps (overlay format + device_add) are deferred to a job continuation that - * fires when the blockdev-create job concludes. */ +/* Base image (read-only) + anonymous qcow2 overlay (read-write). Overlay format + * and device_add run from the blockdev-create continuation. */ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { int r; @@ -416,16 +505,24 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D assert(drive->fd >= 0); assert(drive->overlay_fd >= 0); - uint64_t counter = bridge->next_block_counter++; + drive->bridge = bridge; + drive->counter = bridge->next_block_counter++; _cleanup_free_ char *base_file_node = NULL, *base_fmt_node = NULL, *overlay_file_node = NULL; - if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", counter) < 0 || - asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", counter) < 0 || - asprintf(&base_file_node, "vmspawn-%" PRIu64 "-base-file", counter) < 0 || - asprintf(&base_fmt_node, "vmspawn-%" PRIu64 "-base-fmt", counter) < 0 || - asprintf(&overlay_file_node, "vmspawn-%" PRIu64 "-overlay-file", counter) < 0) + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", drive->counter) < 0 || + asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", drive->counter) < 0 || + asprintf(&base_file_node, "vmspawn-%" PRIu64 "-base-file", drive->counter) < 0 || + asprintf(&base_fmt_node, "vmspawn-%" PRIu64 "-base-fmt", drive->counter) < 0 || + asprintf(&overlay_file_node, "vmspawn-%" PRIu64 "-overlay-file", drive->counter) < 0) return log_oom(); + /* Auto-assigned user id reuses qmp_device_id (matching vmspawn_qmp_add_block_device). */ + if (!drive->id) { + drive->id = strdup(drive->qmp_device_id); + if (!drive->id) + return log_oom(); + } + /* Read virtual size before passing the fd to QEMU (TAKE_FD consumes it) */ uint64_t virtual_size; r = get_image_virtual_size(drive->fd, drive->format, FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE), &virtual_size); @@ -515,7 +612,7 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D return log_error_errno(r, "Failed to build blockdev-create options: %m"); _cleanup_free_ char *job_id = NULL; - if (asprintf(&job_id, "vmspawn-%" PRIu64 "-overlay-create", counter) < 0) + if (asprintf(&job_id, "vmspawn-%" PRIu64 "-overlay-create", drive->counter) < 0) return log_oom(); _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd_args = NULL; @@ -525,29 +622,23 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return log_error_errno(r, "Failed to build blockdev-create JSON: %m"); + /* Fold DISCARD_NO_UNREF into drive->flags so the continuation's overlay format blockdev-add + * picks it up via drive->flags. */ + if (FLAGS_SET(drive->flags, QMP_DRIVE_DISCARD) && + FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_DISCARD_NO_UNREF)) + drive->flags |= QMP_DRIVE_DISCARD_NO_UNREF; + /* Register continuation: when the job concludes, fire overlay format + device_add */ _cleanup_(ephemeral_drive_ctx_freep) EphemeralDriveCtx *ectx = new(EphemeralDriveCtx, 1); if (!ectx) return log_oom(); - QmpDriveFlags ectx_flags = drive->flags & (QMP_DRIVE_DISCARD|QMP_DRIVE_BOOT); - if (FLAGS_SET(drive->flags, QMP_DRIVE_DISCARD) && - FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_DISCARD_NO_UNREF)) - ectx_flags |= QMP_DRIVE_DISCARD_NO_UNREF; - *ectx = (EphemeralDriveCtx) { - .qmp_node_name = strdup(drive->qmp_node_name), - .qmp_device_id = strdup(drive->qmp_device_id), + .drive = drive_info_ref(drive), .overlay_file_node = strdup(overlay_file_node), .base_fmt_node = strdup(base_fmt_node), - .disk_driver = strdup(drive->disk_driver), - .serial = drive->serial ? strdup(drive->serial) : NULL, - .pcie_port = drive->pcie_port ? strdup(drive->pcie_port) : NULL, - .flags = ectx_flags, }; - if (!ectx->qmp_node_name || !ectx->qmp_device_id || !ectx->overlay_file_node || - !ectx->base_fmt_node || !ectx->disk_driver || - (drive->serial && !ectx->serial) || (drive->pcie_port && !ectx->pcie_port)) + if (!ectx->overlay_file_node || !ectx->base_fmt_node) return log_oom(); r = vmspawn_qmp_bridge_register_job(bridge, job_id, @@ -559,91 +650,345 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D TAKE_PTR(ectx); r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-create", QMP_CLIENT_ARGS(cmd_args), on_qmp_complete, (void*) "blockdev-create"); - if (r < 0) + if (r < 0) { + _unused_ _cleanup_(pending_job_freep) PendingJob *dead = hashmap_remove(bridge->pending_jobs, job_id); return log_error_errno(r, "Failed to send blockdev-create for '%s': %m", drive->path); + } log_debug("Queued ephemeral drive setup for '%s' (job %s)", drive->path, job_id); return 0; } -/* Set up a regular (non-ephemeral) drive: single file node + format node + device_add. */ -static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { +static int reply_qmp_error(sd_varlink *link, const char *error_desc, int error) { + assert(link); + + if (ERRNO_IS_DISCONNECT(error)) + return sd_varlink_error(link, "io.systemd.MachineInstance.NotConnected", NULL); + if (error_desc) + log_warning("QMP error: %s", error_desc); + return sd_varlink_error_errno(link, error < 0 ? error : -EIO); +} + +/* After the pipelined remove-fd at add time, QEMU auto-frees the fdset when + * raw_close (during blockdev-del) releases the last dup. So teardown only + * needs to fire blockdev-del. */ +static void vmspawn_qmp_block_device_teardown(QmpClient *client, const char *qmp_node_name, BlockDeviceStateFlags stages) { + assert(client); + assert(qmp_node_name); + + if (!FLAGS_SET(stages, BLOCK_DEVICE_STATE_BLOCKDEV_ADDED)) + return; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + if (sd_json_buildo(&args, SD_JSON_BUILD_PAIR_STRING("node-name", qmp_node_name)) >= 0) + (void) qmp_client_invoke(client, /* ret_slot= */ NULL, "blockdev-del", QMP_CLIENT_ARGS(args), + on_qmp_complete, (void*) "teardown blockdev-del"); +} + +/* Insert into the owning primary map and the non-owning qmp_device_id view. On + * secondary-put failure, roll back the primary so neither map carries a stale entry. */ +static int bridge_register_drive(VmspawnQmpBridge *b, DriveInfo *d) { + int r; + + assert(b); + assert(d); + assert(d->id); + assert(d->qmp_device_id); + + r = hashmap_ensure_put(&b->block_devices, &block_devices_hash_ops, + d->id, drive_info_ref(d)); + if (r < 0) { + drive_info_unref(d); + return r; + } + + r = hashmap_ensure_put(&b->block_devices_by_qmp_id, &string_hash_ops, + d->qmp_device_id, d); + if (r < 0) { + drive_info_unref(hashmap_remove(b->block_devices, d->id)); + return r; + } + + return 0; +} + +/* Drop the drive from both maps; returns the pointer removed from the primary + * (NULL if it wasn't there) so the caller can decide whether to unref. */ +static DriveInfo* bridge_unregister_drive(VmspawnQmpBridge *b, DriveInfo *d) { + assert(b); + assert(d); + + hashmap_remove_value(b->block_devices_by_qmp_id, d->qmp_device_id, d); + return hashmap_remove_value(b->block_devices, d->id, d); +} + +/* First-error entry point: marks FAILED so cascading callbacks no-op, drops + * the registry slot, then replies on the link or exits the loop. */ +static int drive_info_add_fail(DriveInfo *d, int error, const char *error_desc) { + assert(d); + + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) + return 0; + + vmspawn_qmp_block_device_teardown(d->bridge->qmp, d->qmp_node_name, d->state); + d->state = BLOCK_DEVICE_STATE_ADD_FAILED; + + if (bridge_unregister_drive(d->bridge, d)) + drive_info_unref(d); + + if (d->link) { + (void) reply_qmp_error(d->link, error_desc, error); + d->link = sd_varlink_unref(d->link); + return 0; + } + + log_error_errno(error, "Block device '%s' setup failed: %s", + strna(d->id), strna(error_desc)); + + /* Boot-time (link == NULL) is always fatal — even for late-arriving ephemeral replies. */ + return sd_event_exit(qmp_client_get_event(d->bridge->qmp), error); +} + +/* Rolls back the up-front registry insert on a sync error path. */ +static void drive_info_unregister_on_failurep(DriveInfo **dp) { + assert(dp); + + DriveInfo *d = *dp; + if (!d) + return; + d->state |= BLOCK_DEVICE_STATE_ADD_FAILED; + if (bridge_unregister_drive(d->bridge, d)) + drive_info_unref(d); +} + +/* Shared by the intermediate stages that don't need to record a rollback bit + * (add-fd, remove-fd). Just forwards errors to drive_info_add_fail so cascades + * from earlier stage failures get suppressed via the FAILED sentinel. */ +static int on_add_observe_stage( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + return 0; +} + +static int on_add_blockdev_stage( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + + /* A sync error after blockdev-add was queued may have marked the chain FAILED. + * The blockdev node we just created is orphaned — tear it down retroactively + * and don't claim BLOCKDEV_ADDED on a drive that's already been unregistered. */ + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) { + vmspawn_qmp_block_device_teardown(d->bridge->qmp, d->qmp_node_name, + BLOCK_DEVICE_STATE_BLOCKDEV_ADDED); + return 0; + } + + d->state |= BLOCK_DEVICE_STATE_BLOCKDEV_ADDED; + return 0; +} + +static int on_add_device_add_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *d = ASSERT_PTR(userdata); + + assert(client); + + if (error < 0) + return drive_info_add_fail(d, error, error_desc); + + if (FLAGS_SET(d->state, BLOCK_DEVICE_STATE_ADD_FAILED)) + return 0; + + if (d->link) { + (void) sd_varlink_replybo(d->link, SD_JSON_BUILD_PAIR_STRING("id", d->id)); + d->link = sd_varlink_unref(d->link); + } + + log_info("Block device '%s' attached", d->id); + return 0; +} + +static int on_scsi_controller_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + assert(client); + + VmspawnQmpBridge *bridge = ASSERT_PTR(qmp_client_get_userdata(client)); + + if (error < 0) { + /* QEMU's device_add is transactional — on error it calls object_unparent() + * before replying, so the "vmspawn_scsi" id is free for the next retry. */ + vmspawn_qmp_bridge_release_pcie_port_by_idx(bridge, bridge->scsi_controller_port_idx); + bridge->scsi_controller_port_idx = -1; + bridge->scsi_controller_created = false; + log_warning("virtio-scsi-pci controller setup failed: %s", strna(error_desc)); + if (!bridge->setup_done) + return sd_event_exit(qmp_client_get_event(client), error); + } + + return 0; +} + +static int qmp_setup_scsi_controller(VmspawnQmpBridge *bridge, const char *pcie_port) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + int r; + + assert(bridge); + + r = sd_json_buildo( + &args, + SD_JSON_BUILD_PAIR_STRING("driver", "virtio-scsi-pci"), + SD_JSON_BUILD_PAIR_STRING("id", "vmspawn_scsi"), + SD_JSON_BUILD_PAIR_CONDITION(!!pcie_port, "bus", SD_JSON_BUILD_STRING(pcie_port))); + if (r < 0) + return log_error_errno(r, "Failed to build SCSI controller JSON: %m"); + + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), + on_scsi_controller_complete, NULL); + if (r < 0) + return log_error_errno(r, "Failed to send SCSI controller device_add: %m"); + + log_debug("Queued virtio-scsi-pci controller setup"); + return 0; +} + +static int vmspawn_qmp_add_block_device(VmspawnQmpBridge *bridge, DriveInfo *drive) { int r; assert(bridge); - assert(qmp); assert(drive); + assert(drive->format); + assert(drive->disk_driver); assert(drive->fd >= 0); - uint64_t counter = bridge->next_block_counter++; - _cleanup_free_ char *file_node_name = NULL; - if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", counter) < 0 || - asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", counter) < 0 || - asprintf(&file_node_name, "vmspawn-%" PRIu64 "-file", counter) < 0) + _unused_ _cleanup_(drive_info_unrefp) DriveInfo *owned = drive; + _cleanup_(drive_info_unrefp) DriveInfo *slot_ref = NULL; + _cleanup_(drive_info_unregister_on_failurep) DriveInfo *registered = NULL; + + drive->bridge = bridge; + drive->counter = bridge->next_block_counter++; + if (asprintf(&drive->qmp_node_name, "vmspawn-%" PRIu64 "-storage", drive->counter) < 0) return log_oom(); + if (asprintf(&drive->qmp_device_id, "vmspawn-%" PRIu64 "-disk", drive->counter) < 0) + return log_oom(); + /* Auto-assigned user ids reuse qmp_device_id. */ + if (!drive->id) { + drive->id = strdup(drive->qmp_device_id); + if (!drive->id) + return log_oom(); + } + + /* Reserve the registry slot up-front so the device_add callback's commit can't fail. */ + r = bridge_register_drive(bridge, drive); + if (r < 0) + return r; + registered = drive; + + /* First SCSI hotplug needs a virtio-scsi-pci controller to attach to. */ + if (STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd") && !bridge->scsi_controller_created) { + _cleanup_free_ char *controller_port = NULL; + int controller_port_idx = -1; + if (ARCHITECTURE_NEEDS_PCIE_ROOT_PORTS) { + r = vmspawn_qmp_bridge_allocate_pcie_port(bridge, "vmspawn_scsi", + &controller_port, &controller_port_idx); + if (r == -EBUSY) + return log_error_errno(r, "No PCIe hotplug ports left for SCSI controller"); + if (r < 0) + return log_error_errno(r, "Failed to allocate PCIe hotplug port for SCSI controller: %m"); + } + + r = qmp_setup_scsi_controller(bridge, controller_port); + if (r < 0) { + vmspawn_qmp_bridge_release_pcie_port_by_idx(bridge, controller_port_idx); + return r; + } + + /* Set before the reply so a second SCSI hotplug queued in the meantime + * doesn't re-create the controller; reset in on_scsi_controller_complete on error. */ + bridge->scsi_controller_port_idx = controller_port_idx; + bridge->scsi_controller_created = true; + } - _cleanup_free_ char *fdset_path = NULL; uint64_t fdset_id; - r = qmp_fdset_add(qmp, TAKE_FD(drive->fd), - on_qmp_complete, (void*) "add-fd", &fdset_path, &fdset_id); + slot_ref = drive_info_ref(drive); + r = qmp_fdset_add(bridge->qmp, TAKE_FD(drive->fd), + on_add_observe_stage, slot_ref, &drive->fdset_path, &fdset_id); if (r < 0) - return log_error_errno(r, "Failed to send add-fd for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); - QmpFileNodeParams file_params = { - .node_name = file_node_name, - .filename = fdset_path, - .driver = FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE) ? "host_device" : "file", - .flags = drive->flags & (QMP_DRIVE_READ_ONLY|QMP_DRIVE_NO_FLUSH), - }; - if (FLAGS_SET(bridge->features, VMSPAWN_QMP_FEATURE_IO_URING)) - file_params.flags |= QMP_DRIVE_IO_URING; - r = qmp_add_file_node(qmp, &file_params); - if (r < 0) - return log_error_errno(r, "Failed to send blockdev-add for '%s': %m", drive->path); - - /* The file node now holds a dup of the fd; release the monitor's - * original so the fdset auto-frees when raw_close runs at teardown. */ - r = qmp_fdset_remove(qmp, fdset_id, on_qmp_complete, (void*) "remove-fd"); - if (r < 0) - return log_error_errno(r, "Failed to send remove-fd for '%s': %m", drive->path); - - QmpFormatNodeParams fmt_params = { - .node_name = drive->qmp_node_name, - .format = drive->format, - .file_node_name = file_node_name, - .flags = drive->flags & (QMP_DRIVE_READ_ONLY|QMP_DRIVE_DISCARD), - }; - _cleanup_(sd_json_variant_unrefp) sd_json_variant *fmt_args = NULL; - r = qmp_build_blockdev_add_format(&fmt_params, &fmt_args); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *blockdev_args = NULL; + r = qmp_build_blockdev_add_inline( + drive->qmp_node_name, drive->format, drive->fdset_path, + FLAGS_SET(drive->flags, QMP_DRIVE_BLOCK_DEVICE) ? "host_device" : "file", + drive->flags, bridge->features, &blockdev_args); if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(fmt_args), on_qmp_complete, (void*) "blockdev-add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "blockdev-add", QMP_CLIENT_ARGS(blockdev_args), + on_add_blockdev_stage, slot_ref); if (r < 0) - return log_error_errno(r, "Failed to send blockdev-add format for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); + + /* Release the monitor's original fd; the blockdev-add above took a dup. */ + slot_ref = drive_info_ref(drive); + r = qmp_fdset_remove(bridge->qmp, fdset_id, on_add_observe_stage, slot_ref); + if (r < 0) + return r; + TAKE_PTR(slot_ref); - /* device_add: attach to virtual hardware */ _cleanup_(sd_json_variant_unrefp) sd_json_variant *device_args = NULL; r = qmp_build_device_add(drive, &device_args); if (r < 0) return r; - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), on_qmp_complete, (void*) "device_add"); + slot_ref = drive_info_ref(drive); + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(device_args), + on_add_device_add_complete, slot_ref); if (r < 0) - return log_error_errno(r, "Failed to send device_add for '%s': %m", drive->path); + return r; + TAKE_PTR(slot_ref); - log_debug("Queued drive setup for '%s'", drive->path); + TAKE_PTR(registered); return 0; } -/* Configure a single drive via QMP. Dispatches to ephemeral or regular setup. */ -static int qmp_setup_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, DriveInfo *drive) { +static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, DriveInfo *drive) { + assert(bridge); assert(drive); + assert(drive->fd >= 0); + assert(!drive->id); - if (drive->overlay_fd >= 0) - return qmp_setup_ephemeral_drive(bridge, qmp, drive); - - return qmp_setup_regular_drive(bridge, qmp, drive); + return vmspawn_qmp_add_block_device(bridge, drive); } int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { @@ -830,36 +1175,6 @@ int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock) { return 0; } -static bool drives_need_scsi_controller(DriveInfos *drives) { - assert(drives); - - FOREACH_ARRAY(d, drives->drives, drives->n_drives) - if (STR_IN_SET((*d)->disk_driver, "scsi-hd", "scsi-cd")) - return true; - - return false; -} - -static int qmp_setup_scsi_controller(QmpClient *qmp, const char *pcie_port) { - _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; - int r; - - r = sd_json_buildo( - &args, - SD_JSON_BUILD_PAIR_STRING("driver", "virtio-scsi-pci"), - SD_JSON_BUILD_PAIR_STRING("id", "vmspawn_scsi"), - SD_JSON_BUILD_PAIR_CONDITION(!!pcie_port, "bus", SD_JSON_BUILD_STRING(pcie_port))); - if (r < 0) - return log_error_errno(r, "Failed to build SCSI controller JSON: %m"); - - r = qmp_client_invoke(qmp, /* ret_slot= */ NULL, "device_add", QMP_CLIENT_ARGS(args), on_qmp_complete, (void*) "device_add"); - if (r < 0) - return log_error_errno(r, "Failed to send SCSI controller device_add: %m"); - - log_debug("Queued virtio-scsi-pci controller setup"); - return 0; -} - int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives) { int r; @@ -869,16 +1184,15 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives) { QmpClient *qmp = vmspawn_qmp_bridge_get_qmp(bridge); /* io_uring support was probed during vmspawn_qmp_init(). The cached result in - * bridge->features is passed to each file node setup call. */ - - if (drives_need_scsi_controller(drives)) { - r = qmp_setup_scsi_controller(qmp, drives->scsi_pcie_port); - if (r < 0) - return r; - } + * bridge->features is passed to each file node setup call. SCSI controller + * creation is handled on-demand by vmspawn_qmp_add_block_device() for the first + * SCSI drive, using the hotplug-spares pool. */ FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - r = qmp_setup_drive(bridge, qmp, *d); + if ((*d)->overlay_fd >= 0) + r = qmp_setup_ephemeral_drive(bridge, qmp, *d); + else + r = qmp_setup_regular_drive(bridge, TAKE_PTR(*d)); if (r < 0) return r; } @@ -898,9 +1212,16 @@ VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b) { if (!b) return NULL; + /* Unref first: pending QMP callbacks may release hotplug ports through the bridge. */ + qmp_client_unref(b->qmp); + + hashmap_free(b->block_devices_by_qmp_id); + hashmap_free(b->block_devices); hashmap_free(b->pending_jobs); - qmp_client_unref(b->qmp); + FOREACH_ELEMENT(owner, b->hotplug_port_owner) + free(*owner); + return mfree(b); } @@ -1067,6 +1388,8 @@ int vmspawn_qmp_init(VmspawnQmpBridge **ret, int fd, sd_event *event) { if (!bridge) return log_oom(); + bridge->scsi_controller_port_idx = -1; + r = qmp_client_connect_fd(&bridge->qmp, fd); if (r < 0) return log_error_errno(r, "Failed to create QMP client: %m"); diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index a58dfae3be..f1407c6f2f 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -3,6 +3,7 @@ #include +#include "machine-util.h" #include "shared-forward.h" #define VMSPAWN_PCIE_HOTPLUG_SPARES 10 @@ -28,10 +29,15 @@ typedef enum VmspawnQmpFeatureFlags { typedef struct VmspawnQmpBridge { QmpClient *qmp; - Hashmap *pending_jobs; /* blockdev-create continuations */ - uint64_t next_block_counter; /* monotonic counter feeding internal QMP names (vmspawn--*) */ + Hashmap *pending_jobs; /* blockdev-create continuations */ + Hashmap *block_devices; /* user_id (char*) → DriveInfo* (owned ref) */ + Hashmap *block_devices_by_qmp_id; /* qmp_device_id (char*) → DriveInfo* (non-owning view) */ + char *hotplug_port_owner[VMSPAWN_PCIE_HOTPLUG_SPARES]; /* owner id per port; NULL = free */ + int scsi_controller_port_idx; /* hotplug port idx taken by virtio-scsi-pci, -1 if none */ + uint64_t next_block_counter; /* monotonic counter feeding internal QMP names (vmspawn--*) */ VmspawnQmpFeatureFlags features; bool setup_done; + bool scsi_controller_created; /* virtio-scsi-pci has been device_add'd */ } VmspawnQmpBridge; VmspawnQmpBridge* vmspawn_qmp_bridge_free(VmspawnQmpBridge *b); @@ -68,21 +74,39 @@ typedef enum QmpDriveFlags { QMP_DRIVE_DISCARD_NO_UNREF = 1u << 6, /* qcow2 only */ } QmpDriveFlags; -/* Drive info for QMP-based drive setup. All string fields are owned. - * Each DriveInfo is individually heap-allocated so it can be handed off - * to the block device registry via TAKE_PTR. */ +typedef enum BlockDeviceStateFlags { + BLOCK_DEVICE_STATE_BLOCKDEV_ADDED = 1u << 0, + BLOCK_DEVICE_STATE_ADD_FAILED = 1u << 1, /* first error fired; suppress cascades */ +} BlockDeviceStateFlags; + +/* Ref-counted; each of the four add-stage QMP slots holds one ref. + * + * link == NULL → boot-time: failure calls sd_event_exit (if !setup_done). + * link != NULL → hotplug: failure replies via the varlink link. */ typedef struct DriveInfo { unsigned n_ref; + + /* Config */ char *path; /* original path (for logging; not passed to QEMU) */ char *format; /* "raw" or "qcow2" */ char *disk_driver; /* "virtio-blk-pci", "scsi-hd", "scsi-cd", "nvme" */ char *serial; - char *qmp_node_name; /* "vmspawn--storage" */ - char *qmp_device_id; /* "vmspawn--disk" */ char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ int fd; /* pre-opened image fd (-EBADF if unused) */ int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-EBADF if unused) */ QmpDriveFlags flags; + + /* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */ + VmspawnQmpBridge *bridge; /* weak */ + char *id; /* varlink-visible id (caller-supplied, or falls back to qmp_device_id) */ + DiskType disk_type; /* for the ListBlockDevices `driver` field */ + uint64_t counter; /* internal N used in qmp_node_name / qmp_device_id */ + char *qmp_node_name; /* "vmspawn--storage" */ + char *qmp_device_id; /* "vmspawn--disk" */ + char *fdset_path; /* "/dev/fdset/N" */ + int pcie_port_idx; /* hotplug port idx held by this drive; -1 once committed or unused */ + BlockDeviceStateFlags state; + sd_varlink *link; /* ref'd iff hotplug */ } DriveInfo; DriveInfo* drive_info_new(void); @@ -92,7 +116,6 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DriveInfo *, drive_info_unref); typedef struct DriveInfos { DriveInfo **drives; /* array of individually heap-allocated entries */ size_t n_drives; - char *scsi_pcie_port; /* owned: pcie-root-port id for SCSI controller (NULL if no SCSI or non-PCIe) */ } DriveInfos; void drive_infos_done(DriveInfos *infos); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 3f99d0547c..1a349a1d63 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2440,19 +2440,15 @@ static int assign_pcie_ports(MachineConfig *c) { size_t port = 0; - /* Drives: non-SCSI drives get individual ports, SCSI controller gets one port */ - bool need_scsi = false; + /* Non-SCSI drives get individual ports. SCSI controllers (if any) allocate + * from the hotplug-spares pool on demand at device-add time. */ FOREACH_ARRAY(d, drives->drives, drives->n_drives) { - if (STR_IN_SET((*d)->disk_driver, "scsi-hd", "scsi-cd")) { - need_scsi = true; + DriveInfo *drive = *d; + if (STR_IN_SET(drive->disk_driver, "scsi-hd", "scsi-cd")) continue; - } - if (asprintf(&(*d)->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) + if (asprintf(&drive->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) return log_oom(); } - if (need_scsi) - if (asprintf(&drives->scsi_pcie_port, "vmspawn-pcieport-%zu", port++) < 0) - return log_oom(); if (network->type) if (asprintf(&network->pcie_port, "vmspawn-pcieport-%zu", port++) < 0) From 93c04d4aba520afb4bf57e27fb0faac8268ed685 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 22 Apr 2026 10:19:09 +0200 Subject: [PATCH 18/19] vmspawn-qmp: add vmspawn_qmp_remove_block_device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hot-remove counterpart to vmspawn_qmp_add_block_device. Looks the drive up in the bridge's block_devices registry by caller-supplied id and dispatches device_del using the internal qmp_device_id; the varlink link gets the immediate ack/error reply once QEMU completes the request. Concurrency: a second remove for the same id while the first is in flight (between device_del dispatch and DEVICE_DELETED) would otherwise reach QEMU and earn a confusing 'already in the process of unplug' reply. Track the in-flight state with a new BLOCK_DEVICE_REMOVE_PENDING bit on the existing rollback_mask, and short-circuit duplicate calls with -EBUSY. The bit is cleared on device_del failure (the drive is still attached, so retries make sense) and naturally vanishes on success when the registry entry is dropped. DEVICE_DELETED handling: the actual blockdev-del + registry removal + pcie-port release is deferred to vmspawn_qmp_dispatch_device_deleted, which fires from on_qmp_event in vmspawn-varlink.c when the guest acks the eject. Hooking it from the existing QMP event dispatcher keeps the cleanup local to vmspawn-qmp.{c,h}. The function has no varlink callers in this PR — the io.systemd.VirtualMachineInstance method handler that forwards into it lands with the rest of the hotplug PR. Signed-off-by: Christian Brauner --- src/vmspawn/vmspawn-qmp.c | 83 +++++++++++++++++++++++++++++++++++ src/vmspawn/vmspawn-qmp.h | 3 ++ src/vmspawn/vmspawn-varlink.c | 5 +++ 3 files changed, 91 insertions(+) diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 9158d5b10a..621c5e781a 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -991,6 +991,89 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, DriveInfo *drive) { return vmspawn_qmp_add_block_device(bridge, drive); } +/* device_del completion is just QEMU acking the request; teardown happens + * in vmspawn_qmp_dispatch_device_deleted() once the guest acks the eject. */ +static int on_remove_device_del_complete( + QmpClient *client, + sd_json_variant *result, + const char *error_desc, + int error, + void *userdata) { + + _cleanup_(drive_info_unrefp) DriveInfo *drive = ASSERT_PTR(userdata); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = TAKE_PTR(drive->link); + + assert(client); + assert(link); + + if (error < 0) { + /* device_del rejected: clear the pending bit so the caller can retry. */ + drive->state &= ~BLOCK_DEVICE_STATE_REMOVE_PENDING; + + return reply_qmp_error(link, error_desc, error); + } + + return sd_varlink_reply(link, NULL); +} + +int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link, const char *id) { + int r; + + assert(bridge); + assert(link); + assert(id); + + DriveInfo *drive = hashmap_get(bridge->block_devices, id); + if (!drive) + return reply_qmp_error(link, "Unknown block device id", -ENOENT); + if (!FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_BLOCKDEV_ADDED)) + return reply_qmp_error(link, "Block device add pending", -EBUSY); + if (FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_REMOVE_PENDING)) + return reply_qmp_error(link, "Block device removal pending", -EBUSY); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL; + r = sd_json_buildo(&args, SD_JSON_BUILD_PAIR_STRING("id", drive->qmp_device_id)); + if (r < 0) + return sd_varlink_error_errno(link, r); + + assert(!drive->link); + drive->link = sd_varlink_ref(link); + drive->state |= BLOCK_DEVICE_STATE_REMOVE_PENDING; + + r = qmp_client_invoke(bridge->qmp, /* ret_slot= */ NULL, "device_del", QMP_CLIENT_ARGS(args), + on_remove_device_del_complete, drive_info_ref(drive)); + if (r < 0) { + drive->link = sd_varlink_unref(drive->link); + drive->state &= ~BLOCK_DEVICE_STATE_REMOVE_PENDING; + drive_info_unref(drive); + return sd_varlink_error_errno(link, r); + } + return 0; +} + +/* DEVICE_DELETED arrives once the guest has acked the eject; only then is it + * safe to drop the blockdev node and release the registry slot (and PCIe port). */ +int vmspawn_qmp_dispatch_device_deleted(VmspawnQmpBridge *bridge, sd_json_variant *data) { + assert(bridge); + + if (!data) + return 0; + + const char *qmp_device_id = sd_json_variant_string(sd_json_variant_by_key(data, "device")); + if (!qmp_device_id) + return 0; + + DriveInfo *drive = hashmap_get(bridge->block_devices_by_qmp_id, qmp_device_id); + if (!drive) + return 0; + + vmspawn_qmp_block_device_teardown(bridge->qmp, drive->qmp_node_name, drive->state); + + assert_se(bridge_unregister_drive(bridge, drive) == drive); + drive_info_unref(drive); + return 0; +} + int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *netdev_args = NULL, *device_args = NULL; bool tap_by_fd; diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index f1407c6f2f..d8403520c9 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -77,6 +77,7 @@ typedef enum QmpDriveFlags { typedef enum BlockDeviceStateFlags { BLOCK_DEVICE_STATE_BLOCKDEV_ADDED = 1u << 0, BLOCK_DEVICE_STATE_ADD_FAILED = 1u << 1, /* first error fired; suppress cascades */ + BLOCK_DEVICE_STATE_REMOVE_PENDING = 1u << 2, /* device_del in flight; reject concurrent removes */ } BlockDeviceStateFlags; /* Ref-counted; each of the four add-stage QMP slots holds one ref. @@ -176,3 +177,5 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives); int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network); int vmspawn_qmp_setup_virtiofs(VmspawnQmpBridge *bridge, const VirtiofsInfos *virtiofs); int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock); +int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link, const char *id); +int vmspawn_qmp_dispatch_device_deleted(VmspawnQmpBridge *bridge, sd_json_variant *data); diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 8df234a9bb..2e0daa6039 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -12,6 +12,7 @@ #include "varlink-io.systemd.QemuMachineInstance.h" #include "varlink-io.systemd.VirtualMachineInstance.h" #include "varlink-util.h" +#include "vmspawn-qmp.h" #include "vmspawn-varlink.h" DEFINE_PRIVATE_HASH_OPS_FULL( @@ -315,6 +316,10 @@ static int on_qmp_event( if (streq(event, "JOB_STATUS_CHANGE")) return dispatch_pending_job(ctx->bridge, data); + /* Notification still fans out below. */ + if (streq(event, "DEVICE_DELETED")) + (void) vmspawn_qmp_dispatch_device_deleted(ctx->bridge, data); + return notify_event_subscribers(ctx, event, data); } From 67cd0977cd96595432fe1b9848bfed4864b2f443 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 10:22:17 +0200 Subject: [PATCH 19/19] vmspawn-varlink: drop AcquireQMP stub and QemuMachineInstance interface The AcquireQMP() method was a placeholder that always returned EOPNOTSUPP, reserving room for a future id-rewriting QMP multiplex proxy. The broader direction is for systemd-vmspawn to remain the single source of truth for VM control rather than exposing raw QMP to clients. Since AcquireQMP was the only method on io.systemd.QemuMachineInstance (and AlreadyAcquired was its only error), remove the whole interface along with the stub, and update the controlAddress field comment in io.systemd.Machine to stop referencing it. Signed-off-by: Christian Brauner (Amutable) --- src/shared/meson.build | 1 - src/shared/varlink-io.systemd.Machine.c | 2 +- .../varlink-io.systemd.QemuMachineInstance.c | 17 ----------------- .../varlink-io.systemd.QemuMachineInstance.h | 6 ------ src/vmspawn/vmspawn-varlink.c | 11 ++--------- 5 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 src/shared/varlink-io.systemd.QemuMachineInstance.c delete mode 100644 src/shared/varlink-io.systemd.QemuMachineInstance.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 56b823c50c..a94b78ba94 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -238,7 +238,6 @@ shared_sources = files( 'varlink-io.systemd.Network.Link.c', 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.PCRLock.c', - 'varlink-io.systemd.QemuMachineInstance.c', 'varlink-io.systemd.Repart.c', 'varlink-io.systemd.Resolve.c', 'varlink-io.systemd.Resolve.Hook.c', diff --git a/src/shared/varlink-io.systemd.Machine.c b/src/shared/varlink-io.systemd.Machine.c index da373a3c20..cb1b0665d6 100644 --- a/src/shared/varlink-io.systemd.Machine.c +++ b/src/shared/varlink-io.systemd.Machine.c @@ -57,7 +57,7 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(vSockCid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(sshAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(sshPrivateKeyPath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), - SD_VARLINK_FIELD_COMMENT("Varlink socket address for direct machine control. The server at this address is expected to implement io.systemd.MachineInstance and optionally io.systemd.VirtualMachineInstance and io.systemd.QemuMachineInstance."), + SD_VARLINK_FIELD_COMMENT("Varlink socket address for direct machine control. The server at this address is expected to implement io.systemd.MachineInstance and optionally io.systemd.VirtualMachineInstance."), SD_VARLINK_DEFINE_INPUT(controlAddress, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Controls whether to allocate a scope unit for the machine to register. If false, the client already took care of that and registered a service/scope specific to the machine."), SD_VARLINK_DEFINE_INPUT(allocateUnit, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), diff --git a/src/shared/varlink-io.systemd.QemuMachineInstance.c b/src/shared/varlink-io.systemd.QemuMachineInstance.c deleted file mode 100644 index b03fa2199c..0000000000 --- a/src/shared/varlink-io.systemd.QemuMachineInstance.c +++ /dev/null @@ -1,17 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ - -#include "varlink-io.systemd.QemuMachineInstance.h" - -static SD_VARLINK_DEFINE_METHOD_FULL( - AcquireQMP, - SD_VARLINK_REQUIRES_UPGRADE); - -static SD_VARLINK_DEFINE_ERROR(AlreadyAcquired); - -SD_VARLINK_DEFINE_INTERFACE( - io_systemd_QemuMachineInstance, - "io.systemd.QemuMachineInstance", - SD_VARLINK_SYMBOL_COMMENT("Acquire a direct QMP connection to the QEMU instance via protocol upgrade"), - &vl_method_AcquireQMP, - SD_VARLINK_SYMBOL_COMMENT("A QMP connection has already been acquired by another client"), - &vl_error_AlreadyAcquired); diff --git a/src/shared/varlink-io.systemd.QemuMachineInstance.h b/src/shared/varlink-io.systemd.QemuMachineInstance.h deleted file mode 100644 index 203dacb40c..0000000000 --- a/src/shared/varlink-io.systemd.QemuMachineInstance.h +++ /dev/null @@ -1,6 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ -#pragma once - -#include "sd-varlink-idl.h" - -extern const sd_varlink_interface vl_interface_io_systemd_QemuMachineInstance; diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c index 2e0daa6039..51a1091e40 100644 --- a/src/vmspawn/vmspawn-varlink.c +++ b/src/vmspawn/vmspawn-varlink.c @@ -9,7 +9,6 @@ #include "string-util.h" #include "strv.h" #include "varlink-io.systemd.MachineInstance.h" -#include "varlink-io.systemd.QemuMachineInstance.h" #include "varlink-io.systemd.VirtualMachineInstance.h" #include "varlink-util.h" #include "vmspawn-qmp.h" @@ -184,10 +183,6 @@ static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *paramet return 0; } -static int vl_method_acquire_qmp(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { - return sd_varlink_error_errno(link, -EOPNOTSUPP); -} - static void vl_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata); @@ -372,8 +367,7 @@ int vmspawn_varlink_setup( r = sd_varlink_server_add_interface_many( ctx->varlink_server, &vl_interface_io_systemd_MachineInstance, - &vl_interface_io_systemd_VirtualMachineInstance, - &vl_interface_io_systemd_QemuMachineInstance); + &vl_interface_io_systemd_VirtualMachineInstance); if (r < 0) return log_error_errno(r, "Failed to add varlink interfaces: %m"); @@ -385,8 +379,7 @@ int vmspawn_varlink_setup( "io.systemd.MachineInstance.Resume", vl_method_resume, "io.systemd.MachineInstance.Reboot", vl_method_reboot, "io.systemd.MachineInstance.Describe", vl_method_describe, - "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events, - "io.systemd.QemuMachineInstance.AcquireQMP", vl_method_acquire_qmp); + "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events); if (r < 0) return log_error_errno(r, "Failed to bind varlink methods: %m");