From 2dd07068a388c31610fca8c8e181dc6b501be9e4 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 31 Jul 2026 21:13:12 +0200 Subject: [PATCH] vmspawn: apply --grow-image= to the ephemeral copy Combining --ephemeral with --grow-image= truncated the file passed to --image= to the requested size before the VM was started, so a mode whose entire purpose is to leave the original image untouched modified it on disk. Skip growing the original image when running ephemeral, and instead create the qcow2 overlay with the requested virtual size, so the guest sees the larger disk while the base image stays read-only. As only the overlay is sized in that case, the base image format no longer matters, so allow --grow-image= together with qcow2 images when running ephemeral. The round up to a multiple of 4096 moves to option parsing so both paths apply the same value. The vmspawn drive test grows an ephemeral VM and checks that the image passed to --image= keeps its size, and that the overlay QEMU runs off is created at the requested one. --- man/systemd-vmspawn.xml | 4 ++ src/vmspawn/vmspawn-qmp.c | 8 +++ src/vmspawn/vmspawn-qmp.h | 1 + src/vmspawn/vmspawn.c | 30 ++++++---- .../TEST-87-AUX-UTILS-VM.vmspawn-drives.sh | 58 ++++++++++++++++++- 5 files changed, 88 insertions(+), 13 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 2475a0464f..3c5519fd88 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -169,6 +169,10 @@ already as large or larger than requested. The specified size accepts the usual K, M, G suffixes (to the base of 1024). Specified values are rounded up to multiples of 4096. + If is specified, the original image file is left untouched and + the requested size is applied to the ephemeral copy instead. In that case qcow2 images are + supported too. + diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c index 7dc58a8ccb..092f8dae44 100644 --- a/src/vmspawn/vmspawn-qmp.c +++ b/src/vmspawn/vmspawn-qmp.c @@ -13,6 +13,7 @@ #include "errno-util.h" #include "ether-addr-util.h" #include "fd-util.h" +#include "format-util.h" #include "hashmap.h" #include "json-util.h" #include "log.h" @@ -493,6 +494,13 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D if (r < 0) return r; + /* Growing the overlay leaves the base image untouched, which is the whole point of ephemeral mode. */ + if (drive->grow_to > virtual_size) { + log_debug("Growing ephemeral overlay of '%s' from %s to %s.", + drive->path, FORMAT_BYTES(virtual_size), FORMAT_BYTES(drive->grow_to)); + virtual_size = drive->grow_to; + } + /* Step 1-2: Pass both fds to QEMU */ _cleanup_free_ char *base_path = NULL; uint64_t base_fdset_id; diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h index bf9919f93a..df0242bbe0 100644 --- a/src/vmspawn/vmspawn-qmp.h +++ b/src/vmspawn/vmspawn-qmp.h @@ -98,6 +98,7 @@ typedef struct DriveInfo { 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) */ + uint64_t grow_to; /* ephemeral: minimum virtual size of the overlay (0 if unset) */ QmpDriveFlags flags; /* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */ diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 1e1283c327..9d90cbed92 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -423,6 +423,11 @@ static int parse_argv(int argc, char *argv[]) { r = parse_size(opts.arg, 1024, &arg_grow_image); if (r < 0) return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", opts.arg); + + if (arg_grow_image > UINT64_MAX - 4095) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), + "Specified --grow-image= size too large: %s", opts.arg); + arg_grow_image = ROUND_UP(arg_grow_image, 4096); break; OPTION_GROUP("Host Configuration"): {} @@ -2088,12 +2093,6 @@ static int grow_image(const char *path, uint64_t size) { if (size == 0) return 0; - /* Round up to multiple of 4K */ - size = DIV_ROUND_UP(size, 4096); - if (size > UINT64_MAX / 4096) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified file size too large, refusing."); - size *= 4096; - _cleanup_close_ int fd = xopenat_full(AT_FDCWD, path, O_RDWR|O_CLOEXEC, XO_REGULAR, /* mode= */ 0); if (fd < 0) return log_error_errno(fd, "Failed to open image file '%s': %m", path); @@ -2418,6 +2417,7 @@ static int prepare_primary_drive(const char *runtime_dir, DriveInfos *drives) { } d->overlay_fd = TAKE_FD(overlay_fd); d->flags |= QMP_DRIVE_NO_FLUSH; + d->grow_to = arg_grow_image; } drives->drives[drives->n_drives++] = TAKE_PTR(d); @@ -3249,14 +3249,18 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { arg_image); } - if (arg_image_disk_type != DISK_TYPE_VIRTIO_SCSI_CDROM) { + if (arg_image_disk_type == DISK_TYPE_VIRTIO_SCSI_CDROM) { + /* CD-ROMs are read-only, so override any "rw" on the kernel command line. */ + if (strv_contains(arg_kernel_cmdline_extra, "rw") && + strv_extend(&arg_kernel_cmdline_extra, "ro") < 0) + return log_oom(); + } else if (!arg_ephemeral) { + /* In ephemeral mode the original image is never written to, the requested size is + * applied to the qcow2 overlay instead, see prepare_primary_drive(). */ r = grow_image(arg_image, arg_grow_image); if (r < 0) return r; - /* CD-ROMs are read-only, so override any "rw" on the kernel command line. */ - } else if (strv_contains(arg_kernel_cmdline_extra, "rw") && - strv_extend(&arg_kernel_cmdline_extra, "ro") < 0) - return log_oom(); + } } _cleanup_(sd_event_unrefp) sd_event *event = NULL; @@ -4225,7 +4229,9 @@ static int verify_arguments(void) { log_warning("--grow-image has no effect with --image-disk-type=scsi-cd (CD-ROMs are read-only)."); } - if (arg_grow_image && arg_image_format == IMAGE_FORMAT_QCOW2) + /* In ephemeral mode the size is picked when creating the qcow2 overlay, so the base image format + * doesn't matter. */ + if (arg_grow_image && arg_image_format == IMAGE_FORMAT_QCOW2 && !arg_ephemeral) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'."); diff --git a/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh b/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh index 5ec2fd2f7b..31b3bf8b51 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.vmspawn-drives.sh @@ -53,7 +53,7 @@ WORKDIR="$(mktemp -d)" at_exit() { set +e - for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}"; do + for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}" "${MACHINE_GROW:-}"; do [[ -n "$m" ]] || continue if machinectl status "$m" &>/dev/null; then machinectl terminate "$m" 2>/dev/null @@ -62,6 +62,7 @@ at_exit() { done [[ -n "${VMSPAWN_MULTI_PID:-}" ]] && kill "$VMSPAWN_MULTI_PID" 2>/dev/null && wait "$VMSPAWN_MULTI_PID" 2>/dev/null [[ -n "${VMSPAWN_EPHEMERAL_PID:-}" ]] && kill "$VMSPAWN_EPHEMERAL_PID" 2>/dev/null && wait "$VMSPAWN_EPHEMERAL_PID" 2>/dev/null + [[ -n "${VMSPAWN_GROW_PID:-}" ]] && kill "$VMSPAWN_GROW_PID" 2>/dev/null && wait "$VMSPAWN_GROW_PID" 2>/dev/null rm -rf "$WORKDIR" } trap at_exit EXIT @@ -185,4 +186,59 @@ timeout 10 bash -c "while machinectl status '$MACHINE_EPHEMERAL' &>/dev/null; do timeout 10 bash -c "while kill -0 '$VMSPAWN_EPHEMERAL_PID' 2>/dev/null; do sleep .5; done" echo "Ephemeral VM terminated cleanly" +# --- Test 3: Ephemeral overlay grown with --grow-image= --- +# In ephemeral mode the requested size has to land on the qcow2 overlay. The +# image passed to --image= is opened read-only and has to come out of the run +# at its original size. + +MACHINE_GROW="test-vmspawn-grow-$$" +GROW_SIZE=$((512 * 1024 * 1024)) +IMAGE_SIZE="$(stat -c %s "$WORKDIR/root.raw")" + +systemd-vmspawn \ + --machine="$MACHINE_GROW" \ + --ram=256M \ + --image="$WORKDIR/root.raw" \ + --ephemeral \ + --grow-image="$GROW_SIZE" \ + --linux="$KERNEL" \ + --tpm=no \ + --console=headless \ + root=/dev/vda rw \ + &>"$WORKDIR/vmspawn-grow.log" & +VMSPAWN_GROW_PID=$! + +wait_for_machine "$MACHINE_GROW" "$VMSPAWN_GROW_PID" "$WORKDIR/vmspawn-grow.log" +echo "Grown ephemeral machine '$MACHINE_GROW' registered with machined" + +assert_eq "$(stat -c %s "$WORKDIR/root.raw")" "$IMAGE_SIZE" +echo "Image passed to --image= was left at its original size" + +# The overlay is unlinked, so QEMU's fd is the only way to reach it. vmspawn +# registers QEMU itself as the machine leader, so machined knows its PID. +QEMU_PID="$(varlinkctl call /run/systemd/machine/io.systemd.Machine \ + io.systemd.Machine.List "{\"name\":\"$MACHINE_GROW\"}" | jq -r '.leader.pid')" + +OVERLAY="" +for fd in /proc/"$QEMU_PID"/fd/*; do + target="$(readlink "$fd" 2>/dev/null)" || continue + # O_TMPFILE in the runtime directory, or the memfd fallback. + [[ "$target" == */vmspawn/"$MACHINE_GROW"/#* || "$target" == /memfd:vmspawn-overlay* ]] || continue + OVERLAY="$fd" + break +done +assert_neq "$OVERLAY" "" +echo "Ephemeral overlay is QEMU fd ${OVERLAY##*/}" + +# qcow2 header: the magic, followed by the virtual size as a big endian u64 at +# offset 24. Read it out of the header directly, qemu-img may not be installed. +assert_eq "$(od -An -tx1 -N4 "$OVERLAY" | tr -d ' ')" "514649fb" +assert_eq "$(od -An -tu8 -j24 -N8 --endian=big "$OVERLAY" | tr -d ' ')" "$GROW_SIZE" +echo "Ephemeral overlay was created at the requested size" + +machinectl terminate "$MACHINE_GROW" +timeout 10 bash -c "while machinectl status '$MACHINE_GROW' &>/dev/null; do sleep .5; done" +timeout 10 bash -c "while kill -0 '$VMSPAWN_GROW_PID' 2>/dev/null; do sleep .5; done" +echo "Grown ephemeral VM terminated cleanly" + echo "All vmspawn drive setup tests passed"