Commit Graph
339 Commits
Author SHA1 Message Date
Luca Boccassi b811fa5b32 vmspawn: enforce minimum uptime with --console=gui
When using --console=gui the QEMU window closes immediately when the VM
has stopped, so any console output at shutdown is lost, which makes
debugging difficult. Ensure the VM stays booted for a minimum of 15s.
2026-06-01 18:59:20 +01:00
Daan De Meyer 0a63318420 ssh-generator: Make sure sshd can always read the authorized keys file
sshd reads AuthorizedKeysFile after dropping to the authenticating user's UID, so the
0400 credential file under %d/ is unreadable for non-root users. Materialize a 0444
copy in a RuntimeDirectory so the ephemeral key works for any user. */
2026-05-29 16:12:45 +02:00
Lennart Poettering b12a365866 vmspawn: move --kernel=/--initrd= under the "Execution" section
This has little to do with host configuration (where it was so far), and
a lot with what being executed, let's move it over.

Note that --help and man page so far differed here quite a bit: the
former had the "Execution" section, the latter didn't. This creates it
in the man page, to bring the two back in sync.
2026-05-20 23:39:47 +02:00
Lennart Poettering 183ec272ef vmspawn: split out networking related options into its own "Networking" section in --help 2026-05-20 23:38:30 +02:00
Lennart Poettering f9a40f7aa3 vmspawn: split 'Integration' section into 'Logging' and 'SSH'
Each of the two has multiple options, hence let's split them out.
2026-05-20 23:37:32 +02:00
Lennart Poettering 1a0b17df32 vmspawn: move image related knobs close to --image=
(Also, arrange the order if the image options group in the man page to
the same as in the --help text)
2026-05-20 23:37:31 +02:00
Lennart Poettering f941dff917 vmspawn: initial support for SEV-SNP guests (#42193) 2026-05-20 17:42:26 +02:00
Paul Meyer 1ccda1533d vmspawn: use EPYC-v4 cpu for SNP
SNP requires a named, stable CPU model so the launch measurement is
reproducible across hosts. EPYC-v4 is the baseline that covers all
SNP-capable processors (Milan and later).

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
2026-05-20 13:26:03 +02:00
Paul Meyer 4f8215add2 vmspawn: initial support for SEV-SNP guests
Add --confidential-computing=sev-snp to run the guest as an AMD SEV-SNP
confidential VM. Loads a raw OVMF firmware blob via -bios (SNP doesn't
support the pflash + NVRAM split), attaches a sev-snp-guest object,
and hashes the kernel, initrd and cmdline into the launch measurement
when direct kernel boot is used. Incompatible features (Secure Boot,
CXL, virtio-balloon, SMBIOS credentials) are rejected or disabled; an
attached vTPM must be treated as untrusted by the guest.

The feature is marked experimental in the man page.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
2026-05-20 13:25:34 +02:00
Lennart Poettering 3f0e95b7a8 vmspawn: port help() to help-util.h APIs 2026-05-19 15:00:09 +01:00
Christian Brauner 0bd38f2258 vmspawn: multifunction-pack pcie-root-ports on pcie.0
The pre-allocated pcie-root-port block in run_virtual_machine() places
every port directly on pcie.0 with an auto-assigned PCI address. A
minimal VM already costs 4 builtin + 10 hotplug spares = 14 pcie.0
slots, on top of 3 implicit virtio devices (virtio-rng-pci,
virtio-balloon, virtio-serial-pci) for another 3.

pcie.0 has 32 device-numbers; q35 reserves 0x00 (host bridge) and 0x1f
(ICH9 LPC), leaving ~30 auto-assignable slots. TEST-64-UDEV-STORAGE-
nvme_basic pushes 20 '-device nvme' lines through
$SYSTEMD_VMSPAWN_QEMU_EXTRA, which vmspawn does not see — total demand
14 + 3 + 20 = 37 > 30. Bus realization fails after QEMU's chardev has
already emitted the QMP greeting, and the monitor socket POLLHUPs
while we are mid-feature-probe, reported as 'QMP connection dropped
during feature probing'.

Pack the root ports as multifunction devices, 8 per pcie.0 device-
number (QEMU docs/pcie.txt:84, 117-120, 255-258). Function 0 of each
group carries multifunction=on; functions 1-7 ride the same slot via
addr=N.F. Each function remains independently hot-pluggable so
vmspawn's QMP device_add machinery is unaffected. 14 ports collapse to
2 pcie.0 slots; the nvme_basic budget becomes 2 + 3 + 20 = 25.

The chassis/slot properties (used for ACPI hotplug identity) stay as
i+1 — they live in a uint8_t namespace independent of the PCI BDF and
are still unique. Base PCI slot 0x10 sits above the auto-assigned
virtio devices (which land at 0x01-0x03 in config order) and below
the q35 LPC reservation at 0x1f.

While here, rebuild the slot-count formula to match what
assign_pcie_ports() actually allocates. The +1 'SCSI controller' term
was bogus — virtio-scsi-pci comes from the hotplug-spares pool via
hotplug_port_owner[] in vmspawn-qmp.c, never from a builtin port (see
the comment in assign_pcie_ports()). The +1 'network' and +1 'vsock'
terms are now conditional on arg_network_stack and use_vsock. Bind
volumes were missing entirely. And the per-drive accounting now
mirrors assign_pcie_ports()'s skip-SCSI behaviour: non-SCSI drives
(root + extras + bind volumes) take one builtin port each, SCSI
drives take none — they share a controller drawn from the hotplug
pool at device-add time. Cap at 120 ports (15 device-numbers × 8) so
we cannot run off the end of the 5-bit PCI device-number space — the
usable range starting at 0x10 ends at 0x1e because ICH9 LPC sits at
0x1f.0 single-function, blocking the rest of that slot for
multifunction packing.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-13 23:48:53 +02:00
Christian Brauner 91575e3a5a varlink-io.systemd.MachineInstance,vmspawn: treat AddStorage/RemoveStorage name as opaque
The 'name' field on AddStorage and RemoveStorage was documented as
'<provider>:<volume>' and enforced via machine_storage_name_split() at
the varlink boundary. That form is only the convention machinectl
inherits from the StorageProvider routing path; the API itself only
needs a unique identifier the caller can re-use to detach the binding.

Drop the strict format check, require only a non-empty string, and
update the IDL docs to describe the field as a caller-supplied
identifier with machinectl's convention as a non-normative example.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:08 +02:00
Christian Brauner 968d189414 vmspawn: reject O_PATH and O_WRONLY fds in AddStorage
An fd opened O_PATH cannot be read, and an O_WRONLY fd cannot serve as
a backing file for a virtual disk image. Reject both at the bind-volume
entry point with -EBADF instead of letting the request proceed to QMP
where QEMU's file backend would fail to read from the fd. The
ReplaceStorage entry point grew the same checks in parallel.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:07 +02:00
Christian Brauner b9155f789c vmspawn: implement io.systemd.MachineInstance.ReplaceStorage
Wire up the runtime hot-swap Varlink method. The signature mirrors
AddStorage minus 'config': the device frontend (virtio-blk,
virtio-scsi, nvme, scsi-cd) doesn't change, only the backing file
behind it. Read-only/read-write may flip based on the new fd's
O_ACCMODE; scsi-cd is forced read-only to match the boot-time policy.

QMP sequence (entry: vmspawn_qmp_replace_block_device):

  add-fd                          → on_replace_observe_stage
  blockdev-add (new file)         → on_replace_blockdev_add_complete
  remove-fd (new fdset)           → on_replace_observe_stage
  blockdev-reopen (format)        → on_replace_blockdev_reopen_complete
                                    [commit + fire trailing del]
  blockdev-del (old file)         → on_replace_old_blockdev_del_complete

The reopen options must be a superset of every option that
qmp_build_blockdev_add_format() may emit, otherwise reopen rejects
'Cannot reset option X to default'. The 'file' field is a string
reference to the new file node — case 3 of the schema in
qemu/qapi/block-core.json:5034-5040 ("the current child is replaced
with that other node"). The format node's qmp_node_name is preserved
so the device frontend's drive=<X> binding does not move.

ReplaceCtx tracks the per-call state with a refcount mirroring the
add-stage drive-info pattern. On any pre-commit failure replace_fail
tears down whatever new-side state we created on the wire and replies
on drive->link via reply_qmp_error (disconnect → NotConnected). On
post-commit del failure we log a warning, leak the orphan, and reply
success — the swap itself succeeded and the leak resolves at VM exit.

file_generation is bumped before issuing blockdev-add so failed
attempts cannot collide on node-name when the user retries.

Errors:
  NoSuchStorage     - drive not in the registry
  StorageImmutable  - drive lacks QMP_DRIVE_REMOVABLE (boot-time)
  EBUSY             - add still pending or another replace/remove in flight
  NotConnected      - QMP transport disconnect during the chain
  EIO               - QEMU rejected blockdev-reopen

Also gates RemoveStorage on REPLACE_PENDING so a device_del cannot
race a mid-flight blockdev-reopen on the same drive.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:07 +02:00
Christian Brauner 8dc5dbe3c0 vmspawn: split blockdev-add into separate file and format calls
The current vmspawn_qmp_add_block_device() emits a single blockdev-add
that combines the format-level node ("vmspawn-N-storage") with an
inline file child. QEMU's qmp_blockdev_add() only marks the top-level
returned BDS as monitor-owned (qemu/blockdev.c:3440); inline children
are NOT, so qmp_blockdev_del() rejects them with "Node X is not owned
by the monitor" (qemu/blockdev.c:3513-3517).

To prepare for ReplaceStorage — which needs to swap the file child of
an existing format node via blockdev-reopen, and then blockdev-del the
old file node — make the file node monitor-owned by issuing it as its
own blockdev-add call. The 4-stage add chain becomes 5 stages:

  add-fd
  blockdev-add (file)    → on_add_file_node_stage   sets FILE_NODE_ADDED
  blockdev-add (format)  → on_add_format_node_stage sets BLOCKDEV_ADDED
  remove-fd
  device_add

DriveInfo gains qmp_file_node_name ("vmspawn-N-file-G", G a generation
counter bumped on every replace), file_generation, and a stashed
fdset_id so future ReplaceStorage can target both for cleanup.

vmspawn_qmp_block_device_teardown() now deletes both nodes in order —
format first, then file — because the format holds a strong reference
to its file child and a file-first del is rejected with "Node X is
busy: node is used as 'file' of Y".

Folds bridge->features VMSPAWN_QMP_FEATURE_IO_URING into the file
node's flags so the new path inherits io_uring just like the old
inline form did. The format-level options (read-only, discard,
discard-no-unref) are unchanged.

The ephemeral path is structurally already separate file+format with
monitor-owned children; no behavioural change there beyond the
on_add_blockdev_stage → on_add_format_node_stage rename.

Drops the now-unused qmp_build_blockdev_add_inline() helper.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:07 +02:00
Daan De Meyer 2fcf963273 vmspawn: Add missing error logging 2026-05-12 08:16:24 +02:00
Daan De Meyer 54776b5fce vmspawn: Use builtin vdagent instead of spicevmc
The builtin one also makes the clipboard and such work. spicevmc
is only required for remote desktop use cases, so let's use the 
builtin one instead.
2026-05-11 21:10:48 +02:00
Daan De Meyer 6f3fa71775 vmspawn: Attach a USB keyboard in GUI mode
EDK2's UsbKbDxe is the only driver that registers a default HII
keyboard layout via the HII database protocol; the PS/2 driver does
not. Adding a USB xHCI controller and usb-kbd in CONSOLE_GUI mode
gives us a layout to query, which systemd-boot exports through the
LoaderKeyboardLayout EFI variable — useful for exercising that
codepath end-to-end.
2026-05-11 16:16:14 +01:00
Paul Meyer 67e23e0e05 vmspawn: search XDG_DATA_DIRS for QEMU firmware
get_firmware_search_dirs() previously hardcoded /usr/share/qemu/firmware
as the only system-wide search path. That assumption breaks on
distributions that deliberately do not populate /usr/share, making
vmspawn fail: "Failed to find OVMF config: No such file or directory".
NixOS exposes those firmware locations through XDG_DATA_DIRS.

Extend the search list with XDG_DATA_HOME/XDG_DATA_DIRS. This is the
standard XDG mechanism and is already what QEMU itself uses for the same
descriptors, so behavior matches user expectations across tooling.

To avoid regressing setups where user has set XDG_DATA_DIRS to a custom
value that omits /usr/share, keep /usr/share/qemu/firmware as an
unconditional fallback.

Precedence is unchanged: XDG_CONFIG_HOME/qemu/firmware still wins
over /etc/qemu/firmware, which still wins over any shared-data dir.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
2026-05-07 10:33:01 +02:00
Christian Brauner cbadda5e60 vmspawn: reject --bind-volume= duplicates at parse time
bind_volume_parse() does not look at peers, so passing the same
PROVIDER:VOLUME twice on the command line silently produces two parsed
entries in arg_bind_volumes. vmspawn_bind_volume_acquire() then builds
two DriveInfo with identical d->id ("<provider>:<volume>"). At boot,
bridge_register_drive() puts d->id into the b->block_devices hashmap;
the second insert returns -EEXIST and the user sees a bare "File
exists" with no context for which volume is responsible.

Reject the collision at the parse site with a linear scan over the
existing array — n_items is small (one entry per --bind-volume on the
command line), and a clear error message naming the offending volume
is much more useful than the late EEXIST from the QMP setup loop.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-06 14:30:37 +02:00
Christian Brauner e7eac392ef vmspawn: implement io.systemd.MachineInstance.AddStorage / RemoveStorage
Wire up the runtime hotplug Varlink methods on the per-VM control
socket:

  AddStorage  → take fd from the link, look up the DiskType from the
                'config' field, build a DriveInfo flagged
                QMP_DRIVE_REMOVABLE, dispatch to
                vmspawn_qmp_add_block_device(). Reply delivered async
                by on_add_device_add_complete() once the guest sees
                the device.

  RemoveStorage → forward the user-visible name to
                  vmspawn_qmp_remove_block_device(); the existing
                  device_del / DEVICE_DELETED / blockdev-del chain
                  replies on the link.

Add SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT to the server flags so
clients can push storage fds across via sd_varlink_push_fd().

Maps -EEXIST → StorageExists and -EOPNOTSUPP/-EINVAL →
ConfigNotSupported in the AddStorage handler so callers see the
specific MachineInstance errors.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-06 10:30:17 +02:00
Christian Brauner 413fd62dd7 vmspawn: add --bind-volume= command line option
systemd-vmspawn --bind-volume=PROVIDER:VOLUME[:CONFIG][:K=V,...]

For each --bind-volume passed at startup, vmspawn calls Acquire() on
the named StorageProvider and attaches the resulting fd to the VM as
an additional drive. The drive is identified by the user-visible name
'<provider>:<volume>' on the bridge — that is also the handle used
later when machinectl unbind-volume detaches drives at runtime
(though boot-time drives like these are NOT removable; that is the
StorageImmutable behaviour added earlier).

The colon grammar is parsed by the shared bind_volume_parse() helper.
The 3rd 'config' field selects the guest device type from the
disk_type_table[] vocabulary (virtio-blk, virtio-scsi, nvme, scsi-cd);
empty defaults to virtio-blk per the TASK grammar.

Wiring lives next to the existing --extra-drive setup: parse_argv()
appends a parsed BindVolume to arg_bind_volumes, and prepare_device_info()
hands the array to vmspawn_bind_volume_prepare_boot() which Acquires
each volume and pushes a DriveInfo onto the existing drives array.
PCIe port assignment (assign_pcie_ports()) and the QMP setup loop pick
them up automatically.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-06 10:30:17 +02:00
Christian Brauner a34ad7f7b0 vmspawn: add vmspawn-bind-volume glue
This is vmspawn's per-backend code for the StorageProvider integration.
Other backends (future systemd-nspawn, future service-manager
BindVolume=) consume the same shared parser and Acquire helper but
each provides its own attach/detach glue; this is vmspawn's.

  - disk_type_from_bind_volume_config() turns the opaque BindVolume
    'config' field (e.g. "scsi-cd") into a DiskType. Empty defaults to
    virtio-blk to match the --bind-volume CLI grammar.

  - vmspawn_bind_volume_acquire() takes a parsed BindVolume, calls
    storage_acquire_volume() for the fd, and builds a DriveInfo ready
    for vmspawn_qmp_setup_drives() (boot) or vmspawn_qmp_add_block_device()
    (hotplug). Rejects directory-typed volumes (vmspawn block devices
    need a regular file or a host block device).

  - vmspawn_bind_volume_attach_fd() is the runtime path: takes a fd
    that was already pushed across by an AddStorage caller plus the
    name+config it specified, builds the DriveInfo with
    QMP_DRIVE_REMOVABLE set and a varlink link, and dispatches to
    vmspawn_qmp_add_block_device(). Reply is delivered asynchronously
    by the existing on_add_device_add_complete() callback.

  - vmspawn_bind_volume_prepare_boot() is a thin loop the boot-time
    path uses to populate DriveInfos.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-06 10:30:17 +02:00
Christian Brauner 0e911e4164 vmspawn: track removability as a QmpDriveFlags bit and expose add_block_device
Drives attached at boot via the existing CLI options (--image,
--extra-drive) must not be detachable at runtime via the upcoming
RemoveStorage Varlink method, while drives added at runtime via
AddStorage must be. Track this distinction with a new QMP_DRIVE_REMOVABLE
property flag — placed alongside QMP_DRIVE_BLOCK_DEVICE, not in the
transient BlockDeviceStateFlags state-machine, since "may be removed"
is a permanent property of the drive.

vmspawn_qmp_remove_block_device() now early-rejects unknown ids with
io.systemd.MachineInstance.NoSuchStorage and immutable drives with
io.systemd.MachineInstance.StorageImmutable.

vmspawn_qmp_add_block_device() loses its 'static' qualifier and gets a
declaration in the header, so the runtime hotplug path
(vmspawn-bind-volume.c, next) can dispatch into it directly.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-06 10:30:17 +02:00
Luca Boccassi 5eb256c99b vmspawn-qmp: take temporary ref in drive_info_add_fail
drive_info_add_fail() calls bridge_unregister_drive() followed by
drive_info_unref(), then continues to access the DriveInfo object.
While all current callers hold their own reference, it is a bit
fragile and it trips static analyzers. Take a local reference.

CID#1655804

Follow-up for 1d0a8e5dbd
2026-05-05 14:39:38 +01:00
Lennart Poettering 99ce7a0770 options: get rid of "on_error" parameter to FOREACH_OPTION
I am really not a fan of full code lines passed to macros as parameters.
Let's get rid of the 3rd parameter of FOREACH_OPTION() hence:

1. Let's return errors just as a regular value (though a negative one),
   that can be handled via a OPTION_ERROR case statement for the switch.
   This normalizes handling of the error, just like any other event
   returned by the option parser.

2. In order to avoid exploding the amount of boilerplate in each use
   (that just propagates the error on OPTION_ERROR), let's then
   introduce an explicit FOREACH_OPTION_OR_RETURN(), that returns from
   the calling function on its own (and makes that clear in the name).

Together this cleans up, normalizes the logic and shortens the code.
2026-04-30 11:12:33 +01:00
Daan De Meyer 9d2f5b4611 fundamental/cleanup: add CLEANUP_ELEMENTS() and DEFINE_POINTER_ARRAY_CLEAR_FUNC()
DEFINE_POINTER_ARRAY_CLEAR_FUNC() generates a helper of the form
helper_array_clear(T *array, size_t n) that drops each element but does
not free the array itself, parallel to DEFINE_POINTER_ARRAY_FREE_FUNC()
for cases where the array has automatic storage duration.

CLEANUP_ELEMENTS() pairs with these helpers to provide a _cleanup_-like
attribute for fixed-size arrays: the bound is taken from ELEMENTSOF(),
and the helper is invoked across the elements at scope exit. Compared to
CLEANUP_ARRAY(), the storage is neither freed nor zeroed.

Migrate various logic across the tree over to the new macros.

sd-device: use DEFINE_POINTER_ARRAY_CLEAR_FUNC() for sd_device_unref_array_clear()

Replace the local device_unref_many() helper with the macro-generated
equivalent.

format-table: switch help-table arrays to CLEANUP_ELEMENTS()

Generate table_unref_array_clear() via DEFINE_POINTER_ARRAY_CLEAR_FUNC()
and convert the help-table arrays in bootctl, cryptenroll, nspawn,
repart and vmspawn to CLEANUP_ELEMENTS(). The arrays no longer need a
trailing NULL slot, so the size matches ELEMENTSOF() of the groups
array.

firewall-util: switch netlink message arrays to CLEANUP_ELEMENTS()

Generate sd_netlink_message_unref_array_clear() via
DEFINE_POINTER_ARRAY_CLEAR_FUNC() in place of the NULL-terminated
sd_netlink_message_unref_many(), and convert the two stack arrays of
sd_netlink_message pointers to CLEANUP_ELEMENTS().
2026-04-30 09:15:34 +02:00
Daan De Meyer ed1373f889 fork-notify: Use callback instead of argv NULL code path with return (#41813) 2026-04-28 10:15:31 +02:00
Zbigniew Jędrzejewski-Szmek b2d72eabcd tree-wide: change option_parse() to return option and arg via internal state (#41833) 2026-04-27 19:42:52 +02:00
Kai Lüke 4dbcc319ca fork-notify: Use callback instead of argv NULL code path with return
In 012d87c1fc/cc8f398202 it was made possible for fork_notify() to
return in the child but at that point all FDs were closed and the
_cleanup path from the return causes assertion failures due to invalid
FDs in notify_event_source/event, leading to a vmspawn failing to start
with a SIGABRT logged in coredump.
Instead of TAKE_PTR on a bunch of things which is fragile, rather avoid
the return and instead add an explicit callback handler and guarantee
to exit directly after it. A userdata argument is also added but not
used yet I think it's quite normal to have for a callback.
2026-04-27 22:14:05 +09:00
Christian Brauner 0d6d2c9ebd vmspawn-varlink: treat QMP disconnect as success for Terminate
QMP "quit" tells QEMU to exit, which races the reply with the socket
EOF: sometimes the disconnect lands in qmp_client_fail_pending() with
-ECONNRESET before the reply has been parsed. The shared completion
callback then translates that into io.systemd.MachineInstance.NotConnected,
turning the desired outcome into a varlink error.

This is exactly what TEST-87-AUX-UTILS-VM exposes during its repeated
start/pause/resume/terminate stress loop: a successful Pause/Describe
followed milliseconds later by a Terminate that fails with NotConnected
when the disconnect path wins the race.

Give Terminate its own completion callback that treats disconnect-class
errors as success, since QEMU shutting down is the whole point of "quit".
The other simple commands (Pause, Resume, PowerOff, Reboot) keep the
existing semantics: they expect QMP to remain alive, so NotConnected is
the correct reply for them.

Link: https://github.com/systemd/systemd/actions/runs/24986080288/job/73159585425?pr=41835
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-04-27 15:07:26 +02:00
Zbigniew Jędrzejewski-Szmek dbe3602abe nspawn,vmspawn: fix state optarg uses 2026-04-27 11:33:13 +02:00
Zbigniew Jędrzejewski-Szmek 5ff0ccaf8b tree-wide: change option_parse() to return option and arg via internal state
It was requested to make the 'c', 'opt', and 'arg' params the same, i.e.
defined through the FOREACH_OPTION macro. But we can't do that easily,
because 'c' was defined in the for loop definition, and we can only
define variables of the same type in that way. Also, in some cases we
need only 'c', in other cases with need 'c' and 'arg, in some cases 'c'
and 'opt', and in other cases all three. We'd need to either
conditionalize or mark those variables with _unused_ to deal with
compiler warnings. But a different approach works quite nicely: add
state.opt and state.arg to show the current option and it's argument.
(The short names are picked on purpose to reduce verbosity since those
are used a lot.)
2026-04-27 11:33:10 +02:00
Christian Brauner 67cd0977cd 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 93c04d4aba vmspawn-qmp: add vmspawn_qmp_remove_block_device
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 <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 1d0a8e5dbd vmspawn-qmp: add the hotplug-capable block-device add machinery
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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 1dbfab132a vmspawn-qmp: keep the event loop running on post-setup QMP failures
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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner b75b6041e0 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner a57fbf4e6c vmspawn-qmp: derive QMP node and device ids from a bridge counter
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-<N>-storage"  (blockdev node-name)
  qmp_device_id   = "vmspawn-<N>-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-<N>-{base-file,base-fmt,overlay-file}
convention; the blockdev-create job-id becomes
vmspawn-<N>-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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner f0c1dc50ef 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 06816dfee3 vmspawn-qmp: pass bridge to on_cont_complete via invoke userdata
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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 9ed1af2bd1 vmspawn-varlink: treat empty event subscription filter as catch-all
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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 5b3a9f20a6 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 34d1219763 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner b34413db8e 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 6920dfc0f0 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) <brauner@kernel.org>
2026-04-24 14:39:25 +02:00
Christian Brauner 4caa73563a 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) <brauner@kernel.org>
2026-04-24 14:39:24 +02:00
Christian Brauner 74a07cb774 vmspawn: use qemu_device_driver_to_string() in resolve_disk_driver
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) <brauner@kernel.org>
2026-04-24 14:39:24 +02:00
Christian Brauner 258c667845 shared: extract disk-spec parsing into machine-util
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) <brauner@kernel.org>
2026-04-24 14:39:24 +02:00
Christian Brauner c697634ae2 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) <brauner@kernel.org>
2026-04-24 14:39:24 +02:00