]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: Use qemu config files
authorDaan De Meyer <daan@amutable.com>
Wed, 25 Mar 2026 13:20:03 +0000 (14:20 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 25 Mar 2026 21:12:54 +0000 (22:12 +0100)
Let's avoid generating giant qemu command lines by using qemu config
files instead.

src/vmspawn/meson.build
src/vmspawn/vmspawn-qemu-config.c [new file with mode: 0644]
src/vmspawn/vmspawn-qemu-config.h [new file with mode: 0644]
src/vmspawn/vmspawn.c

index a836b316578a0f1b2326fe50dfb9b5396398c40a..722e6a52cc7f274be40ddb67e0a5195da3623152 100644 (file)
@@ -6,6 +6,7 @@ endif
 
 vmspawn_sources = files(
         'vmspawn.c',
+        'vmspawn-qemu-config.c',
         'vmspawn-settings.c',
         'vmspawn-scope.c',
         'vmspawn-mount.c',
diff --git a/src/vmspawn/vmspawn-qemu-config.c b/src/vmspawn/vmspawn-qemu-config.c
new file mode 100644 (file)
index 0000000..08908ff
--- /dev/null
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdarg.h>
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "log.h"
+#include "vmspawn-qemu-config.h"
+
+static bool qemu_config_type_valid(const char *type) {
+        return !strchr(type, '\n');
+}
+
+static bool qemu_config_id_valid(const char *id) {
+        return !strpbrk(id, "\"\n");
+}
+
+static bool qemu_config_key_name_valid(const char *key) {
+        return !strpbrk(key, "=\n");
+}
+
+static bool qemu_config_value_valid(const char *value) {
+        return !strpbrk(value, "\"\n");
+}
+
+int qemu_config_key(FILE *f, const char *key, const char *value) {
+        assert(f);
+        assert(key);
+        assert(value);
+
+        if (!qemu_config_key_name_valid(key))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "QEMU config key '%s' contains '=' or newline.", key);
+        if (!qemu_config_value_valid(value))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "QEMU config value '%s' contains quote or newline.", value);
+
+        if (fprintf(f, "  %s = \"%s\"\n", key, value) < 0)
+                return -errno_or_else(EIO);
+
+        return 0;
+}
+
+int qemu_config_keyf(FILE *f, const char *key, const char *format, ...) {
+        _cleanup_free_ char *value = NULL;
+        va_list ap;
+        int r;
+
+        assert(f);
+        assert(key);
+        assert(format);
+
+        va_start(ap, format);
+        r = vasprintf(&value, format, ap);
+        va_end(ap);
+        if (r < 0)
+                return -ENOMEM;
+
+        return qemu_config_key(f, key, value);
+}
+
+int qemu_config_section_impl(FILE *f, const char *type, const char *id, ...) {
+        va_list ap;
+        int r;
+
+        assert(f);
+        assert(type);
+
+        if (!qemu_config_type_valid(type))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "QEMU config section type '%s' contains newline.", type);
+
+        if (id) {
+                if (!qemu_config_id_valid(id))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "QEMU config section id '%s' contains quote or newline.", id);
+                fprintf(f, "\n[%s \"%s\"]\n", type, id);
+        } else
+                fprintf(f, "\n[%s]\n", type);
+
+        va_start(ap, id);
+        for (;;) {
+                const char *key = va_arg(ap, const char *);
+                if (!key)
+                        break;
+
+                const char *value = ASSERT_PTR(va_arg(ap, const char *));
+
+                r = qemu_config_key(f, key, value);
+                if (r < 0) {
+                        va_end(ap);
+                        return r;
+                }
+        }
+        va_end(ap);
+
+        if (ferror(f))
+                return -errno_or_else(EIO);
+
+        return 0;
+}
diff --git a/src/vmspawn/vmspawn-qemu-config.h b/src/vmspawn/vmspawn-qemu-config.h
new file mode 100644 (file)
index 0000000..cd782be
--- /dev/null
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdio.h>
+
+#include "macro.h"
+
+/* Helpers for writing QEMU -readconfig INI-style config files.
+ *
+ * QEMU config format:
+ *   [type "id"]
+ *     key = "value"
+ *
+ * Usage:
+ *   qemu_config_section(f, "device", "rng0",
+ *                       "driver", "virtio-rng-pci",
+ *                       "rng", "rng0");
+ */
+
+/* Write a single key = "value" pair (for conditional keys added after a section header) */
+int qemu_config_key(FILE *f, const char *key, const char *value);
+
+/* Write a single key with a printf-formatted value */
+int qemu_config_keyf(FILE *f, const char *key, const char *format, ...) _printf_(3, 4);
+
+/* Write a section header with key-value pairs. Varargs are alternating key, value strings. */
+int qemu_config_section_impl(FILE *f, const char *type, const char *id, ...) _sentinel_;
+#define qemu_config_section(...) qemu_config_section_impl(__VA_ARGS__, NULL)
index f730a756e28edbaa3fa453fecb69227949dc876f..2e41e31d712d720afe98c11bdb7c09dfc316052d 100644 (file)
@@ -37,6 +37,7 @@
 #include "event-util.h"
 #include "extract-word.h"
 #include "fd-util.h"
