* [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot
@ 2026-09-07 9:47 Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 1/9] d/patches: normalize header of pbs-restore no-cache patch Erik Fastermann
` (9 more replies)
0 siblings, 10 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
Snapshotting a paused VM left it in finish-migrate, where it stayed
until the VM was restarted. Fix this by storing the run state instead
and put it back, similar to migration_iteration_finish().
While working on that, I noticed some memory leaks and other issues,
which I found by using sanitizer tools like ASAN and reading the code
and docs.
Changes since the RFC:
* store the run state via runstate_get() and restore it instead of
skipping the transition into finish-migrate (Thanks @Fiona)
* refuse a snapshot while an incoming migration is pending, which
migrate_prepare() already does, otherwise the restore aborts
* fixed multiple memory leaks
* fixed multiple discarded error messages
* removed raw uses of allocators
* folded the changes into the relevant debian/patches
Future work:
* Once SAVE_STATE_ERROR is set, no new snapshot can be started and
savevm-start fails with "VM snapshot already started" until the VM is
restarted. Resetting that state looks difficult and could introduce
new bugs. A couple of people ran into this before, see [0] and [1].
It would definitely be nicer if a single failed snapshot did not
block further snapshots until a VM restart.
* TSAN flags a data race on snap_state.bs_pos. Probably simply fixed
by using atomics. There might be some other issues in savevm-async,
but I have to check this in more detail.
Note that patch 9 touches the same lines as [2].
[0] https://forum.proxmox.com/threads/snapshot-creation-issue.176090/
[1] https://forum.proxmox.com/threads/snapshot-fails-to-cleanup-and-prevents-future-snapshots.144757/
[2] https://lore.proxmox.com/all/20260714142629.277917-2-f.ebner@proxmox.com/
Erik Fastermann (9):
d/patches: normalize header of pbs-restore no-cache patch
pbs-restore: keep callback data on the stack
migration/qemu-file: allocate buffer with g_malloc
migration/pbs-state: check allocation of incoming state buffer
savevm-async: drop QIOChannel reference
savevm-async: free migration vmdesc
savevm-async: release resources on start failure path
savevm-async: include reason when file open fails
savevm-async: restore run state from before snapshot
...async-for-background-state-snapshots.patch | 111 +++++++++++++-----
...add-optional-buffer-size-to-QEMUFile.patch | 29 ++---
...estore-new-command-to-restore-from-p.patch | 16 +--
...igrate-dirty-bitmap-state-via-savevm.patch | 25 ++--
...se-migration-blocker-check-for-snaps.patch | 4 +-
...no-cache-flag-to-skip-host-page-cach.patch | 7 +-
6 files changed, 129 insertions(+), 63 deletions(-)
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH qemu v2 1/9] d/patches: normalize header of pbs-restore no-cache patch
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 2/9] pbs-restore: keep callback data on the stack Erik Fastermann
` (8 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
Every other patch in the series is generated with --zero-commit and
--no-signature. Bring the outlier in line, so regenerating the series
does not produce unrelated churn.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...bs-restore-add-no-cache-flag-to-skip-host-page-cach.patch | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch b/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
index fff7127..3ef6e69 100644
--- a/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
+++ b/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
@@ -1,4 +1,4 @@
-From d7ca855d826bb20cd896fe95e1cc2154ce32980d Mon Sep 17 00:00:00 2001
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Christian Ebner <c.ebner@proxmox.com>
Date: Mon, 17 Aug 2026 10:21:58 +0200
Subject: [PATCH] pbs-restore: add no-cache flag to skip host page cache on
@@ -67,6 +67,3 @@ index f165f418af..55a3cb235d 100644
BlockBackend *blk = blk_new_open(target, NULL, options, flags, &local_err);
if (!blk) {
fprintf(stderr, "%s\n", error_get_pretty(local_err));
---
-2.47.3
-
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 2/9] pbs-restore: keep callback data on the stack
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 1/9] d/patches: normalize header of pbs-restore no-cache patch Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 3/9] migration/qemu-file: allocate buffer with g_malloc Erik Fastermann
` (7 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
proxmox_restore_image() is synchronous and only hands the pointer back
to the callback, so the lifetime is the enclosing scope. Dropping the
heap allocation also drops its missing free and calloc() call, which is
discouraged in QEMU.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...s-restore-new-command-to-restore-from-p.patch | 16 +++++++++-------
...dd-no-cache-flag-to-skip-host-page-cach.patch | 2 +-
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/debian/patches/pve/0030-PVE-Backup-pbs-restore-new-command-to-restore-from-p.patch b/debian/patches/pve/0030-PVE-Backup-pbs-restore-new-command-to-restore-from-p.patch
index 6d5bb42..bd11662 100644
--- a/debian/patches/pve/0030-PVE-Backup-pbs-restore-new-command-to-restore-from-p.patch
+++ b/debian/patches/pve/0030-PVE-Backup-pbs-restore-new-command-to-restore-from-p.patch
@@ -7,6 +7,8 @@ Subject: [PATCH] PVE-Backup: pbs-restore - new command to restore from proxmox
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
[WB: add namespace support]
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
+[EF: keep callback data on the stack]
+Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
meson.build | 4 +
pbs-restore.c | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -30,7 +32,7 @@ index 1eee80f7d8..b7a8702711 100644
foreach exe: [ 'qemu-img', 'qemu-io', 'qemu-nbd', 'qemu-storage-daemon']
diff --git a/pbs-restore.c b/pbs-restore.c
new file mode 100644
-index 0000000000..f165f418af
+index 0000000000..f5e3552193
--- /dev/null
+++ b/pbs-restore.c
@@ -0,0 +1,236 @@
@@ -240,11 +242,11 @@ index 0000000000..f165f418af
+ return -1;
+ }
+
-+ CallbackData *callback_data = calloc(sizeof(CallbackData), 1);
-+
-+ callback_data->target = blk;
-+ callback_data->skip_zero = skip_zero;
-+ callback_data->last_offset = 0;
++ CallbackData callback_data = {
++ .target = blk,
++ .skip_zero = skip_zero,
++ .last_offset = 0,
++ };
+
+ // blk_set_enable_write_cache(blk, !writethrough);
+
@@ -256,7 +258,7 @@ index 0000000000..f165f418af
+ conn,
+ archive_name,
+ write_callback,
-+ callback_data,
++ &callback_data,
+ &pbs_error,
+ verbose);
+
diff --git a/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch b/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
index 3ef6e69..7b79722 100644
--- a/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
+++ b/debian/patches/pve/0047-pbs-restore-add-no-cache-flag-to-skip-host-page-cach.patch
@@ -25,7 +25,7 @@ Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
1 file changed, 11 insertions(+)
diff --git a/pbs-restore.c b/pbs-restore.c
-index f165f418af..55a3cb235d 100644
+index f5e3552193..592b20f7ad 100644
--- a/pbs-restore.c
+++ b/pbs-restore.c
@@ -81,6 +81,7 @@ int main(int argc, char **argv)
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 3/9] migration/qemu-file: allocate buffer with g_malloc
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 1/9] d/patches: normalize header of pbs-restore no-cache patch Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 2/9] pbs-restore: keep callback data on the stack Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 4/9] migration/pbs-state: check allocation of incoming state buffer Erik Fastermann
` (6 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
Plain malloc() can fail and the result was never checked, while
savevm-async asks for a 4 MiB buffer. g_malloc() exits instead of
leaving a NULL buffer for the first write to dereference.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
.../0018-PVE-add-optional-buffer-size-to-QEMUFile.patch | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index ab8aac1..b56dd34 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -12,6 +12,8 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
[FE: rebase for 10.2.0]
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
+[EF: allocate buffer with g_malloc]
+Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
migration/qemu-file.c | 48 +++++++++++++++++++++++++++-------------
migration/qemu-file.h | 2 ++
@@ -19,7 +21,7 @@ Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
3 files changed, 38 insertions(+), 17 deletions(-)
diff --git a/migration/qemu-file.c b/migration/qemu-file.c
-index 9cf7dc3bd5..4a4de51fd9 100644
+index 9cf7dc3bd5..b1b7aa453e 100644
--- a/migration/qemu-file.c
+++ b/migration/qemu-file.c
@@ -34,8 +34,8 @@
@@ -59,7 +61,7 @@ index 9cf7dc3bd5..4a4de51fd9 100644
f->can_pass_fd = qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_FD_PASS);
QTAILQ_INIT(&f->fds);
+ f->buf_allocated_size = buffer_size;
-+ f->buf = malloc(buffer_size);
++ f->buf = g_malloc(buffer_size);
return f;
}
@@ -108,7 +110,7 @@ index 9cf7dc3bd5..4a4de51fd9 100644
}
g_clear_pointer(&f->ioc, object_unref);
+
-+ free(f->buf);
++ g_free(f->buf);
+
error_free(f->last_error_obj);
g_free(f);
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 4/9] migration/pbs-state: check allocation of incoming state buffer
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (2 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 3/9] migration/qemu-file: allocate buffer with g_malloc Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference Erik Fastermann
` (5 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
The length is read from the migration stream, so it cannot be trusted.
An unchecked malloc() means a corrupt or truncated state file makes
qemu_get_buffer() write through a NULL pointer. Also the short read path
returned without freeing the buffer.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...igrate-dirty-bitmap-state-via-savevm.patch | 25 +++++++++++++------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/debian/patches/pve/0033-PVE-Migrate-dirty-bitmap-state-via-savevm.patch b/debian/patches/pve/0033-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
index adb41f4..4ce1d8c 100644
--- a/debian/patches/pve/0033-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
+++ b/debian/patches/pve/0033-PVE-Migrate-dirty-bitmap-state-via-savevm.patch
@@ -15,14 +15,16 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
[FE: split up state_pending for 8.0]
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
+[EF: check allocation of incoming state buffer]
+Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
include/migration/misc.h | 3 ++
migration/meson.build | 2 +
migration/migration.c | 1 +
- migration/pbs-state.c | 104 +++++++++++++++++++++++++++++++++++++++
+ migration/pbs-state.c | 111 +++++++++++++++++++++++++++++++++++++++
pve-backup.c | 1 +
qapi/block-core.json | 6 +++
- 6 files changed, 117 insertions(+)
+ 6 files changed, 124 insertions(+)
create mode 100644 migration/pbs-state.c
diff --git a/include/migration/misc.h b/include/migration/misc.h
@@ -71,10 +73,10 @@ index dfc60372cf..f415448689 100644
typedef struct {
diff --git a/migration/pbs-state.c b/migration/pbs-state.c
new file mode 100644
-index 0000000000..a97187e4d7
+index 0000000000..c0c7f2ff6f
--- /dev/null
+++ b/migration/pbs-state.c
-@@ -0,0 +1,104 @@
+@@ -0,0 +1,111 @@
+/*
+ * PBS (dirty-bitmap) state migration
+ */
@@ -104,18 +106,25 @@ index 0000000000..a97187e4d7
+{
+ /* safe cast, we cannot migrate to target with less bits than source */
+ size_t buf_size = (size_t)qemu_get_be64(f);
++ if (buf_size == 0) {
++ return 0;
++ }
+
-+ uint8_t *buf = (uint8_t *)malloc(buf_size);
-+ size_t read = qemu_get_buffer(f, buf, buf_size);
++ g_autofree uint8_t *buf = g_try_malloc(buf_size);
++ if (!buf) {
++ fprintf(stderr,
++ "error receiving PBS state: cannot allocate %zu bytes\n",
++ buf_size);
++ return -ENOMEM;
++ }
+
++ size_t read = qemu_get_buffer(f, buf, buf_size);
+ if (read < buf_size) {
+ fprintf(stderr, "error receiving PBS state: not enough data\n");
+ return -EIO;
+ }
+
+ proxmox_import_state(buf, buf_size);
-+
-+ free(buf);
+ return 0;
+}
+
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (3 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 4/9] migration/pbs-state: check allocation of incoming state buffer Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-07 9:47 ` [PATCH qemu v2 6/9] savevm-async: free migration vmdesc Erik Fastermann
` (4 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
qemu_file_new_impl() takes its own reference and qemu_fclose() drops
exactly one, so the reference from qio_channel_savevm_async_new() was
never released. The channel also holds a BlockBackend reference that
only its finalizer drops, so both leaked on every snapshot.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...async-for-background-state-snapshots.patch | 15 ++++++++++-----
...add-optional-buffer-size-to-QEMUFile.patch | 19 +++++++++----------
...se-migration-blocker-check-for-snaps.patch | 2 +-
3 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
index ce9df5c..f252236 100644
--- a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
+++ b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
@@ -37,19 +37,21 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
add parameter to skip vm start to be used for hibernation
rebase for 11.0.0]
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
+[EF: drop QIOChannel reference]
+Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
hmp-commands-info.hx | 13 +
hmp-commands.hx | 17 +
include/migration/snapshot.h | 2 +
include/monitor/hmp.h | 3 +
migration/meson.build | 1 +
- migration/savevm-async.c | 597 +++++++++++++++++++++++++++++++++++
+ migration/savevm-async.c | 600 +++++++++++++++++++++++++++++++++++
monitor/hmp-cmds.c | 38 +++
qapi/migration.json | 34 ++
qapi/misc.json | 25 ++
qemu-options.hx | 12 +
system/vl.c | 10 +
- 11 files changed, 752 insertions(+)
+ 11 files changed, 755 insertions(+)
create mode 100644 migration/savevm-async.c
diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
@@ -147,10 +149,10 @@ index 0222d5ea6e..90d62d5723 100644
), gnutls, zlib)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
new file mode 100644
-index 0000000000..2a860f11a1
+index 0000000000..cab08643ad
--- /dev/null
+++ b/migration/savevm-async.c
-@@ -0,0 +1,597 @@
+@@ -0,0 +1,600 @@
+#include "qemu/osdep.h"
+#include "migration/channel-savevm-async.h"
+#include "migration/migration.h"
@@ -572,6 +574,7 @@ index 0000000000..2a860f11a1
+ QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(snap_state.target,
+ &snap_state.bs_pos));
+ snap_state.file = qemu_file_new_output(ioc);
++ object_unref(ioc);
+
+ if (!snap_state.file) {
+ error_setg(errp, "failed to open '%s'", statefile);
@@ -708,7 +711,9 @@ index 0000000000..2a860f11a1
+ bdrv_op_block_all(bs, blocker);
+
+ /* restore the VM state */
-+ f = qemu_file_new_input(QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos)));
++ QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
++ f = qemu_file_new_input(ioc);
++ object_unref(ioc);
+ if (!f) {
+ error_setg(errp, "Could not open VM state file");
+ goto the_end;
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index b56dd34..39cacf7 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -17,8 +17,8 @@ Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
migration/qemu-file.c | 48 +++++++++++++++++++++++++++-------------
migration/qemu-file.h | 2 ++
- migration/savevm-async.c | 5 +++--
- 3 files changed, 38 insertions(+), 17 deletions(-)
+ migration/savevm-async.c | 4 ++--
+ 3 files changed, 37 insertions(+), 17 deletions(-)
diff --git a/migration/qemu-file.c b/migration/qemu-file.c
index 9cf7dc3bd5..b1b7aa453e 100644
@@ -186,7 +186,7 @@ index a390554208..eda093b16a 100644
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QEMUFile, qemu_fclose)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index 2a860f11a1..f5a4819e27 100644
+index cab08643ad..f9eaf3b8e6 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
@@ -418,7 +418,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
@@ -195,16 +195,15 @@ index 2a860f11a1..f5a4819e27 100644
&snap_state.bs_pos));
- snap_state.file = qemu_file_new_output(ioc);
+ snap_state.file = qemu_file_new_output_sized(ioc, 4 * 1024 * 1024);
+ object_unref(ioc);
if (!snap_state.file) {
- error_setg(errp, "failed to open '%s'", statefile);
-@@ -555,7 +555,8 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
- bdrv_op_block_all(bs, blocker);
+@@ -557,7 +557,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
/* restore the VM state */
-- f = qemu_file_new_input(QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos)));
-+ f = qemu_file_new_input_sized(QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos)),
-+ 4 * 1024 * 1024);
+ QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
+- f = qemu_file_new_input(ioc);
++ f = qemu_file_new_input_sized(ioc, 4 * 1024 * 1024);
+ object_unref(ioc);
if (!f) {
error_setg(errp, "Could not open VM state file");
- goto the_end;
diff --git a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
index 25a06a6..5cc3893 100644
--- a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
+++ b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
@@ -136,7 +136,7 @@ index b6888daced..80eb0dcd1f 100644
bool migration_in_postcopy(void);
bool migration_postcopy_is_alive(MigrationStatus state);
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index f5a4819e27..41376406eb 100644
+index f9eaf3b8e6..51340c5dcd 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
@@ -384,7 +384,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 6/9] savevm-async: free migration vmdesc
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (4 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-07 9:47 ` [PATCH qemu v2 7/9] savevm-async: release resources on start failure path Erik Fastermann
` (3 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
migrate_init() allocates it and only migration_cleanup() frees it, which
savevm-async does not call. Free it with the rest of the migration
state, tens of KiB per snapshot even for a minimal guest.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...async-for-background-state-snapshots.patch | 27 ++++++++++++-------
...add-optional-buffer-size-to-QEMUFile.patch | 6 ++---
...se-migration-blocker-check-for-snaps.patch | 4 +--
3 files changed, 23 insertions(+), 14 deletions(-)
diff --git a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
index f252236..8750897 100644
--- a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
+++ b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
@@ -37,7 +37,8 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
add parameter to skip vm start to be used for hibernation
rebase for 11.0.0]
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
-[EF: drop QIOChannel reference]
+[EF: drop QIOChannel reference
+ free migration vmdesc]
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
hmp-commands-info.hx | 13 +
@@ -45,13 +46,13 @@ Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
include/migration/snapshot.h | 2 +
include/monitor/hmp.h | 3 +
migration/meson.build | 1 +
- migration/savevm-async.c | 600 +++++++++++++++++++++++++++++++++++
+ migration/savevm-async.c | 608 +++++++++++++++++++++++++++++++++++
monitor/hmp-cmds.c | 38 +++
qapi/migration.json | 34 ++
qapi/misc.json | 25 ++
qemu-options.hx | 12 +
system/vl.c | 10 +
- 11 files changed, 755 insertions(+)
+ 11 files changed, 763 insertions(+)
create mode 100644 migration/savevm-async.c
diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
@@ -149,10 +150,10 @@ index 0222d5ea6e..90d62d5723 100644
), gnutls, zlib)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
new file mode 100644
-index 0000000000..cab08643ad
+index 0000000000..4f44c2f12d
--- /dev/null
+++ b/migration/savevm-async.c
-@@ -0,0 +1,600 @@
+@@ -0,0 +1,608 @@
+#include "qemu/osdep.h"
+#include "migration/channel-savevm-async.h"
+#include "migration/migration.h"
@@ -261,6 +262,15 @@ index 0000000000..cab08643ad
+ return info;
+}
+
++static void migration_state_cleanup(MigrationState *ms, bool failed)
++{
++ migrate_set_state(&ms->state, MIGRATION_STATUS_SETUP,
++ failed ? MIGRATION_STATUS_FAILED : MIGRATION_STATUS_COMPLETED);
++ ms->to_dst_file = NULL;
++ /* see migration_cleanup_json_writer */
++ g_clear_pointer(&ms->vmdesc, json_writer_free);
++}
++
+static int save_snapshot_cleanup(void)
+{
+ int ret = 0;
@@ -359,10 +369,7 @@ index 0000000000..cab08643ad
+ DPRINTF("timing: process_savevm_finalize (state saving) took %ld ms\n",
+ qemu_clock_get_ms(QEMU_CLOCK_REALTIME) - start_time);
+
-+ /* clear migration state */
-+ migrate_set_state(&ms->state, MIGRATION_STATUS_SETUP,
-+ ret || aborted ? MIGRATION_STATUS_FAILED : MIGRATION_STATUS_COMPLETED);
-+ ms->to_dst_file = NULL;
++ migration_state_cleanup(ms, ret || aborted);
+
+ /*
+ * Same as in migration_iteration_finish(): saving RAM might've turned on CPU throttling for
@@ -618,6 +625,8 @@ index 0000000000..cab08643ad
+ return;
+
+fail:
++ migration_state_cleanup(ms, true);
++
+ savevm_cleanup_iothread();
+ save_snapshot_error("setup failed");
+}
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index 39cacf7..d73d432 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -186,10 +186,10 @@ index a390554208..eda093b16a 100644
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QEMUFile, qemu_fclose)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index cab08643ad..f9eaf3b8e6 100644
+index 4f44c2f12d..0025d69284 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
-@@ -418,7 +418,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
+@@ -424,7 +424,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(snap_state.target,
&snap_state.bs_pos));
@@ -198,7 +198,7 @@ index cab08643ad..f9eaf3b8e6 100644
object_unref(ioc);
if (!snap_state.file) {
-@@ -557,7 +557,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
+@@ -565,7 +565,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
/* restore the VM state */
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
diff --git a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
index 5cc3893..66b6b7b 100644
--- a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
+++ b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
@@ -136,10 +136,10 @@ index b6888daced..80eb0dcd1f 100644
bool migration_in_postcopy(void);
bool migration_postcopy_is_alive(MigrationStatus state);
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index f9eaf3b8e6..51340c5dcd 100644
+index 0025d69284..73ba96ba25 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
-@@ -384,7 +384,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
+@@ -390,7 +390,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
return;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 7/9] savevm-async: release resources on start failure path
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (5 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 6/9] savevm-async: free migration vmdesc Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 8/9] savevm-async: include reason when file open fails Erik Fastermann
` (2 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
The fail label only set the save state and destroyed the iothread,
leaving the finalize bottom half, the blocker, the file, and the target
behind.
Also free the local error of the failing call, of which only the
message is copied into errp, here and in load_snapshot_from_blockdev().
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...async-for-background-state-snapshots.patch | 35 ++++++++++++++++---
...add-optional-buffer-size-to-QEMUFile.patch | 4 +--
...se-migration-blocker-check-for-snaps.patch | 2 +-
3 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
index 8750897..d21fb1f 100644
--- a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
+++ b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
@@ -38,7 +38,8 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
rebase for 11.0.0]
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
[EF: drop QIOChannel reference
- free migration vmdesc]
+ free migration vmdesc
+ release resources on start failure path]
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
hmp-commands-info.hx | 13 +
@@ -46,13 +47,13 @@ Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
include/migration/snapshot.h | 2 +
include/monitor/hmp.h | 3 +
migration/meson.build | 1 +
- migration/savevm-async.c | 608 +++++++++++++++++++++++++++++++++++
+ migration/savevm-async.c | 632 +++++++++++++++++++++++++++++++++++
monitor/hmp-cmds.c | 38 +++
qapi/migration.json | 34 ++
qapi/misc.json | 25 ++
qemu-options.hx | 12 +
system/vl.c | 10 +
- 11 files changed, 763 insertions(+)
+ 11 files changed, 787 insertions(+)
create mode 100644 migration/savevm-async.c
diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
@@ -150,10 +151,10 @@ index 0222d5ea6e..90d62d5723 100644
), gnutls, zlib)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
new file mode 100644
-index 0000000000..4f44c2f12d
+index 0000000000..d688fc7112
--- /dev/null
+++ b/migration/savevm-async.c
-@@ -0,0 +1,608 @@
+@@ -0,0 +1,632 @@
+#include "qemu/osdep.h"
+#include "migration/channel-savevm-async.h"
+#include "migration/migration.h"
@@ -625,9 +626,31 @@ index 0000000000..4f44c2f12d
+ return;
+
+fail:
++ if (snap_state.finalize_bh) {
++ qemu_bh_delete(snap_state.finalize_bh);
++ snap_state.finalize_bh = NULL;
++ }
++
++ if (snap_state.blocker) {
++ bdrv_op_unblock_all(target_bs, snap_state.blocker);
++ error_free(snap_state.blocker);
++ snap_state.blocker = NULL;
++ }
++
+ migration_state_cleanup(ms, true);
+
++ if (snap_state.file) {
++ qemu_fclose(snap_state.file);
++ snap_state.file = NULL;
++ }
++
++ if (snap_state.target) {
++ blk_unref(snap_state.target);
++ snap_state.target = NULL;
++ }
++
+ savevm_cleanup_iothread();
++ error_free(local_err);
+ save_snapshot_error("setup failed");
+}
+
@@ -760,6 +783,8 @@ index 0000000000..4f44c2f12d
+ error_free(blocker);
+ blk_unref(be);
+ }
++
++ error_free(local_err);
+ return ret;
+}
diff --git a/monitor/hmp-cmds.c b/monitor/hmp-cmds.c
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index d73d432..cac1842 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -186,7 +186,7 @@ index a390554208..eda093b16a 100644
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QEMUFile, qemu_fclose)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index 4f44c2f12d..0025d69284 100644
+index d688fc7112..24f7ba60c9 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
@@ -424,7 +424,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
@@ -198,7 +198,7 @@ index 4f44c2f12d..0025d69284 100644
object_unref(ioc);
if (!snap_state.file) {
-@@ -565,7 +565,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
+@@ -587,7 +587,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
/* restore the VM state */
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
diff --git a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
index 66b6b7b..98a0ca4 100644
--- a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
+++ b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
@@ -136,7 +136,7 @@ index b6888daced..80eb0dcd1f 100644
bool migration_in_postcopy(void);
bool migration_postcopy_is_alive(MigrationStatus state);
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index 0025d69284..73ba96ba25 100644
+index 24f7ba60c9..78fc27b320 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
@@ -390,7 +390,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 8/9] savevm-async: include reason when file open fails
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (6 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 7/9] savevm-async: release resources on start failure path Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot Erik Fastermann
2026-09-08 14:21 ` partially-applied: [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Fiona Ebner
9 siblings, 0 replies; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
blk_new_open() reports why it could not open the file, but the error was
discarded.
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...m-async-for-background-state-snapshots.patch | 17 ++++++++++-------
...E-add-optional-buffer-size-to-QEMUFile.patch | 6 +++---
...euse-migration-blocker-check-for-snaps.patch | 2 +-
3 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
index d21fb1f..94908d7 100644
--- a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
+++ b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
@@ -39,7 +39,8 @@ Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
[EF: drop QIOChannel reference
free migration vmdesc
- release resources on start failure path]
+ release resources on start failure path
+ include reason when file open fails]
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
hmp-commands-info.hx | 13 +
@@ -47,13 +48,13 @@ Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
include/migration/snapshot.h | 2 +
include/monitor/hmp.h | 3 +
migration/meson.build | 1 +
- migration/savevm-async.c | 632 +++++++++++++++++++++++++++++++++++
+ migration/savevm-async.c | 634 +++++++++++++++++++++++++++++++++++
monitor/hmp-cmds.c | 38 +++
qapi/migration.json | 34 ++
qapi/misc.json | 25 ++
qemu-options.hx | 12 +
system/vl.c | 10 +
- 11 files changed, 787 insertions(+)
+ 11 files changed, 789 insertions(+)
create mode 100644 migration/savevm-async.c
diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
@@ -151,10 +152,10 @@ index 0222d5ea6e..90d62d5723 100644
), gnutls, zlib)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
new file mode 100644
-index 0000000000..d688fc7112
+index 0000000000..56c60b345f
--- /dev/null
+++ b/migration/savevm-async.c
-@@ -0,0 +1,632 @@
+@@ -0,0 +1,634 @@
+#include "qemu/osdep.h"
+#include "migration/channel-savevm-async.h"
+#include "migration/migration.h"
@@ -570,7 +571,8 @@ index 0000000000..d688fc7112
+ qdict_put_str(options, "driver", "raw");
+ snap_state.target = blk_new_open(statefile, NULL, options, bdrv_oflags, &local_err);
+ if (!snap_state.target) {
-+ error_setg(errp, "failed to open '%s'", statefile);
++ error_setg(errp, "failed to open '%s' - %s", statefile,
++ local_err ? error_get_pretty(local_err) : "unknown error");
+ goto fail;
+ }
+ target_bs = blk_bs(snap_state.target);
@@ -729,7 +731,8 @@ index 0000000000..d688fc7112
+ be = blk_new_open(filename, NULL, options, 0, &local_err);
+
+ if (!be) {
-+ error_setg(errp, "Could not open VM state file");
++ error_setg(errp, "Could not open VM state file - %s",
++ local_err ? error_get_pretty(local_err) : "unknown error");
+ goto the_end;
+ }
+
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index cac1842..4345f42 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -186,10 +186,10 @@ index a390554208..eda093b16a 100644
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QEMUFile, qemu_fclose)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index d688fc7112..24f7ba60c9 100644
+index 56c60b345f..a460970dd5 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
-@@ -424,7 +424,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
+@@ -425,7 +425,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(snap_state.target,
&snap_state.bs_pos));
@@ -198,7 +198,7 @@ index d688fc7112..24f7ba60c9 100644
object_unref(ioc);
if (!snap_state.file) {
-@@ -587,7 +587,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
+@@ -589,7 +589,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
/* restore the VM state */
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
diff --git a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
index 98a0ca4..d87875b 100644
--- a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
+++ b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
@@ -136,7 +136,7 @@ index b6888daced..80eb0dcd1f 100644
bool migration_in_postcopy(void);
bool migration_postcopy_is_alive(MigrationStatus state);
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index 24f7ba60c9..78fc27b320 100644
+index a460970dd5..b703c5d5a3 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
@@ -390,7 +390,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (7 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 8/9] savevm-async: include reason when file open fails Erik Fastermann
@ 2026-09-07 9:47 ` Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-08 14:21 ` partially-applied: [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Fiona Ebner
9 siblings, 1 reply; 14+ messages in thread
From: Erik Fastermann @ 2026-09-07 9:47 UTC (permalink / raw)
To: pve-devel; +Cc: Erik Fastermann
A snapshot of a paused VM left it in finish-migrate, where it stayed
until the VM was restarted. vm_needs_start only recorded whether the VM
had been running, so there was nothing to restore a paused VM to.
Store the run state instead and put it back, similar to
migration_iteration_finish().
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
...async-for-background-state-snapshots.patch | 57 ++++++++++++-------
...add-optional-buffer-size-to-QEMUFile.patch | 6 +-
...se-migration-blocker-check-for-snaps.patch | 4 +-
3 files changed, 41 insertions(+), 26 deletions(-)
diff --git a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
index 94908d7..9a4a049 100644
--- a/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
+++ b/debian/patches/pve/0017-PVE-add-savevm-async-for-background-state-snapshots.patch
@@ -40,7 +40,8 @@ Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
[EF: drop QIOChannel reference
free migration vmdesc
release resources on start failure path
- include reason when file open fails]
+ include reason when file open fails
+ restore run state from before snapshot]
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
hmp-commands-info.hx | 13 +
@@ -48,13 +49,13 @@ Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
include/migration/snapshot.h | 2 +
include/monitor/hmp.h | 3 +
migration/meson.build | 1 +
- migration/savevm-async.c | 634 +++++++++++++++++++++++++++++++++++
- monitor/hmp-cmds.c | 38 +++
+ migration/savevm-async.c | 648 +++++++++++++++++++++++++++++++++++
+ monitor/hmp-cmds.c | 38 ++
qapi/migration.json | 34 ++
qapi/misc.json | 25 ++
qemu-options.hx | 12 +
system/vl.c | 10 +
- 11 files changed, 789 insertions(+)
+ 11 files changed, 803 insertions(+)
create mode 100644 migration/savevm-async.c
diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
@@ -152,10 +153,10 @@ index 0222d5ea6e..90d62d5723 100644
), gnutls, zlib)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
new file mode 100644
-index 0000000000..56c60b345f
+index 0000000000..fe455016ab
--- /dev/null
+++ b/migration/savevm-async.c
-@@ -0,0 +1,634 @@
+@@ -0,0 +1,648 @@
+#include "qemu/osdep.h"
+#include "migration/channel-savevm-async.h"
+#include "migration/migration.h"
@@ -210,7 +211,7 @@ index 0000000000..56c60b345f
+ int state;
+ Error *error;
+ Error *blocker;
-+ bool vm_needs_start;
++ RunState vm_old_state;
+ bool skip_vm_start;
+ QEMUFile *file;
+ int64_t total_time;
@@ -230,6 +231,24 @@ index 0000000000..56c60b345f
+ return snap_state.state == SAVE_STATE_COMPLETED && snap_state.skip_vm_start;
+}
+
++static void restore_vm_old_state(void)
++{
++ RunState old_state = snap_state.vm_old_state;
++
++ if (old_state == RUN_STATE__MAX) {
++ return;
++ }
++ snap_state.vm_old_state = RUN_STATE__MAX;
++
++ if (runstate_is_live(old_state)) {
++ if (!should_skip_vm_start() && !runstate_check(RUN_STATE_SHUTDOWN)) {
++ vm_start();
++ }
++ } else if (runstate_check(RUN_STATE_FINISH_MIGRATE)) {
++ runstate_set(old_state);
++ }
++}
++
+SaveVMInfo *qmp_query_savevm(Error **errp)
+{
+ SaveVMInfo *info = g_malloc0(sizeof(*info));
@@ -352,7 +371,7 @@ index 0000000000..56c60b345f
+ */
+ blk_set_aio_context(snap_state.target, qemu_get_aio_context(), NULL);
+
-+ snap_state.vm_needs_start = runstate_is_running();
++ snap_state.vm_old_state = runstate_get();
+ ret = vm_stop_force_state(RUN_STATE_FINISH_MIGRATE);
+ if (ret < 0) {
+ save_snapshot_error("vm_stop_force_state error %d", ret);
@@ -396,12 +415,7 @@ index 0000000000..56c60b345f
+ save_snapshot_error("process_savevm_cleanup: invalid state: %d",
+ snap_state.state);
+ }
-+ if (snap_state.vm_needs_start) {
-+ if (!should_skip_vm_start()) {
-+ vm_start();
-+ }
-+ snap_state.vm_needs_start = false;
-+ }
++ restore_vm_old_state();
+
+ DPRINTF("timing: process_savevm_finalize (full) took %ld ms\n",
+ qemu_clock_get_ms(QEMU_CLOCK_REALTIME) - start_time);
@@ -529,12 +543,18 @@ index 0000000000..56c60b345f
+ return;
+ }
+
++ if (runstate_check(RUN_STATE_INMIGRATE)) {
++ error_setg(errp, "There's an incoming migration in progress");
++ return;
++ }
++
+ /* initialize snapshot info */
+ snap_state.bs_pos = 0;
+ snap_state.total_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
+ snap_state.blocker = NULL;
+ snap_state.target_close_wait = (QemuCoSleep){ .to_wake = NULL };
+ snap_state.skip_vm_start = has_skip_vm_start && skip_vm_start;
++ snap_state.vm_old_state = RUN_STATE__MAX;
+
+ if (snap_state.error) {
+ error_free(snap_state.error);
@@ -542,7 +562,7 @@ index 0000000000..56c60b345f
+ }
+
+ if (!statefile) {
-+ snap_state.vm_needs_start = runstate_is_running();
++ snap_state.vm_old_state = runstate_get();
+ vm_stop(RUN_STATE_SAVE_VM);
+ snap_state.state = SAVE_STATE_COMPLETED;
+ return;
@@ -703,12 +723,7 @@ index 0000000000..56c60b345f
+ return;
+ }
+
-+ if (snap_state.vm_needs_start) {
-+ if (!should_skip_vm_start()) {
-+ vm_start();
-+ }
-+ snap_state.vm_needs_start = false;
-+ }
++ restore_vm_old_state();
+
+ qemu_coroutine_enter(wait_for_close);
+}
diff --git a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
index 4345f42..e29c721 100644
--- a/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
+++ b/debian/patches/pve/0018-PVE-add-optional-buffer-size-to-QEMUFile.patch
@@ -186,10 +186,10 @@ index a390554208..eda093b16a 100644
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QEMUFile, qemu_fclose)
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index 56c60b345f..a460970dd5 100644
+index fe455016ab..91a3f404fb 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
-@@ -425,7 +425,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
+@@ -444,7 +444,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(snap_state.target,
&snap_state.bs_pos));
@@ -198,7 +198,7 @@ index 56c60b345f..a460970dd5 100644
object_unref(ioc);
if (!snap_state.file) {
-@@ -589,7 +589,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
+@@ -603,7 +603,7 @@ int load_snapshot_from_blockdev(const char *filename, Error **errp)
/* restore the VM state */
QIOChannel *ioc = QIO_CHANNEL(qio_channel_savevm_async_new(be, &bs_pos));
diff --git a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
index d87875b..410f13b 100644
--- a/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
+++ b/debian/patches/pve/0046-savevm-async-reuse-migration-blocker-check-for-snaps.patch
@@ -136,10 +136,10 @@ index b6888daced..80eb0dcd1f 100644
bool migration_in_postcopy(void);
bool migration_postcopy_is_alive(MigrationStatus state);
diff --git a/migration/savevm-async.c b/migration/savevm-async.c
-index a460970dd5..b703c5d5a3 100644
+index 91a3f404fb..6c16000898 100644
--- a/migration/savevm-async.c
+++ b/migration/savevm-async.c
-@@ -390,7 +390,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
+@@ -409,7 +409,7 @@ void qmp_savevm_start(const char *statefile, bool has_skip_vm_start,
return;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 14+ messages in thread
* partially-applied: [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
` (8 preceding siblings ...)
2026-09-07 9:47 ` [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot Erik Fastermann
@ 2026-09-08 14:21 ` Fiona Ebner
9 siblings, 0 replies; 14+ messages in thread
From: Fiona Ebner @ 2026-09-08 14:21 UTC (permalink / raw)
To: pve-devel, Erik Fastermann
On Mon, 07 Sep 2026 11:47:34 +0200, Erik Fastermann wrote:
> Snapshotting a paused VM left it in finish-migrate, where it stayed
> until the VM was restarted. Fix this by storing the run state instead
> and put it back, similar to migration_iteration_finish().
>
> While working on that, I noticed some memory leaks and other issues,
> which I found by using sanitizer tools like ASAN and reading the code
> and docs.
>
> [...]
Applied the first five patches, thanks!
[1/9] d/patches: normalize header of pbs-restore no-cache patch
commit: 7498b1bd7b71da1465631267620400b541d1de15
[2/9] pbs-restore: keep callback data on the stack
commit: 10fd013483270154354657766e6645ca3e63af24
[3/9] migration/qemu-file: allocate buffer with g_malloc
commit: 45d5a170a7c8628b1c743417b7e3d2a65a67033f
[4/9] migration/pbs-state: check allocation of incoming state buffer
commit: b6f2adbcea48c6b13613d638fc45643d4181471e
[5/9] savevm-async: drop QIOChannel reference
commit: 513917f9a745c641c6a783d35dd340beb469b785
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH qemu v2 6/9] savevm-async: free migration vmdesc
2026-09-07 9:47 ` [PATCH qemu v2 6/9] savevm-async: free migration vmdesc Erik Fastermann
@ 2026-09-08 14:22 ` Fiona Ebner
0 siblings, 0 replies; 14+ messages in thread
From: Fiona Ebner @ 2026-09-08 14:22 UTC (permalink / raw)
To: Erik Fastermann, pve-devel
Am 07.09.26 um 11:48 AM schrieb Erik Fastermann:
> diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx
> @@ -149,10 +150,10 @@ index 0222d5ea6e..90d62d5723 100644
> ), gnutls, zlib)
> diff --git a/migration/savevm-async.c b/migration/savevm-async.c
> new file mode 100644
> -index 0000000000..cab08643ad
> +index 0000000000..4f44c2f12d
> --- /dev/null
> +++ b/migration/savevm-async.c
> -@@ -0,0 +1,600 @@
> +@@ -0,0 +1,608 @@
> +#include "qemu/osdep.h"
> +#include "migration/channel-savevm-async.h"
> +#include "migration/migration.h"
> @@ -261,6 +262,15 @@ index 0000000000..cab08643ad
> + return info;
> +}
> +
> ++static void migration_state_cleanup(MigrationState *ms, bool failed)
> ++{
> ++ migrate_set_state(&ms->state, MIGRATION_STATUS_SETUP,
> ++ failed ? MIGRATION_STATUS_FAILED : MIGRATION_STATUS_COMPLETED);
> ++ ms->to_dst_file = NULL;
> ++ /* see migration_cleanup_json_writer */
> ++ g_clear_pointer(&ms->vmdesc, json_writer_free);
> ++}
> ++
> +static int save_snapshot_cleanup(void)
> +{
> + int ret = 0;
> @@ -359,10 +369,7 @@ index 0000000000..cab08643ad
> + DPRINTF("timing: process_savevm_finalize (state saving) took %ld ms\n",
> + qemu_clock_get_ms(QEMU_CLOCK_REALTIME) - start_time);
> +
> -+ /* clear migration state */
> -+ migrate_set_state(&ms->state, MIGRATION_STATUS_SETUP,
> -+ ret || aborted ? MIGRATION_STATUS_FAILED : MIGRATION_STATUS_COMPLETED);
> -+ ms->to_dst_file = NULL;
> ++ migration_state_cleanup(ms, ret || aborted);
> +
> + /*
> + * Same as in migration_iteration_finish(): saving RAM might've turned on CPU throttling for
> @@ -618,6 +625,8 @@ index 0000000000..cab08643ad
> + return;
> +
> +fail:
> ++ migration_state_cleanup(ms, true);
While fine in practice, because the implementations handle it, it reads
wrong to have the migrate_set_state() and g_clear_pointer(&ms->vmdesc,
...) calls when we fail before having a successful migrate_init(). Can
we guard this call based on whether we already have a successful init?
> ++
> + savevm_cleanup_iothread();
> + save_snapshot_error("setup failed");
> +}
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot
2026-09-07 9:47 ` [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot Erik Fastermann
@ 2026-09-08 14:22 ` Fiona Ebner
0 siblings, 0 replies; 14+ messages in thread
From: Fiona Ebner @ 2026-09-08 14:22 UTC (permalink / raw)
To: Erik Fastermann, pve-devel
Am 07.09.26 um 11:49 AM schrieb Erik Fastermann:
> @@ -230,6 +231,24 @@ index 0000000000..56c60b345f
> + return snap_state.state == SAVE_STATE_COMPLETED && snap_state.skip_vm_start;
> +}
> +
> ++static void restore_vm_old_state(void)
> ++{
> ++ RunState old_state = snap_state.vm_old_state;
> ++
> ++ if (old_state == RUN_STATE__MAX) {
> ++ return;
> ++ }
> ++ snap_state.vm_old_state = RUN_STATE__MAX;
> ++
> ++ if (runstate_is_live(old_state)) {
> ++ if (!should_skip_vm_start() && !runstate_check(RUN_STATE_SHUTDOWN)) {
> ++ vm_start();
> ++ }
> ++ } else if (runstate_check(RUN_STATE_FINISH_MIGRATE)) {
I'd prefer this condition to be dropped or extended to also check for
RUN_STATE_SAVE_VM. Because when not using a statefile, we call
vm_stop(RUN_STATE_SAVE_VM) and do not set the state to
RUN_STATE_FINISH_MIGRATE. It might be fine in practice, because if the
runstate was not live, then RUN_STATE_SAVE_VM is not actually applied,
as vm_stop() skips setting the state if already not live. But still, it
would be good to be explicit about it.
> ++ runstate_set(old_state);
> ++ }
> ++}
> ++
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference
2026-09-07 9:47 ` [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference Erik Fastermann
@ 2026-09-08 14:22 ` Fiona Ebner
0 siblings, 0 replies; 14+ messages in thread
From: Fiona Ebner @ 2026-09-08 14:22 UTC (permalink / raw)
To: Erik Fastermann, pve-devel
Am 07.09.26 um 11:49 AM schrieb Erik Fastermann:
> qemu_file_new_impl() takes its own reference and qemu_fclose() drops
> exactly one, so the reference from qio_channel_savevm_async_new() was
> never released. The channel also holds a BlockBackend reference that
> only its finalizer drops, so both leaked on every snapshot.
>From a quick look, it seems like there are places in upstream code that
suffer from the same issue as well. For example, qemu_fopen_bdrv().
Could you check this and send patches upstream to fix the actually
affected ones?
https://www.qemu.org/docs/master/devel/submitting-a-patch.html
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-09-08 14:22 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-07 9:47 [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 1/9] d/patches: normalize header of pbs-restore no-cache patch Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 2/9] pbs-restore: keep callback data on the stack Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 3/9] migration/qemu-file: allocate buffer with g_malloc Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 4/9] migration/pbs-state: check allocation of incoming state buffer Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 5/9] savevm-async: drop QIOChannel reference Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-07 9:47 ` [PATCH qemu v2 6/9] savevm-async: free migration vmdesc Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-07 9:47 ` [PATCH qemu v2 7/9] savevm-async: release resources on start failure path Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 8/9] savevm-async: include reason when file open fails Erik Fastermann
2026-09-07 9:47 ` [PATCH qemu v2 9/9] savevm-async: restore run state from before snapshot Erik Fastermann
2026-09-08 14:22 ` Fiona Ebner
2026-09-08 14:21 ` partially-applied: [PATCH qemu v2 0/9] savevm-async: fix stuck paused vm after snapshot Fiona Ebner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox