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.
This commit is contained in:
Daan De Meyer
2026-08-04 23:21:55 +09:00
committed by Yu Watanabe
parent 0b7c68eb3a
commit 2dd07068a3
5 changed files with 88 additions and 13 deletions
+4
View File
@@ -169,6 +169,10 @@
already as large or larger than requested. The specified size accepts the usual K, M, G suffixes 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.</para> (to the base of 1024). Specified values are rounded up to multiples of 4096.</para>
<para>If <option>--ephemeral</option> 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.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem> <xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry> </varlistentry>
</variablelist> </variablelist>
+8
View File
@@ -13,6 +13,7 @@
#include "errno-util.h" #include "errno-util.h"
#include "ether-addr-util.h" #include "ether-addr-util.h"
#include "fd-util.h" #include "fd-util.h"
#include "format-util.h"
#include "hashmap.h" #include "hashmap.h"
#include "json-util.h" #include "json-util.h"
#include "log.h" #include "log.h"
@@ -493,6 +494,13 @@ static int qmp_setup_ephemeral_drive(VmspawnQmpBridge *bridge, QmpClient *qmp, D
if (r < 0) if (r < 0)
return r; 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 */ /* Step 1-2: Pass both fds to QEMU */
_cleanup_free_ char *base_path = NULL; _cleanup_free_ char *base_path = NULL;
uint64_t base_fdset_id; uint64_t base_fdset_id;
+1
View File
@@ -98,6 +98,7 @@ typedef struct DriveInfo {
char *pcie_port; /* pcie-root-port id for device_add bus (NULL on non-PCIe) */ 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 fd; /* pre-opened image fd (-EBADF if unused) */
int overlay_fd; /* pre-opened anonymous overlay fd for ephemeral (-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; QmpDriveFlags flags;
/* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */ /* Per-add-op state (populated by the add flow; zeroed at CLI-parse time) */
+18 -12
View File
@@ -423,6 +423,11 @@ static int parse_argv(int argc, char *argv[]) {
r = parse_size(opts.arg, 1024, &arg_grow_image); r = parse_size(opts.arg, 1024, &arg_grow_image);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse --grow-image= parameter: %s", opts.arg); 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; break;
OPTION_GROUP("Host Configuration"): {} OPTION_GROUP("Host Configuration"): {}
@@ -2088,12 +2093,6 @@ static int grow_image(const char *path, uint64_t size) {
if (size == 0) if (size == 0)
return 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); _cleanup_close_ int fd = xopenat_full(AT_FDCWD, path, O_RDWR|O_CLOEXEC, XO_REGULAR, /* mode= */ 0);
if (fd < 0) if (fd < 0)
return log_error_errno(fd, "Failed to open image file '%s': %m", path); 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->overlay_fd = TAKE_FD(overlay_fd);
d->flags |= QMP_DRIVE_NO_FLUSH; d->flags |= QMP_DRIVE_NO_FLUSH;
d->grow_to = arg_grow_image;
} }
drives->drives[drives->n_drives++] = TAKE_PTR(d); 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); 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); r = grow_image(arg_image, arg_grow_image);
if (r < 0) if (r < 0)
return r; 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; _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)."); 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), return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'."); "--grow-image is not supported for qcow2 images, use 'qemu-img resize FILE SIZE'.");
@@ -53,7 +53,7 @@ WORKDIR="$(mktemp -d)"
at_exit() { at_exit() {
set +e set +e
for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}"; do for m in "${MACHINE_MULTI:-}" "${MACHINE_EPHEMERAL:-}" "${MACHINE_GROW:-}"; do
[[ -n "$m" ]] || continue [[ -n "$m" ]] || continue
if machinectl status "$m" &>/dev/null; then if machinectl status "$m" &>/dev/null; then
machinectl terminate "$m" 2>/dev/null machinectl terminate "$m" 2>/dev/null
@@ -62,6 +62,7 @@ at_exit() {
done done
[[ -n "${VMSPAWN_MULTI_PID:-}" ]] && kill "$VMSPAWN_MULTI_PID" 2>/dev/null && wait "$VMSPAWN_MULTI_PID" 2>/dev/null [[ -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_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" rm -rf "$WORKDIR"
} }
trap at_exit EXIT 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" timeout 10 bash -c "while kill -0 '$VMSPAWN_EPHEMERAL_PID' 2>/dev/null; do sleep .5; done"
echo "Ephemeral VM terminated cleanly" 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" echo "All vmspawn drive setup tests passed"