+#include "fileio.h"
 #include "fork-notify.h"
 #include "format-util.h"
 #include "fs-util.h"
@@ -85,6 +86,7 @@
 #include "user-util.h"
 #include "utf8.h"
 #include "vmspawn-mount.h"
+#include "vmspawn-qemu-config.h"
 #include "vmspawn-register.h"
 #include "vmspawn-scope.h"
 #include "vmspawn-settings.h"
@@ -2040,7 +2042,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         _cleanup_free_ int *pass_fds = NULL;
         sd_event_source **children = NULL;
         size_t n_children = 0, n_pass_fds = 0;
-        const char *accel;
         int r;
 
         CLEANUP_ARRAY(children, n_children, fork_notify_terminate_many);
@@ -2119,16 +2120,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         if (r < 0)
                 return r;
 
-        _cleanup_free_ char *machine = NULL;
-        const char *shm = arg_directory || arg_runtime_mounts.n_mounts != 0 ? ",memory-backend=mem" : "";
-        const char *hpet = ARCHITECTURE_SUPPORTS_HPET ? ",hpet=off" : "";
-        if (ARCHITECTURE_SUPPORTS_SMM)
-                machine = strjoin("type=" QEMU_MACHINE_TYPE ",smm=", on_off(ovmf_config->supports_sb), shm, hpet);
-        else
-                machine = strjoin("type=" QEMU_MACHINE_TYPE, shm, hpet);
-        if (!machine)
-                return log_oom();
-
         if (arg_linux) {
                 kernel = strdup(arg_linux);
                 if (!kernel)
@@ -2151,45 +2142,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         if (asprintf(&mem, "%" PRIu64 "M", DIV_ROUND_UP(arg_ram, U64_MB)) < 0)
                 return log_oom();
 
-        cmdline = strv_new(
-                qemu_binary,
-                "-machine", machine,
-                "-smp", arg_cpus ?: "1",
-                "-m", mem,
-                "-object", "rng-random,filename=/dev/urandom,id=rng0",
-                "-device", "virtio-rng-pci,rng=rng0,id=rng-device0",
-                "-device", "virtio-balloon,free-page-reporting=on"
-        );
-        if (!cmdline)
-                return log_oom();
-
-        if (!sd_id128_is_null(arg_uuid))
-                if (strv_extend_many(&cmdline, "-uuid", SD_ID128_TO_UUID_STRING(arg_uuid)) < 0)
-                        return log_oom();
-
-        if (ARCHITECTURE_SUPPORTS_VMGENID) {
-                /* Derive a vmgenid automatically from the invocation ID, in a deterministic way. */
-                sd_id128_t vmgenid;
-                r = sd_id128_get_invocation_app_specific(SD_ID128_MAKE(bd,84,6d,e3,e4,7d,4b,6c,a6,85,4a,87,0f,3c,a3,a0), &vmgenid);
-                if (r < 0) {
-                        log_debug_errno(r, "Failed to get invocation ID, making up randomized vmgenid: %m");
-
-                        r = sd_id128_randomize(&vmgenid);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to make up randomized vmgenid: %m");
-                }
-
-                if (strv_extend(&cmdline, "-device") < 0)
-                        return log_oom();
-
-                if (strv_extendf(&cmdline, "vmgenid,guid=" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(vmgenid)) < 0)
-                        return log_oom();
-        }
-
-        /* if we are going to be starting any units with state then create our runtime dir */
+        /* Create runtime directory for the QEMU config file and other state */
         _cleanup_free_ char *runtime_dir = NULL;
         _cleanup_(rm_rf_physical_and_freep) char *runtime_dir_destroy = NULL;
-        if (arg_tpm != 0 || arg_directory || arg_runtime_mounts.n_mounts != 0 || arg_pass_ssh_key) {
+        {
                 _cleanup_free_ char *subdir = NULL;
 
                 if (asprintf(&subdir, "systemd/vmspawn.%" PRIx64, random_u64()) < 0)
@@ -2214,6 +2170,88 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 log_debug("Using runtime directory: %s", runtime_dir);
         }
 
+        /* Build a QEMU config file for -readconfig. Items that can be expressed as QemuOpts sections go
+         * here; things that require cmdline-only switches (e.g. -kernel, -smbios, -nographic, --add-fd)
+         * are added to the cmdline strv below. */
+        _cleanup_fclose_ FILE *config_file = NULL;
+        _cleanup_(unlink_and_freep) char *config_path = NULL;
+        r = fopen_temporary_child(runtime_dir, &config_file, &config_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create QEMU config file: %m");
+
+        r = qemu_config_section(config_file, "machine", /* id= */ NULL,
+                                "type", QEMU_MACHINE_TYPE);
+        if (r < 0)
+                return r;
+
+        if (ARCHITECTURE_SUPPORTS_SMM) {
+                r = qemu_config_key(config_file, "smm", on_off(ovmf_config->supports_sb));
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_directory || arg_runtime_mounts.n_mounts != 0) {
+                r = qemu_config_key(config_file, "memory-backend", "mem");
+                if (r < 0)
+                        return r;
+        }
+
+        if (ARCHITECTURE_SUPPORTS_HPET) {
+                r = qemu_config_key(config_file, "hpet", "off");
+                if (r < 0)
+                        return r;
+        }
+
+        r = qemu_config_section(config_file, "object", "rng0",
+                                "qom-type", "rng-random",
+                                "filename", "/dev/urandom");
+        if (r < 0)
+                return r;
+
+        r = qemu_config_section(config_file, "device", "rng-device0",
+                                "driver", "virtio-rng-pci",
+                                "rng", "rng0");
+        if (r < 0)
+                return r;
+
+        r = qemu_config_section(config_file, "device", "balloon0",
+                                "driver", "virtio-balloon",
+                                "free-page-reporting", "on");
+        if (r < 0)
+                return r;
+
+        if (ARCHITECTURE_SUPPORTS_VMGENID) {
+                sd_id128_t vmgenid;
+                r = sd_id128_get_invocation_app_specific(SD_ID128_MAKE(bd,84,6d,e3,e4,7d,4b,6c,a6,85,4a,87,0f,3c,a3,a0), &vmgenid);
+                if (r < 0) {
+                        log_debug_errno(r, "Failed to get invocation ID, making up randomized vmgenid: %m");
+
+                        r = sd_id128_randomize(&vmgenid);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to make up randomized vmgenid: %m");
+                }
+
+                r = qemu_config_section(config_file, "device", "vmgenid0",
+                                        "driver", "vmgenid");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_keyf(config_file, "guid", SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(vmgenid));
+                if (r < 0)
+                        return r;
+        }
+
+        /* Start building the cmdline for items that must remain as command line arguments */
+        cmdline = strv_new(qemu_binary,
+                           "-smp", arg_cpus ?: "1",
+                           "-m", mem);
+        if (!cmdline)
+                return log_oom();
+
+        if (!sd_id128_is_null(arg_uuid))
+                if (strv_extend_many(&cmdline, "-uuid", SD_ID128_TO_UUID_STRING(arg_uuid)) < 0)
+                        return log_oom();
+
         _cleanup_close_ int delegate_userns_fd = -EBADF, tap_fd = -EBADF;
         if (arg_network_stack == NETWORK_STACK_TAP) {
                 if (have_effective_cap(CAP_NET_ADMIN) <= 0) {
@@ -2238,11 +2276,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         if (tap_fd < 0)
                                 return log_error_errno(tap_fd, "Failed to allocate network tap device: %m");
 
-                        r = strv_extend(&cmdline, "-nic");
+                        r = strv_extend(&cmdline, "-netdev");
+                        if (r < 0)
+                                return log_oom();
+
+                        r = strv_extendf(&cmdline, "tap,id=net0,fd=%i", tap_fd);
                         if (r < 0)
                                 return log_oom();
 
-                        r = strv_extendf(&cmdline, "tap,fd=%i,model=virtio-net-pci", tap_fd);
+                        r = strv_extend_many(&cmdline, "-device", "virtio-net-pci,netdev=net0");
                         if (r < 0)
                                 return log_oom();
 
@@ -2267,30 +2309,46 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         } else
                                 mac_vm = arg_network_provided_mac;
 
-                        r = strv_extend(&cmdline, "-nic");
+                        r = qemu_config_section(config_file, "netdev", "net0",
+                                                "type", "tap",
+                                                "ifname", tap_name,
+                                                "script", "no",
+                                                "downscript", "no");
                         if (r < 0)
-                                return log_oom();
+                                return r;
 
-                        r = strv_extendf(&cmdline, "tap,ifname=%s,script=no,downscript=no,model=virtio-net-pci,mac=%s", tap_name, ETHER_ADDR_TO_STR(&mac_vm));
+                        r = qemu_config_section(config_file, "device", "nic0",
+                                                "driver", "virtio-net-pci",
+                                                "netdev", "net0",
+                                                "mac", ETHER_ADDR_TO_STR(&mac_vm));
                         if (r < 0)
-                                return log_oom();
+                                return r;
                 }
-        } else if (arg_network_stack == NETWORK_STACK_USER)
-                r = strv_extend_many(&cmdline, "-nic", "user,model=virtio-net-pci");
-        else
-                r = strv_extend_many(&cmdline, "-nic", "none");
-        if (r < 0)
-                return log_oom();
+        } else if (arg_network_stack == NETWORK_STACK_USER) {
+                r = qemu_config_section(config_file, "netdev", "net0",
+                                        "type", "user");
+                if (r < 0)
+                        return r;
 
-        /* A shared memory backend might increase ram usage so only add one if actually necessary for virtiofsd. */
-        if (arg_directory || arg_runtime_mounts.n_mounts != 0) {
-                r = strv_extend(&cmdline, "-object");
+                r = qemu_config_section(config_file, "device", "nic0",
+                                        "driver", "virtio-net-pci",
+                                        "netdev", "net0");
+                if (r < 0)
+                        return r;
+        } else {
+                r = strv_extend_many(&cmdline, "-nic", "none");
                 if (r < 0)
                         return log_oom();
+        }
 
-                r = strv_extendf(&cmdline, "memory-backend-memfd,id=mem,size=%s,share=on", mem);
+        /* A shared memory backend might increase ram usage so only add one if actually necessary for virtiofsd. */
+        if (arg_directory || arg_runtime_mounts.n_mounts != 0) {
+                r = qemu_config_section(config_file, "object", "mem",
+                                        "qom-type", "memory-backend-memfd",
+                                        "size", mem,
+                                        "share", "on");
                 if (r < 0)
-                        return log_oom();
+                        return r;
         }
 
         bool use_vsock = arg_vsock > 0 && ARCHITECTURE_SUPPORTS_SMBIOS;
@@ -2308,10 +2366,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         }
 
         if (use_kvm && kvm_device_fd >= 0) {
-                /* /dev/fdset/1 is magic string to tell qemu where to find the fd for /dev/kvm
-                 * we use this so that we can take a fd to /dev/kvm and then give qemu that fd */
-                accel = "kvm,device=/dev/fdset/1";
-
                 r = strv_extend(&cmdline, "--add-fd");
                 if (r < 0)
                         return log_oom();
@@ -2324,14 +2378,18 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         return log_oom();
 
                 pass_fds[n_pass_fds++] = kvm_device_fd;
-        } else if (use_kvm)
-                accel = "kvm";
-        else
-                accel = "tcg";
 
-        r = strv_extend_many(&cmdline, "-accel", accel);
-        if (r < 0)
-                return log_oom();
+                r = qemu_config_section(config_file, "accel", /* id= */ NULL,
+                                        "accel", "kvm",
+                                        "device", "/dev/fdset/1");
+                if (r < 0)
+                        return r;
+        } else {
+                r = qemu_config_section(config_file, "accel", /* id= */ NULL,
+                                        "accel", use_kvm ? "kvm" : "tcg");
+                if (r < 0)
+                        return r;
+        }
 
         _cleanup_close_ int child_vsock_fd = -EBADF;
         unsigned child_cid = arg_vsock_cid;
