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"