@samitouri / QOSamiQemu / commits / 1bcb0ecfb5

migration: fix QIOChannelFile leak on error in file_connect_outgoing

Commit 03a680c978 changed g_autoptr(QIOChannelFile) to a plain pointer but failed to restore the necessary object_unref() calls on error paths. Previously, these were handled implicitly by the g_autoptr cleanup mechanism. Two error paths currently leak the QIOChannelFile object and its underlying file descriptor: 1. When ftruncate() fails (e.g., on character or block devices). 2. When qio_channel_io_seek() fails after the channel is created. In environments that retry migration automatically (e.g., libvirt), these FDs accumulate until QEMU hits RLIMIT_NOFILE and fails with EMFILE (Too many open files). Add the missing object_unref() calls to both error paths to ensure resources are properly released. Signed-off-by: Trieu Huynh <vikingtc4@gmail.com> Reviewed-by: Peter Xu <peterx@redhat.com> Link: https://lore.kernel.org/qemu-devel/20260328121215.159532-1-vikingtc4@gmail.com Signed-off-by: Fabiano Rosas <farosas@suse.de>

Trieu Huynh committed Mar 28, 2026 at 21:12 UTC 1bcb0ecfb5ff23f55f071ad53ac4f0e9fc3dc4af
2 files changed +50
migration/file.c
+2
@@ -112,6 +112,7 @@ QIOChannel *file_connect_outgoing(MigrationState *s,
112 error_setg_errno(errp, errno,
113 "failed to truncate migration file to offset %" PRIx64,
114 offset);
115 + object_unref(OBJECT(fioc));
116 goto out;
117 }
118
@@ -119,6 +120,7 @@ QIOChannel *file_connect_outgoing(MigrationState *s,
120
121 ioc = QIO_CHANNEL(fioc);
122 if (offset && qio_channel_io_seek(ioc, offset, SEEK_SET, errp) < 0) {
123 + object_unref(OBJECT(fioc));
124 ioc = NULL;
125 goto out;
126 }
tests/qtest/migration/file-tests.c
+48
@@ -20,6 +20,51 @@
20
21 static char *tmpfs;
22
23 +static int count_proc_fds(pid_t pid)
24 +{
25 + g_autofree char *fddir = g_strdup_printf("/proc/%d/fd", (int)pid);
26 + GDir *dir = g_dir_open(fddir, 0, NULL);
27 + int count = 0;
28 +
29 + if (!dir) {
30 + return -1;
31 + }
32 + while (g_dir_read_name(dir)) {
33 + count++;
34 + }
35 + g_dir_close(dir);
36 + return count;
37 +}
38 +
39 +static void test_file_connect_outgoing_fd_leak(char *name, MigrateCommon *args)
40 +{
41 + QTestState *from, *to;
42 + int fd_before, fd_after;
43 + const int retries = 5;
44 + int i;
45 + if (!g_file_test("/dev/full", G_FILE_TEST_EXISTS)) {
46 + g_test_skip("/dev/full not available");
47 + return;
48 + }
49 +
50 + args->listen_uri = "defer";
51 + if (migrate_start(&from, &to, args->listen_uri, &args->start)) {
52 + return;
53 + }
54 +
55 + fd_before = count_proc_fds(qtest_pid(from));
56 + g_assert_cmpint(fd_before, >, 0);
57 + for (i = 0; i < retries; i++) {
58 + migrate_qmp_fail(from, "file:/dev/full", NULL, "{}");
59 + migration_event_wait(from, "failed");
60 + }
61 +
62 + fd_after = count_proc_fds(qtest_pid(from));
63 + g_assert_cmpint(fd_after, >, 0);
64 + g_assert_cmpint(fd_after, ==, fd_before);
65 + migrate_end(from, to, false);
66 +}
67 +
68 static void test_precopy_file(char *name, MigrateCommon *args)
69 {
70 g_autofree char *uri = g_strdup_printf("file:%s/%s", tmpfs,
@@ -314,6 +359,9 @@ void migration_test_add_file(MigrationTestEnv *env)
359 migration_test_add("/migration/precopy/file/offset/bad",
360 test_precopy_file_offset_bad);
361
362 + migration_test_add("/migration/precopy/file/connect-outgoing-fd-leak",
363 + test_file_connect_outgoing_fd_leak);
364 +
365 migration_test_add("/migration/precopy/file/mapped-ram",
366 test_precopy_file_mapped_ram);
367 migration_test_add("/migration/precopy/file/mapped-ram/live",