@@ -2350,13 +2408,18 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 if (r < 0)
                         return log_error_errno(r, "Failed to fix CID for the guest VSOCK socket: %m");
 
-                r = strv_extend(&cmdline, "-device");
+                r = qemu_config_section(config_file, "device", "vsock0",
+                                        "driver", "vhost-vsock-pci");
                 if (r < 0)
-                        return log_oom();
+                        return r;
 
-                r = strv_extendf(&cmdline, "vhost-vsock-pci,guest-cid=%u,vhostfd=%d", child_cid, device_fd);
+                r = qemu_config_keyf(config_file, "guest-cid", "%u", child_cid);
                 if (r < 0)
-                        return log_oom();
+                        return r;
+
+                r = qemu_config_keyf(config_file, "vhostfd", "%d", device_fd);
+                if (r < 0)
+                        return r;
 
                 if (!GREEDY_REALLOC(pass_fds, n_pass_fds + 1))
                         return log_oom();
@@ -2364,6 +2427,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 pass_fds[n_pass_fds++] = device_fd;
         }
 
+        /* -cpu stays on cmdline since not all flags are supported in config */
         r = strv_extend_many(&cmdline, "-cpu",
 #ifdef __x86_64__
                              "max,hv_relaxed,hv-vapic,hv-time"
@@ -2390,69 +2454,105 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 if (master < 0)
                         return log_error_errno(master, "Failed to setup pty: %m");
 
-                if (strv_extend_many(
-                                &cmdline,
-                                "-nographic",
-                                "-nodefaults",
-                                "-device", "virtio-serial-pci,id=vmspawn-virtio-serial-pci",
-                                "-chardev") < 0)
+                r = strv_extend_many(&cmdline, "-nographic", "-nodefaults");
+                if (r < 0)
                         return log_oom();
 
-                if (strv_extend_joined(&cmdline, "serial,id=console,path=", pty_path) < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci",
+                                        "driver", "virtio-serial-pci");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "chardev", "console",
+                                        "backend", "serial",
+                                        "path", pty_path);
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "device", "virtconsole0",
+                                        "driver", "virtconsole",
+                                        "chardev", "console");
+                if (r < 0)
+                        return r;
 
-                r = strv_extend_many(
-                                &cmdline,
-                                "-device", "virtconsole,chardev=console");
                 break;
         }
 
         case CONSOLE_GUI:
-                /* Enable support for the qemu guest agent for clipboard sharing, resolution scaling, etc. */
-                r = strv_extend_many(
-                                &cmdline,
-                                "-vga",
-                                "virtio",
-                                "-device", "virtio-serial",
-                                "-chardev", "spicevmc,id=vdagent,debug=0,name=vdagent",
-                                "-device", "virtserialport,chardev=vdagent,name=org.qemu.guest_agent.0");
+                /* -vga is a convenience option, keep on cmdline */
+                r = strv_extend_many(&cmdline, "-vga", "virtio");
+                if (r < 0)
+                        return log_oom();
+
+                r = qemu_config_section(config_file, "device", "virtio-serial0",
+                                        "driver", "virtio-serial");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "chardev", "vdagent",
+                                        "backend", "spicevmc",
+                                        "debug", "0",
+                                        "name", "vdagent");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "device", "vdagent-port0",
+                                        "driver", "virtserialport",
+                                        "chardev", "vdagent",
+                                        "name", "org.qemu.guest_agent.0");
+                if (r < 0)
+                        return r;
+
                 break;
 
         case CONSOLE_NATIVE:
-                r = strv_extend_many(
-                                &cmdline,
-                                "-nographic",
-                                "-nodefaults",
-                                "-chardev", "stdio,mux=on,id=console,signal=off",
-                                "-device", "virtio-serial-pci,id=vmspawn-virtio-serial-pci",
-                                "-device", "virtconsole,chardev=console",
-                                "-mon", "console");
+                r = strv_extend_many(&cmdline, "-nographic", "-nodefaults");
+                if (r < 0)
+                        return log_oom();
+
+                r = qemu_config_section(config_file, "chardev", "console",
+                                        "backend", "stdio",
+                                        "mux", "on",
+                                        "signal", "off");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci",
+                                        "driver", "virtio-serial-pci");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "device", "virtconsole0",
+                                        "driver", "virtconsole",
+                                        "chardev", "console");
+                if (r < 0)
+                        return r;
+
+                r = qemu_config_section(config_file, "mon", "mon0",
+                                        "chardev", "console");
+                if (r < 0)
+                        return r;
+
                 break;
 
         case CONSOLE_HEADLESS:
-                r = strv_extend_many(
-                                &cmdline,
-                                "-nographic",
-                                "-nodefaults");
+                r = strv_extend_many(&cmdline, "-nographic", "-nodefaults");
+                if (r < 0)
+                        return log_oom();
+
                 break;
 
         default:
                 assert_not_reached();
         }
-        if (r < 0)
-                return log_oom();
-
-        r = strv_extend(&cmdline, "-drive");
-        if (r < 0)
-                return log_oom();
-
-        _cleanup_free_ char *escaped_ovmf_config_path = escape_qemu_value(ovmf_config->path);
-        if (!escaped_ovmf_config_path)
-                return log_oom();
 
-        r = strv_extendf(&cmdline, "if=pflash,format=%s,readonly=on,file=%s", ovmf_config_format(ovmf_config), escaped_ovmf_config_path);
+        r = qemu_config_section(config_file, "drive", "ovmf-code",
+                                "if", "pflash",
+                                "format", ovmf_config_format(ovmf_config),
+                                "readonly", "on",
+                                "file", ovmf_config->path);
         if (r < 0)
-                return log_oom();
+                return r;
 
         if (arg_efi_nvram_state_mode == STATE_AUTO && !arg_ephemeral) {
                 assert(!arg_efi_nvram_state_path);
@@ -2516,21 +2616,26 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
 
                 destroy_path = mfree(destroy_path); /* disarm auto-destroy */
 
-                r = strv_extend_many(
-                                &cmdline,
-                                "-global", "ICH9-LPC.disable_s3=1",
-                                "-global", "driver=cfi.pflash01,property=secure,value=on",
-                                "-drive");
+                r = qemu_config_section(config_file, "global", /* id= */ NULL,
+                                        "driver", "ICH9-LPC",
+                                        "property", "disable_s3",
+                                        "value", "1");
                 if (r < 0)
-                        return log_oom();
+                        return r;
 
-                _cleanup_free_ char *escaped_state = escape_qemu_value(state);
-                if (!escaped_state)
-                        return log_oom();
+                r = qemu_config_section(config_file, "global", /* id= */ NULL,
+                                        "driver", "cfi.pflash01",
+                                        "property", "secure",
+                                        "value", "on");
+                if (r < 0)
+                        return r;
 
-                r = strv_extendf(&cmdline, "file=%s,if=pflash,format=%s", escaped_state, ovmf_config_format(ovmf_config));
+                r = qemu_config_section(config_file, "drive", "ovmf-vars",
+                                        "file", state,
+                                        "if", "pflash",
+                                        "format", ovmf_config_format(ovmf_config));
                 if (r < 0)
-                        return log_oom();
+                        return r;
         }
 
         if (kernel) {
@@ -2559,8 +2664,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 }
 
         if (need_scsi_controller) {
-                if (strv_extend_many(&cmdline, "-device", "virtio-scsi-pci,id=vmspawn_scsi") < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "device", "vmspawn_scsi",
+                                        "driver", "virtio-scsi-pci");
+                if (r < 0)
+                        return r;
         }
 
         if (arg_image) {
@@ -2574,54 +2681,60 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                                                        arg_image);
                 }
 
-                if (strv_extend(&cmdline, "-drive") < 0)
-                        return log_oom();
-
-                _cleanup_free_ char *escaped_image = escape_qemu_value(arg_image);
-                if (!escaped_image)
-                        return log_oom();
-
-                if (strv_extendf(&cmdline, "if=none,id=vmspawn,file=%s,format=%s,discard=%s,snapshot=%s",
-                                 escaped_image, image_format_to_string(arg_image_format), on_off(arg_discard_disk), on_off(arg_ephemeral)) < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "drive", "vmspawn",
+                                        "if", "none",
+                                        "file", arg_image,
+                                        "format", image_format_to_string(arg_image_format),
+                                        "discard", on_off(arg_discard_disk),
+                                        "snapshot", on_off(arg_ephemeral));
+                if (r < 0)
+                        return r;
 
                 _cleanup_free_ char *image_fn = NULL;
                 r = path_extract_filename(arg_image, &image_fn);
                 if (r < 0)
                         return log_error_errno(r, "Failed to extract filename from path '%s': %m", image_fn);
 
-                _cleanup_free_ char *escaped_image_fn = escape_qemu_value(image_fn);
-                if (!escaped_image_fn)
-                        return log_oom();
-
-                if (strv_extend(&cmdline, "-device") < 0)
-                        return log_oom();
+                const char *disk_driver;
+                _cleanup_free_ char *serial = NULL;
 
                 switch (arg_image_disk_type) {
                 case DISK_TYPE_VIRTIO_BLK:
-                        if (strv_extend_joined(&cmdline, "virtio-blk-pci,drive=vmspawn,bootindex=1,serial=", escaped_image_fn) < 0)
+                        disk_driver = "virtio-blk-pci";
+                        serial = strdup(image_fn);
+                        if (!serial)
                                 return log_oom();
                         break;
-                case DISK_TYPE_VIRTIO_SCSI: {
-                        _cleanup_free_ char *serial = NULL;
-                        if (disk_serial(escaped_image_fn, 30, &serial) < 0)
-                                return log_oom();
-                        if (strv_extend_joined(&cmdline, "scsi-hd,bus=vmspawn_scsi.0,drive=vmspawn,bootindex=1,serial=", serial) < 0)
+                case DISK_TYPE_VIRTIO_SCSI:
+                        disk_driver = "scsi-hd";
+                        r = disk_serial(image_fn, 30, &serial);
+                        if (r < 0)
                                 return log_oom();
                         break;
-                }
-                case DISK_TYPE_NVME: {
-                        _cleanup_free_ char *serial = NULL;
-                        if (disk_serial(escaped_image_fn, 20, &serial) < 0)
-                                return log_oom();
-                        if (strv_extend_joined(&cmdline, "nvme,drive=vmspawn,bootindex=1,serial=", serial) < 0)
+                case DISK_TYPE_NVME:
+                        disk_driver = "nvme";
+                        r = disk_serial(image_fn, 20, &serial);
+                        if (r < 0)
                                 return log_oom();
                         break;
-                }
                 default:
                         assert_not_reached();
                 }
 
+                r = qemu_config_section(config_file, "device", "vmspawn-disk",
+                                        "driver", disk_driver,
+                                        "drive", "vmspawn",
+                                        "bootindex", "1",
+                                        "serial", serial);
+                if (r < 0)
+                        return r;
+
+                if (arg_image_disk_type == DISK_TYPE_VIRTIO_SCSI) {
+                        r = qemu_config_key(config_file, "bus", "vmspawn_scsi.0");
+                        if (r < 0)
+                                return r;
+                }
+
                 r = grow_image(arg_image, arg_grow_image);
                 if (r < 0)
                         return r;
@@ -2686,21 +2799,19 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 pidref_done(&child);
                 children[n_children++] = TAKE_PTR(source);
 
-                _cleanup_free_ char *escaped_listen_address = escape_qemu_value(listen_address);
-                if (!escaped_listen_address)
-                        return log_oom();
-
-                if (strv_extend(&cmdline, "-chardev") < 0)
-                        return log_oom();
-
-                if (strv_extendf(&cmdline, "socket,id=rootdir,path=%s", escaped_listen_address) < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "chardev", "rootdir",
+                                        "backend", "socket",
+                                        "path", listen_address);
+                if (r < 0)
+                        return r;
 
-                if (strv_extend_many(
-                                    &cmdline,
-                                    "-device",
-                                    "vhost-user-fs-pci,queue-size=1024,chardev=rootdir,tag=root") < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "device", "rootdir",
+                                        "driver", "vhost-user-fs-pci",
+                                        "queue-size", "1024",
+                                        "chardev", "rootdir",
+                                        "tag", "root");
+                if (r < 0)
+                        return r;
 
                 if (strv_extend(&arg_kernel_cmdline_extra, "root=root rootfstype=virtiofs rw") < 0)
                         return log_oom();
@@ -2810,25 +2921,23 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 pidref_done(&child);
                 children[n_children++] = TAKE_PTR(source);
 
-                _cleanup_free_ char *escaped_listen_address = escape_qemu_value(listen_address);
-                if (!escaped_listen_address)
-                        return log_oom();
-
-                if (strv_extend(&cmdline, "-chardev") < 0)
-                        return log_oom();
-
                 _cleanup_free_ char *id = NULL;
                 if (asprintf(&id, "mnt%zu", j) < 0)
                         return log_oom();
 
-                if (strv_extendf(&cmdline, "socket,id=%s,path=%s", id, escaped_listen_address) < 0)
-                        return log_oom();
-
-                if (strv_extend(&cmdline, "-device") < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "chardev", id,
+                                        "backend", "socket",
+                                        "path", listen_address);
+                if (r < 0)
+                        return r;
 
-                if (strv_extendf(&cmdline, "vhost-user-fs-pci,queue-size=1024,chardev=%1$s,tag=%1$s", id) < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "device", id,
+                                        "driver", "vhost-user-fs-pci",
+                                        "queue-size", "1024",
+                                        "chardev", id,
+                                        "tag", id);
+                if (r < 0)
+                        return r;
 
                 _cleanup_free_ char *clean_target = xescape(m->target, "\":");
                 if (!clean_target)
@@ -2911,25 +3020,33 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         }
 
         if (tpm_socket_address) {
-                _cleanup_free_ char *escaped_tpm_socket_address = escape_qemu_value(tpm_socket_address);
-                if (!escaped_tpm_socket_address)
-                        return log_oom();
-
-                if (strv_extend(&cmdline, "-chardev") < 0)
-                        return log_oom();
-
-                if (strv_extend_joined(&cmdline, "socket,id=chrtpm,path=", tpm_socket_address) < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "chardev", "chrtpm",
+                                        "backend", "socket",
+                                        "path", tpm_socket_address);
+                if (r < 0)
+                        return r;
 
-                if (strv_extend_many(&cmdline, "-tpmdev", "emulator,id=tpm0,chardev=chrtpm") < 0)
-                        return log_oom();
+                r = qemu_config_section(config_file, "tpmdev", "tpm0",
+                                        "type", "emulator",
+                                        "chardev", "chrtpm");
+                if (r < 0)
+                        return r;
 
+                const char *tpm_driver;
                 if (native_architecture() == ARCHITECTURE_X86_64)
-                        r = strv_extend_many(&cmdline, "-device", "tpm-tis,tpmdev=tpm0");
+                        tpm_driver = "tpm-tis";
                 else if (IN_SET(native_architecture(), ARCHITECTURE_ARM64, ARCHITECTURE_ARM64_BE))
-                        r = strv_extend_many(&cmdline, "-device", "tpm-tis-device,tpmdev=tpm0");
-                if (r < 0)
-                        return log_oom();
+                        tpm_driver = "tpm-tis-device";
+                else
+                        tpm_driver = NULL;
+
+                if (tpm_driver) {
+                        r = qemu_config_section(config_file, "device", "tpmdev0",
+                                                "driver", tpm_driver,
+                                                "tpmdev", "tpm0");
+                        if (r < 0)
+                                return r;
+                }
         }
 
         char *initrd = NULL;
@@ -3083,6 +3200,16 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         return log_error_errno(r, "Failed to call getsockname on VSOCK: %m");
         }
 
+        /* Finalize the config file and add -readconfig to the cmdline */
+        r = fflush_and_check(config_file);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write QEMU config file: %m");
+        config_file = safe_fclose(config_file);
+
+        r = strv_extend_many(&cmdline, "-readconfig", config_path);
+        if (r < 0)
+                return log_oom();
+
         const char *e = secure_getenv("SYSTEMD_VMSPAWN_QEMU_EXTRA");
         if (e) {
                 r = strv_split_and_extend_full(&cmdline, e,
@@ -3093,6 +3220,14 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         }
 
         if (DEBUG_LOGGING) {
+                _cleanup_free_ char *config_contents = NULL;
+
+                r = read_full_file(config_path, &config_contents, /* ret_size= */ NULL);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to read back QEMU config file, ignoring: %m");
+                else
+                        log_debug("QEMU config file %s:\n%s", config_path, config_contents);
+
                 _cleanup_free_ char *joined = quote_command_line(cmdline, SHELL_ESCAPE_EMPTY);
                 if (!joined)
                         return log_oom();