From: Fabiano Rosas <farosas@suse.de>
To: Peter Xu <peterx@redhat.com>
Cc: qemu-devel@nongnu.org, Akash Kulhalli <akulhall@redhat.com>,
Juraj Marcin <jmarcin@redhat.com>
Subject: Re: [PATCH 6/7] migration: Fix incoming channel race with multifd and preempt_pre_7_2
Date: Mon, 28 Sep 2026 17:38:16 -0300 [thread overview]
Message-ID: <87cxtx862v.fsf@suse.de> (raw)
In-Reply-To: <arq-dDIjTbGSbT4W@zhexu-thinkpadt14gen5.rmtcaon.csb>
Peter Xu <peterx@redhat.com> writes:
> On Thu, Sep 24, 2026 at 06:54:25PM -0300, Fabiano Rosas wrote:
>> Peter Xu <peterx@redhat.com> writes:
>>
>> > PROBLEM
>> > =======
>> >
>> > Migration reliably hangs with old machine types (7.2-) when these migration
>> > capabilities are enabled: multifd, postcopy-ram, postcopy-preempt.
>> >
>> > Simplest reproducer :
>> >
>> > $ QTEST_QEMU_MACHINE_TYPE=pc-q35-7.2 \
>> > QTEST_QEMU_BINARY=./qemu-system-x86_64 \
>> > ./tests/qtest/migration-test --full \
>> > -r /x86_64/migration/multifd+postcopy/preempt/plain
>> >
>> > Current workaround is to kill destination QEMU, cancel the migration on
>> > source (with/without yank), disable one or more of the features, and retry.
>> >
>> > The hang is caused by receiving preempt channel before most of the multifd
>> > channels, the peek buffer operation will stuck forever.
>> >
>> > Incoming side QEMU hang (with relevant tracepoints enabled):
>> >
>> > migrate_set_state new state setup
>> > migration_socket_incoming_accepted
>> > migration_set_incoming_channel ioc=0x557d82c1caa0 ioctype=qio-channel-socket
>> > migration_socket_incoming_accepted
>> > migration_set_incoming_channel ioc=0x557d83023840 ioctype=qio-channel-socket
>> > multifd_recv_new_channel channel 1
>> > migration_socket_incoming_accepted
>> > migration_set_incoming_channel ioc=0x557d830c08c0 ioctype=qio-channel-socket
>> > multifd_recv_new_channel channel 0
>> > multifd_recv_thread_start 1
>> > migration_socket_incoming_accepted
>> > migration_set_incoming_channel ioc=0x557d8284a010 ioctype=qio-channel-socket
>> > multifd_recv_thread_start 0
>> > multifd_recv_unfill channel 0 packet_num 0 flags 0x1 next packet size 0
>> > multifd_recv_unfill channel 1 packet_num 1 flags 0x1 next packet size 0
>> > [dest qemu hangs here]
>> >
>> > Incoming side backtrace:
>> >
>> > __syscall_cancel_arch
>> > __internal_syscall_cancel
>> > __syscall_cancel
>> > recvmsg
>> > qio_channel_socket_readv
>> > qio_channel_readv_full
>> > migration_channel_read_peek
>> > migration_channel_identify
>> > migration_channel_process_incoming
>> > socket_accept_incoming_migration
>> > qio_net_listener_channel_func
>> > qio_channel_fd_source_dispatch
>> > g_main_context_dispatch_unlocked.lto_priv
>> > g_main_context_dispatch
>> > glib_pollfds_poll
>> > os_host_main_loop_wait
>> > main_loop_wait
>> > qemu_main_loop
>> > qemu_default_main
>> > main
>> >
>> > This patch tries to fix it, from incoming side first. This patch alone
>> > should enable single-direction migration to this new QEMU. The next patch
>> > will propose a fix for the outgoing path, fixing new->old migrations.
>> >
>> > BACKGROUND
>> > ==========
>> >
>> > preempt_pre_7_2 used to be a flag marking QEMU releases 7.1/7.2 only
>> > because they have a flaw on channel establishment on preempt channel.
>> >
>> > Now, after we have multifd + postcopy support, new QEMU binaries can
>> > migrate old machine types with both features enabled. Juraj reported
>> > constant migration hang at the very beginning for those old machine types
>> > in our downstream systems with such setup. Juraj found it's because of
>> > this flag.
>> >
>> > QEMU source initiates channels in this order on current master when both
>> > features enabled with postcopy-preempt mode:
>> >
>> > - First, main channel connect() happens,
>> > - Second, postcopy_preempt_setup() invokes connect() for preempt channel,
>> > - Last, in migration_thread(), multifd_send_setup() invokes connect()s
>> > for all the multifd channels
>> >
>> > However on an old QEMU, it expect the channels arrive in order of
>> > "main"->"multifd"->"preempt". Even on a new QEMU, the destination expects
>> > the same order of arrival on the channels when the channels are not
>> > peekable.
>> >
>> > For peekable channels (non-tls sockets), this time we'll likely see preempt
>> > channel appearing very soon, peeking at it causing main thread hang, like
>> > mentioned in the PROBLEM section above.
>> >
>> > SOLUTION
>> > ========
>> >
>> > One major problem of current hang is we try to peek at preempt channel,
>> > which offers nothing at the beginning.
>> >
>> > Since we'll need to accept stream from old binaries, arrival of preempt
>> > channel early is unavoidable.
>> >
>> > What we can do, though, is to allow concurrent monitoring of all channels
>> > rather than one. Meanwhile, we shouldn't peek at anything that has no
>> > data.
>> >
>> > migration_channel_read_peek() almost does the wait, but it's too late.
>> > This patch proposes to watch all channels from the start without reading at
>> > them at all, relying on an IO watch attached. This will make sure (1) for
>> > preempt channel, we never read on it (watch can be gracefully disarmed when
>> > we know it's preempt channel later) (2) since we have one IO watch for each
>> > channel, then we can allow accept() to happen in almost any order, process
>> > only when there's some header to read.
>> >
>> > To maintain these watches, a new GArray @channels_early is introduced, with
>> > each element to be a (ioc, GSource) tuple (@MigEarlyIncomingChannel), so
>> > that QEMU can manage all the IO watches later with it. Here I used GSource
>> > rather than io_tag because the GSource may or may not be attached to the
>> > default main thread gcontext: when OOB is used, it may be the iothread
>> > context instead. IOW, g_source_remove() on io_tags stops working, which
>> > only works for default main context.
>> >
>> > The trick this patch used is when we have the ability to wait on all
>> > channel headers at the same time, we pick one by one from main and multifd
>> > channels (since both of the types will contain headers dumped right at the
>> > very beginning), leave preempt channel alone. It means, after dest QEMU
>> > finds all main and multifd channels, there are three possibilities:
>> >
>> > - If preempt not enabled, we finished full channel establishment, done.
>> >
>> > - If preempt enabled,
>> >
>> > - If we have one registered IO watch with the IOC, it must be the
>> > preempt channel,
>> >
>> > - If we have no further IO watch, wait for the next accept(), which
>> > must be the preempt channel.
>> >
>> > This will make channel establish work on incoming side even with the old
>> > QEMU binaries.
>> >
>> > While working on it, I managed to refactor the peeking logic quite a bit:
>> > we used to have a very convoluted migration_channel_identify() function,
>> > taking care of two different cases:
>> >
>> > (1) when the channel is peekable (like normal socket channels),
>> > (2) when the channel is not peekable (like TLS or file migrations).
>> >
>> > This patch refactored that part to make it crystal clear on how we make
>> > decisions on which channel is which, and split the peek/identify paths.
>> >
>> > One example incoming migration from pc-q35-7.2 after this patch applied,
>> > and with relevant tracepoints set (8 multifd channels, 1 preempt channel):
>> >
>> > migrate_set_state new state setup
>> > migration_channel_process_incoming ioc=0x557dc8af4e20 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc8af4e20 ioctype=qio-channel-socket
>> > migration_channel_process_incoming ioc=0x557dc8abc5d0 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc8abc5d0 ioctype=qio-channel-socket
>> > migration_channel_process_incoming ioc=0x557dc953d680 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc953d680 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc8abc5d0 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc898a820 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc898a820 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc953d680 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc919bd70 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc919bd70 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc898a820 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc8734b20 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc8734b20 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc919bd70 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc898bdf0 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc898bdf0 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc8734b20 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc968cfd0 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc968cfd0 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc898bdf0 ioctype=qio-channel-socket type=MULTIFD
>> > migration_channel_process_incoming ioc=0x557dc8c31810 ioctype=qio-channel-socket
>> > migration_incoming_channel_watch ioc=0x557dc8c31810 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc968cfd0 ioctype=qio-channel-socket type=MULTIFD
>> > migration_set_incoming_channel ioc=0x557dc8c31810 ioctype=qio-channel-socket type=MULTIFD
>> > migration_set_incoming_channel ioc=0x557dc8af4e20 ioctype=qio-channel-socket type=MAIN
>> > migration_start_incoming
>> > migrate_set_state new state active
>> > migration_channel_process_incoming ioc=0x557dc8966160 ioctype=qio-channel-socket
>> > migration_set_incoming_channel ioc=0x557dc8966160 ioctype=qio-channel-socket type=PREEMPT
>> >
>> > While at it, I renamed the four channel incoming/outgoing tracepoints, make
>> > all to follow migration_[incoming|outgoing]_channel_*(). Especially for
>> > incoming, then we can enable migration_incoming_channel* for incoming
>> > channel debuggings.
>> >
>> > FINAL NOTE ON TLS
>> > =================
>> >
>> > Note that this solution isn't a magic bullet fixing everything. TLS is not
>> > fixable with this approach because TLS isn't peekable channel, hence the
>> > watch won't help.
>> >
>> > We can have other ways to hack it to make it work, e.g. we can make TLS
>> > channels peekable too, but I don't want to pollute the TLS iochannel only
>> > for this: let's hope handshake will arrive soon so we can drop these code.
>> >
>> > For now, when TLS is involved with such old machine type (7.2-), one needs
>> > to migrate it out with any combination of features except enabling all
>> > four (multifd + TLS + postcopy + preempt) to a patched binary, after that,
>> > all combinations will start working.
>> >
>> > BugLink: https://redhat.atlassian.net/browse/RHEL-254933
>> > Fixes: e27418861288 ("migration: enable multifd and postcopy together")
>> > Reported-by: Juraj Marcin <jmarcin@redhat.com>
>> > Signed-off-by: Peter Xu <peterx@redhat.com>
>> > ---
>> > migration/channel.h | 3 +
>> > migration/migration.h | 35 ++++
>> > migration/channel.c | 414 +++++++++++++++++++++++++++++++++++------
>> > migration/migration.c | 23 ++-
>> > migration/trace-events | 7 +-
>> > 5 files changed, 421 insertions(+), 61 deletions(-)
>>
>> Hi Peter, I think the code looks good, but there are some twists and
>> turns that I think we can get rid of. I put a fixup patch [1] at the end
>> with my suggestions and mentioned inline what I changed and why.
>
> Thanks, especially for covering the tests while suggesting.
>
> I believe I agree with many of them, I'll comment on the rest, see inline.
>
>>
>> >
>> > diff --git a/migration/channel.h b/migration/channel.h
>> > index 678712f291..40bd339a3e 100644
>> > --- a/migration/channel.h
>> > +++ b/migration/channel.h
>> > @@ -49,4 +49,7 @@ bool migration_channel_parse_input(const char *uri,
>> > MigrationChannel **main_channelp,
>> > MigrationChannel **cpr_channelp,
>> > Error **errp);
>> > +
>> > +void migration_incoming_free_early_channels(MigrationIncomingState *mis);
>> > +
>> > #endif
>> > diff --git a/migration/migration.h b/migration/migration.h
>> > index 683bc7bdd5..6f23e1805b 100644
>> > --- a/migration/migration.h
>> > +++ b/migration/migration.h
>> > @@ -90,6 +90,28 @@ typedef enum {
>> > PREEMPT_THREAD_QUIT,
>> > } PreemptThreadStatus;
>> >
>> > +typedef struct {
>> > + /*
>> > + * The GSource we have allocated to track G_IO_IN events for the
>> > + * channel. Note: it may not be attached to the default main gcontext,
>> > + * for example, when in case of io watch created during an OOB
>> > + * migrate-recover command.
>> > + */
>> > + GSource *source;
>> > + /* The channel we will identify later after getting some buffers to read */
>> > + QIOChannel *ioc;
>> > +} MigEarlyIncomingChannel;
>> > +
>> > +typedef struct {
>> > + GArray *channels;
>> > + /*
>> > + * In most cases, channels are only operated with BQl, but since we
>> > + * have OOB command support, don't assume it, just use a standalone
>> > + * mutex to protect any access to the channels array.
>> > + */
>> > + QemuMutex mutex;
>> > +} MigEarlyIncomingChannels;
>> > +
>> > /* State for the incoming migration */
>> > struct MigrationIncomingState {
>> > QEMUFile *from_src_file;
>> > @@ -255,6 +277,19 @@ struct MigrationIncomingState {
>> >
>> > /* Do exit on incoming migration failure */
>> > bool exit_on_error;
>> > +
>> > + /*
>> > + * Maintains all channels only at early stage. After incoming
>> > + * migration started, this should be an empty array and no use anymore.
>> > + *
>> > + * TODO: we logically only need this to make new QEMU binaries work for
>> > + * old machines who have @preempt_pre_7_2 set on the source, but it's
>> > + * also good to have this early stage anyway, making sure when peek the
>> > + * channel there will be something in the channel. Ideally, we should
>> > + * manage all migration channels in the future with proper headers,
>> > + * then we can drop this.
>> > + */
>> > + MigEarlyIncomingChannels channels_early;
>> > };
>> >
>> > MigrationIncomingState *migration_incoming_get_current(void);
>> > diff --git a/migration/channel.c b/migration/channel.c
>> > index b9065efd03..acb7ef5206 100644
>> > --- a/migration/channel.c
>> > +++ b/migration/channel.c
>> > @@ -12,6 +12,8 @@
>> >
>> > #include "qemu/osdep.h"
>> > #include "qemu/cutils.h"
>> > +#include "qemu/main-loop.h"
>> > +#include "qemu/error-report.h"
>> > #include "channel.h"
>> > #include "exec.h"
>> > #include "fd.h"
>> > @@ -146,61 +148,84 @@ bool migration_has_all_channels(void)
>> > return true;
>> > }
>> >
>> > -static MigChannelType migration_channel_identify(MigrationIncomingState *mis,
>> > - QIOChannel *ioc, Error **errp)
>> > +static bool qio_channel_is_peekable(QIOChannel *ioc)
>> > +{
>> > + return qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_READ_MSG_PEEK);
>> > +}
>> > +
>> > +/*
>> > + * With multiple channels, it is possible that we receive channels out of
>> > + * order on destination side, causing incorrect mapping of source channels
>> > + * on destination side.
>> > + *
>> > + * When the channel is peekable (e.g. non-TLS socket channels), check
>> > + * channel MAGIC to decide type of channel.
>> > + *
>> > + * Please note this is best effort, postcopy preempt channel does not send
>> > + * any magic number so avoid it for postcopy live migration.
>> > + *
>> > + * Returns: >0 if success, ==0 (CH_NONE) if error. If error happened,
>> > + * *errp must be set.
>> > + */
>> > +static MigChannelType migration_channel_peek(MigrationIncomingState *mis,
>> > + QIOChannel *ioc,
>> > + Error **errp)
>> > {
>> > MigChannelType channel = CH_NONE;
>> > uint32_t channel_magic = 0;
>> > int ret = 0;
>> >
>> > - if (!migration_has_main_and_multifd_channels()) {
>> > - if (qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_READ_MSG_PEEK)) {
>> > - /*
>> > - * With multiple channels, it is possible that we receive channels
>> > - * out of order on destination side, causing incorrect mapping of
>> > - * source channels on destination side. Check channel MAGIC to
>> > - * decide type of channel. Please note this is best effort,
>> > - * postcopy preempt channel does not send any magic number so
>> > - * avoid it for postcopy live migration. Also tls live migration
>> > - * already does tls handshake while initializing main channel so
>> > - * with tls this issue is not possible.
>> > - */
>> > - ret = migration_channel_read_peek(ioc, (void *)&channel_magic,
>> > - sizeof(channel_magic), errp);
>> > - if (ret != 0) {
>> > - goto out;
>> > - }
>> > + assert(qio_channel_is_peekable(ioc));
>> >
>> > - channel_magic = be32_to_cpu(channel_magic);
>> > - if (channel_magic == QEMU_VM_FILE_MAGIC) {
>> > - channel = CH_MAIN;
>> > - } else if (channel_magic == MULTIFD_MAGIC) {
>> > - assert(migrate_multifd());
>> > - channel = CH_MULTIFD;
>> > - } else if (!mis->from_src_file &&
>> > - mis->state == MIGRATION_STATUS_POSTCOPY_PAUSED) {
>> > - /* reconnect main channel for postcopy recovery */
>> > - channel = CH_MAIN;
>> > - } else {
>> > - error_setg(errp, "unknown channel magic: %u", channel_magic);
>> > - }
>> > - } else if (mis->from_src_file && migrate_multifd()) {
>> > - /*
>> > - * Non-peekable channels like tls/file are processed as
>> > - * multifd channels when multifd is enabled.
>> > - */
>> > - channel = CH_MULTIFD;
>> > - } else if (!mis->from_src_file) {
>> > - channel = CH_MAIN;
>> > - } else {
>> > - error_setg(errp, "non-peekable channel used without multifd");
>> > - }
>> > + ret = migration_channel_read_peek(ioc, (void *)&channel_magic,
>> > + sizeof(channel_magic), errp);
>> > + if (ret != 0) {
>> > + /* Failed */
>> > + return channel;
>> > + }
>> > +
>> > + channel_magic = be32_to_cpu(channel_magic);
>> > +
>> > + if (channel_magic == QEMU_VM_FILE_MAGIC) {
>> > + channel = CH_MAIN;
>> > + } else if (channel_magic == MULTIFD_MAGIC) {
>> > + assert(migrate_multifd());
>> > + channel = CH_MULTIFD;
>> > + } else if (!mis->from_src_file &&
>> > + mis->state == MIGRATION_STATUS_POSTCOPY_PAUSED) {
>> > + /* reconnect main channel for postcopy recovery */
>> > + channel = CH_MAIN;
>>
>> This is not peeking, maybe it should be somewhere else. Not this patch's
>> fault, we can think about it some other day.
>
> If we separate "peek" from "identify", this one still falls into "peek".
>
> Per my definition, the latter "identify" only does "pick channels by order
> they arrive". Then this doesn't belong to that. As it needs to "peek for
> at least something readable", rather than blindly rely on the order of
> arrival.
>
> But yeah, it'll be good we leave this for later regardless.
>
>>
>> > } else {
>> > + error_setg(errp, "Unknown channel magic: %u", channel_magic);
>> > + }
>> > +
>> > + return channel;
>> > +}
>> > +
>> > +/*
>> > + * Returns: >0 if success, ==0 (CH_NONE) if error. If error happened,
>> > + * *errp must be set.
>> > + */
>> > +static MigChannelType migration_channel_identify(MigrationIncomingState *mis,
>> > + QIOChannel *ioc, Error **errp)
>> > +{
>> > + MigChannelType channel = CH_NONE;
>> > +
>> > + if (migration_has_main_and_multifd_channels()) {
>> > assert(migrate_postcopy_preempt());
>> > channel = CH_POSTCOPY;
>>
>> This check is the same as the one at
>> migration_channel_process_incoming(). I think we can simplify this.
>
> True, this path should be dead code when original patch applied. I have a
> slight preference on keeping the other one instead (which I know you tend
> to think "awkward"). I'll explain below, and we'll see.
>
>>
>> See [1].
>>
>> > + } else if (!mis->from_src_file) {
>> > + channel = CH_MAIN;
>> > + } else if (migrate_multifd()) {
>> > + /*
>> > + * Non-peekable channels like tls/file are processed as
>> > + * multifd channels when multifd is enabled.
>> > + */
>> > + channel = CH_MULTIFD;
>> > + } else {
>> > + error_setg(errp, "Unexpected non-peekable channel observed");
>> > }
>> >
>> > -out:
>> > return channel;
>> > }
>> >
>> > @@ -214,31 +239,275 @@ static void migration_incoming_error_propagate(MigrationIncomingState *mis,
>> > }
>> > }
>> >
>> > +/* Must be with mis->channels_early.mutex held */
>> > +static void
>> > +migration_incoming_early_channel_insert(MigrationIncomingState *mis,
>> > + QIOChannel *ioc,
>> > + GSource *source)
>> > +{
>> > + MigEarlyIncomingChannel chan = {
>> > + .source = source,
>> > + .ioc = ioc,
>> > + };
>> > +
>> > + object_ref(OBJECT(ioc));
>> > + g_source_ref(source);
>> > +
>> > + g_array_append_val(mis->channels_early.channels, chan);
>> > +}
>> > +
>> > +static void migration_incoming_early_channel_free(GArray *channels, int i)
>> > +{
>> > + MigEarlyIncomingChannel *chan;
>> > +
>> > + chan = &g_array_index(channels, MigEarlyIncomingChannel, i);
>> > +
>> > + g_source_destroy(chan->source);
>> > + g_source_unref(chan->source);
>> > + object_unref(OBJECT(chan->ioc));
>> > +
>> > + g_array_remove_index_fast(channels, i);
>> > +}
>> > +
>> > +/*
>> > + * Remove this channel from the monitoring of @channels_early array.
>> > + * Return true if found and successful, false otherwise.
>> > + */
>> > +static bool
>> > +migration_incoming_early_channel_remove(MigrationIncomingState *mis,
>> > + QIOChannel *ioc)
>> > +{
>> > + GArray *channels = mis->channels_early.channels;
>> > + MigEarlyIncomingChannel *chan;
>> > + int i;
>> > +
>> > + QEMU_LOCK_GUARD(&mis->channels_early.mutex);
>> > +
>> > + for (i = 0; i < channels->len; i++) {
>> > + chan = &g_array_index(channels, MigEarlyIncomingChannel, i);
>> > + if (chan->ioc != ioc) {
>> > + continue;
>> > + }
>> > + migration_incoming_early_channel_free(channels, i);
>> > + return true;
>> > + }
>> > +
>> > + return false;
>> > +}
>> > +
>> > +void migration_incoming_free_early_channels(MigrationIncomingState *mis)
>> > +{
>> > + GArray *channels = mis->channels_early.channels;
>> > +
>> > + QEMU_LOCK_GUARD(&mis->channels_early.mutex);
>> > +
>> > + while (channels->len) {
>> > + migration_incoming_early_channel_free(channels, 0);
>> > + }
>> > +}
>> > +
>> > +static bool migration_incoming_setup_channel(QIOChannel *ioc,
>> > + MigChannelType ch,
>> > + Error **errp)
>> > +{
>> > + trace_migration_incoming_channel_set(ioc,
>> > + object_get_typename(OBJECT(ioc)),
>> > + mig_channel_str[ch]);
>> > + migration_ioc_register_yank(ioc);
>> > + /* TODO: make this return the success status instead */
>> > + migration_incoming_setup(ioc, ch, errp);
>>
>> I implemented this TODO. It allows us to skip the main+mfd check after
>> starting migration because migration_incoming_setup() already returns
>> that value. See [1].
>
> Actually now looking at this patch, this TODO is so trivial.. yes I'll do
> it.
>
> But when I do it, I think it too error prone to return a value returned
> from migration_has_main_and_multifd_channels() for this function. I
> changed it to simple true/false for success or not instead. I'll attach a
> 2nd version of the fixup patch at the end, applying most changes you
> suggested but stick with returning true/false for this one.
>
>>
>> > +
>> > + return *errp == NULL;
>> > +}
>> > +
>> > static bool migration_incoming_channel_install(MigrationIncomingState *mis,
>> > QIOChannel *ioc,
>> > Error **errp)
>> > {
>> > - MigChannelType ch = migration_channel_identify(mis, ioc, errp);
>> > + MigChannelType ch;
>> > + bool ret;
>> > +
>> > + if (qio_channel_is_peekable(ioc)) {
>>
>> This check is redundant, we can only arrive here via the watch and we
>> checked peekable already at migration_channel_process_incoming. See [1].
>
> Shouldn't? Because the file path uses migration_incoming_channel_install()
> too, afaict.
>
> But I see how your question came, which I'm guessing: I used this function
> in a peekable only path, so here what I can do minimum is I can move the
> migration_channel_peek() to the peekable path, then move the other path of
> migration_channel_identify() to the e.g. file path. I'll do that.
>
>>
>> > + ch = migration_channel_peek(mis, ioc, errp);
>> > + } else {
>> > + ch = migration_channel_identify(mis, ioc, errp);
>> > + }
>> >
>> > if (!ch) {
>> > - assert(*errp);
>> > return false;
>> > }
>> >
>> > - trace_migration_set_incoming_channel(ioc,
>> > - object_get_typename(OBJECT(ioc)),
>> > - mig_channel_str[ch]);
>> > - migration_ioc_register_yank(ioc);
>> > + ret = migration_incoming_setup_channel(ioc, ch, errp);
>> > + if (!ret) {
>> > + return false;
>> > + }
>> >
>> > - if (migration_incoming_setup(ioc, ch, errp)) {
>> > + /* Installation succeeded, kickoff migration if needed */
>> > + if (migration_has_main_and_multifd_channels()) {
>> > migration_start_incoming();
>> > }
>> >
>> > - if (*errp) {
>> > - return false;
>> > + return true;
>> > +}
>> > +
>> > +static void
>> > +migration_incoming_channel_preempt_setup(QIOChannel *ioc)
>> > +{
>> > + assert(migrate_postcopy_preempt());
>> > + /* Installation of preempt channel should never fail */
>> > + migration_incoming_setup_channel(ioc, CH_POSTCOPY, &error_abort);
>> > +}
>> > +
>> > +static void
>> > +migration_incoming_detect_preempt_channel(MigrationIncomingState *mis)
>> > +{
>> > + GArray *channels = mis->channels_early.channels;
>> > + MigEarlyIncomingChannel *chan;
>> > +
>> > + QEMU_LOCK_GUARD(&mis->channels_early.mutex);
>> > +
>> > + /* When preempt mode not enabled, nothing to detect.. */
>> > + if (!migrate_postcopy_preempt()) {
>> > + /*
>> > + * .. but if we found something pending, throw an error only, which
>> > + * should not happen. Even if it happens, resources will still be
>> > + * released after incoming migration is completedly.
>> > + */
>> > + if (channels->len) {
>> > + error_report("%s: Found %u unused channels",
>> > + __func__, channels->len);
>> > + }
>> > + return;
>> > }
>> >
>> > - return true;
>> > + /* Preempt channel hasn't yet arrived? Process it later */
>> > + if (!channels->len) {
>> > + return;
>> > + }
>> > +
>> > + /*
>> > + * More than one channel should never happen.. capture it in case if
>> > + * it happens, then there's not much we can do.
>> > + */
>> > + if (channels->len > 1) {
>> > + error_report("%s: Found %u unused channels, "
>> > + "can't identify preempt channel",
>> > + __func__, channels->len);
>> > + return;
>> > + }
>> > +
>> > + assert(channels->len == 1);
>> > + /* This is the preempt channel, install it directly */
>> > + chan = &g_array_index(channels, MigEarlyIncomingChannel, 0);
>> > + migration_incoming_channel_preempt_setup(chan->ioc);
>> > + migration_incoming_early_channel_free(channels, 0);
>> > +}
>> > +
>> > +static gboolean migration_incoming_channel_readable(QIOChannel *ioc,
>> > + GIOCondition condition,
>> > + gpointer opaque)
>> > +{
>> > + MigrationIncomingState *mis = opaque;
>> > + Error *local_err = NULL;
>> > +
>> > + /*
>> > + * No need to monitor this channel anymore as long as anything arrived,
>> > + * remove it from tracking.
>> > + *
>> > + * NOTE: this means if partial data arrived we may still block here,
>> > + * but it shouldn't happen in production, only malicious stream. Since
>> > + * migration stream is trusted (either due to trusted network, or TLS),
>> > + * that's non-issue.
>>
>> Could we check the amount of data and keep waiting?
>
> We could, but it is about the security side of things, I'd not bother in
> this patch, and maybe we don't bother forever, hopping that at the end of
> this year we enable handshake in next release, then this is a ioc read()
> block reading, as simple as that.
>
Ok, no worries.
>>
>> > + *
>> > + * NOTE2: this will also release the ioc ref that we used to hold, but
>> > + * it's fine since we have at least one more refcount in the current
>> > + * event handler.
>>
>> I don't understand "one more ref in the event handler". Looks like
>> you're trying to say the refcount is consistent, but why even mention
>> it? I may be missing something.
>
> Not about consistency, it's about holding at least one refcount to make sure
> migration_incoming_early_channel_remove() won't already free the IOC.
>
> But I think maybe you're right: likely nobody will notice the context of
> this path processing the gsource, and maybe this sentence brings more
> confusion than clarity? Let me know, I can drop this line.
>
You mean the inc at qio_channel_create_fd_watch()? Then we don't need
this comment.
>>
>> > + *
>> > + * NOTE3: it's theoretically possible that this entry is gone reaching
>> > + * here. Example: the main thread is doing incoming cleanup having this
>> > + * one removed, while this watch can be registered on the monitor
>> > + * iothread's context and fired at the exact same time but in the
>> > + * iothread instead. If it happens, skip the rest. I'm not sure if
>> > + * this could happen at all, may depend on iothread lifespan management
>> > + * in the main thread, but be prepared.
>>
>> I wonder if we should extend the locking a bit further. Not because of
>> what you mentioned here^, but because everything the code running on the
>> monitor thread touches can race with changes from the main loop. All of
>> these migrate_foo_bar() helpers, the QEMUFiles, caps, params, etc.
>
> Is that about the new lock (mis->channels_early.mutex) or existing code?
> Possible to do on top, or a new concern introduced from this patch?
>
If we can reach migration_channel_process_incoming() via OOB command,
then everything below it needs to be audited to ensure we're not
changing global state concurrently. Mostly pre-existing, but you are
adding more code inside the danger window.
>>
>> > + */
>> > + if (!migration_incoming_early_channel_remove(mis, ioc)) {
>> > + goto out;
>> > + }
>> > +
>>
>> I moved the peek into here and passed the already identified
>> channel into migration_incoming_channel_install(). See [1].
>
> Agree on this change, better.
>
>>
>> > + if (!migration_incoming_channel_install(mis, ioc, &local_err)) {
>> > + goto out;
>> > + }
>> > +
>> > + if (migration_has_main_and_multifd_channels()) {
>>
>> This check became redundant because the _setup routine already returns
>> it. See [1].
>
> I didn't adopt this in my new fixup, because I want to avoid returning that
> info deep from stack. Let's continue the discussion in my fixup there as a
> reply.
>
I wrote a migration_can_start() routine and tried to incorporate some
preempt channel knowledge into it, but it didn't work so well. I find it
a bit opaque to look only at main+mfd channels to start the
migration. Since nothing better came up so far, I agree you keep this
here.
>>
>> > + /*
>> > + * Possibilities when reaching here:
>> > + *
>> > + * (1) if preempt not enabled, this should be no-op, all done,
>> > + * (2) if preempt enabled,
>> > + * (2.a) preempt channel arrived @channels_early, handle it now
>> > + * (2.b) preempt channel not arrived, to be handled in
>> > + * migration_channel_process_incoming() later
>> > + */
>> > + migration_incoming_detect_preempt_channel(mis);
>>
>> Moved this into migration_incoming_channel_install(). See [1].
>
> Since my version used migration_incoming_channel_install() for both
> peekable and non-peekable, this path is not needed for non-peekable.
> I still left it here, assuming it's clearer it's only needed for peekable.
> But you can reply with your thought. I hope at least we can be more
> focused with most of the rest addressed.
>
I tend to prefer having a single path and make it a noop via code than
to have two paths and use our knowledge of the code to decide what goes
where. Here we can't escape having two paths, so it's not a huge deal.
>>
>> > + }
>> > +
>> > +out:
>> > + if (local_err) {
>> > + migration_incoming_error_propagate(mis, local_err);
>> > + }
>> > +
>> > + /*
>> > + * NOTE: we should have already detached the GSource, returning
>> > + * G_SOURCE_REMOVE to be logically consistent only.
>> > + */
>> > + return G_SOURCE_REMOVE;
>> > +}
>> > +
>> > +static void migration_incoming_channel_watch(MigrationIncomingState *mis,
>> > + QIOChannel *ioc)
>> > +{
>> > + GMainContext *context = g_main_context_get_thread_default();
>> > + GSource *source;
>> > + guint io_tag;
>> > +
>> > + trace_migration_incoming_channel_watch(ioc,
>> > + object_get_typename(OBJECT(ioc)));
>> > +
>> > + /*
>> > + * Careful: this can be run from either the main thread or the monitor
>> > + * iothread when migrate_recover is used with OOB=on, so we need to
>> > + * take the lock and use the full version to specify the correct
>> > + * context.
>> > + */
>> > + QEMU_LOCK_GUARD(&mis->channels_early.mutex);
>> > +
>> > + /*
>> > + * We should never watch an @ioc that is not peekable, because there's
>> > + * no point. What is worse is we lose the real order of accept()s via
>> > + * the asynchronous IO watch operation.
>> > + *
>> > + * Another note is TLS channel (non-peekable) may or may not work
>> > + * properly with IO watch due to its current .io_create_watch() impl,
>> > + * which is another story. Just guard both points.
>> > + */
>> > + assert(qio_channel_is_peekable(ioc));
>> > + io_tag = qio_channel_add_watch_full(ioc, G_IO_IN,
>> > + migration_incoming_channel_readable,
>> > + mis, NULL, context);
>> > +
>> > + /*
>> > + * Replace this if one day qio_channel_add_watch*() API can directly
>> > + * return the GSource*.. for now, stick with it.
>> > + */
>> > + source = g_main_context_find_source_by_id(context, io_tag);
>> > + /*
>> > + * Nothing can race with adding the IO watch, aka, concurrent removal
>> > + * is not possible when we have the lock. So it must be present.
>> > + */
>> > + assert(source);
>> > +
>> > + migration_incoming_early_channel_insert(mis, ioc, source);
>> > }
>> >
>> > /**
>> > @@ -254,13 +523,44 @@ void migration_channel_process_incoming(QIOChannel *ioc)
>> > MigrationIncomingState *mis = migration_incoming_get_current();
>> > Error *local_err = NULL;
>> >
>> > - trace_migration_channel_process_incoming(
>> > + trace_migration_incoming_channel_process(
>> > ioc, object_get_typename(OBJECT(ioc)));
>> >
>> > if (migrate_channel_requires_tls_upgrade(ioc)) {
>> > migration_tls_channel_process_incoming(ioc, &local_err);
>> > } else {
>> > - migration_incoming_channel_install(mis, ioc, &local_err);
>> > + if (migration_has_main_and_multifd_channels()) {
>>
>> The fact that this check must come before everything is a major
>> weirdness. In fact, this is conceptually the same case as the first
>> branch in migration_channel_identify(). I therefore moved that function
>> to this point and added a comment explaining why this must come
>> first. See [1].
>
> This is one part that I left alone so far. My reason is that preempt
> channel needs special care at least for peekable, it's because it's not
> peekable there when hitting this case. So we need to have this separate
> path. This is also relevant to below, where..
>
You removed the check from _identify, so it's less confusing now.
>>
>> > + /*
>> > + * If all main+multifd channels are present already, this must
>> > + * be the preempt channel.
>> > + *
>> > + * QEMU can't register an IO watch for it if there is only the
>> > + * last preempt channel left, because it means the IO watch
>> > + * will never fire and nobody will pick it up: we rely on the
>> > + * one before the last one to pick both.
>> > + *
>> > + * See comment in migration_incoming_channel_readable() on
>> > + * the migration_incoming_detect_preempt_channel() call.
>> > + */
>> > + migration_incoming_channel_preempt_setup(ioc);
>> > + } else {
>> > + /*
>> > + * Register an IO watch for peekable channels, so that channels
>> > + * can be accept()ed with any order.
>> > + *
>> > + * Non-peekable channels (file, TLS, etc.) cannot register IO
>> > + * watch, not only because there's no data to look at to help
>> > + * making the decision, but also because after registering we
>> > + * will lose the real ordering we get from accept(), which is
>> > + * still so far the only source of truth to identify a channel
>> > + * in such case.
>> > + */
>> > + if (qio_channel_is_peekable(ioc)) {
>>
>> I moved this line into migration_channel_identify(). That way we ensure
>> the ordering regarding the main+mfd check. The watch is still installed
>> at this point. See [1].
>
> ... you suggested moving this into migration_channel_identify().
>
> Personally I think it's against my purpose of split: I wanted to make
> migration_channel_identify() to only rely on order of arrival to identify
> channels, nothing else. Then for all peekable handling, it should be
> inside the _peek() function.
>
ok
>>
>> > + migration_incoming_channel_watch(mis, ioc);
>> > + } else {
>> > + migration_incoming_channel_install(mis, ioc, &local_err);
>>
>> This invocation of install does not check for the preempt channel after
>> it returns. Is this simply assuming that if a non-peekable channel is
>> present then there will be no preempt channel at all?
>>
>> Or if main+TLS is the first channel, then the next channel will be
>> preempt and already enter the first branch?
>
> Yes. It's because for non-peekable, preempt channel is only processed
> above (in my patch logic, with/without my new fixup to be attached).
>
> For peekable, preempt channel may be picked up by the 2nd last channel,
> which is not possible for non-peekable, so that is special way for peekable
> channels only.
>
>>
>> > + }
>> > + }
>> > }
>> >
>> > if (local_err) {
>> > @@ -270,7 +570,7 @@ void migration_channel_process_incoming(QIOChannel *ioc)
>> >
>> > void migration_channel_connect_outgoing(MigrationState *s, QIOChannel *ioc)
>> > {
>> > - trace_migration_set_outgoing_channel(ioc, object_get_typename(OBJECT(ioc)));
>> > + trace_migration_outgoing_channel_set(ioc, object_get_typename(OBJECT(ioc)));
>> >
>> > if (migrate_channel_requires_tls_upgrade(ioc)) {
>> > Error *local_err = NULL;
>> > diff --git a/migration/migration.c b/migration/migration.c
>> > index cfadcc79c2..4cc4077a76 100644
>> > --- a/migration/migration.c
>> > +++ b/migration/migration.c
>> > @@ -334,6 +334,10 @@ void migration_object_init(void)
>> > current_incoming->page_requested = g_tree_new(page_request_addr_cmp);
>> >
>> > current_incoming->exit_on_error = INMIGRATE_DEFAULT_EXIT_ON_ERROR;
>> > + /* zero_terminated=false, clear_=true */
>>
>> There's an extra _ after 'clear' I think.
>
> See? I know someone else will get confused too. :)
>
hahaha crap. Put a +1 next to that comment and let's increment it every
time someone mentions this.
> https://docs.gtk.org/glib/type_func.Array.new.html
>
> GArray*
> g_array_new (
> gboolean zero_terminated,
> gboolean clear_,
> guint element_size
> )
>
> ...
>
> clear_
> Type: gboolean
>
> If true, GArray elements should be automatically cleared to 0 when they are allocated.
>
>>
>> > + current_incoming->channels_early.channels =
>> > + g_array_new(FALSE, TRUE, sizeof(MigEarlyIncomingChannel));
>> > + qemu_mutex_init(¤t_incoming->channels_early.mutex);
>> >
>> > migration_object_check(current_migration, &error_fatal);
>> >
>> > @@ -447,6 +451,24 @@ void migration_incoming_state_destroy(void)
>> > MigrationIncomingState *mis = migration_incoming_get_current();
>> > PostcopyState ps = postcopy_state_get();
>> >
>> > + /* Cleanup listener to make sure no further accept() for sockets */
>> > + migration_incoming_transport_cleanup(mis);
>> > +
>> > + /*
>> > + * It's safer to free channel watches earlier than most of the rest, in
>> > + * case the IO watches could fire in the monitor iothread concurrently
>> > + * against this function.
>>
>> Now I'm getting worried. Cleanup races are super annoying. This is
>> "just" about the recovery command, right?
>
> I think so.
>
> Note: I didn't have any way to trigger any crash at all, this is something
> when I had AI review my code and it pointed out this. I thought I should
> be safe than sorry.
>
It will happen in CI one day when we least expect it, don't worry. =)
It's fine, I don't think it's good use of our time to overthink this at
the moment.
> Below is my fixup. The hope is I addressed most of them but not all, with
> reasonings inline above. Let me know.
>
>
> ===8<===
> From eb03b1b648b91a317a8a7559240186c87d835b8c Mon Sep 17 00:00:00 2001
> From: Peter Xu <peterx@redhat.com>
> Date: Mon, 28 Sep 2026 11:35:20 -0400
> Subject: [PATCH] fixup! migration: Fix incoming channel race with multifd and
> preempt_pre_7_2
>
> Signed-off-by: Peter Xu <peterx@redhat.com>
> ---
> migration/channel.c | 42 +++++++++++++++++-------------------------
> migration/migration.c | 9 +++------
> 2 files changed, 20 insertions(+), 31 deletions(-)
>
> diff --git a/migration/channel.c b/migration/channel.c
> index acb7ef5206..72de9c9778 100644
> --- a/migration/channel.c
> +++ b/migration/channel.c
> @@ -204,17 +204,14 @@ static MigChannelType migration_channel_peek(MigrationIncomingState *mis,
>
> /*
> * Returns: >0 if success, ==0 (CH_NONE) if error. If error happened,
> - * *errp must be set.
> + * *errp must be set. NOTE: preempt channel is done separately.
> */
> static MigChannelType migration_channel_identify(MigrationIncomingState *mis,
> QIOChannel *ioc, Error **errp)
> {
> MigChannelType channel = CH_NONE;
>
> - if (migration_has_main_and_multifd_channels()) {
> - assert(migrate_postcopy_preempt());
> - channel = CH_POSTCOPY;
Fair.
> - } else if (!mis->from_src_file) {
> + if (!mis->from_src_file) {
> channel = CH_MAIN;
> } else if (migrate_multifd()) {
> /*
> @@ -314,31 +311,16 @@ static bool migration_incoming_setup_channel(QIOChannel *ioc,
> object_get_typename(OBJECT(ioc)),
> mig_channel_str[ch]);
> migration_ioc_register_yank(ioc);
> - /* TODO: make this return the success status instead */
> - migration_incoming_setup(ioc, ch, errp);
>
> - return *errp == NULL;
> + return migration_incoming_setup(ioc, ch, errp);
> }
>
> static bool migration_incoming_channel_install(MigrationIncomingState *mis,
> QIOChannel *ioc,
> + MigChannelType ch,
> Error **errp)
> {
> - MigChannelType ch;
> - bool ret;
> -
> - if (qio_channel_is_peekable(ioc)) {
> - ch = migration_channel_peek(mis, ioc, errp);
> - } else {
> - ch = migration_channel_identify(mis, ioc, errp);
> - }
> -
> - if (!ch) {
> - return false;
> - }
> -
> - ret = migration_incoming_setup_channel(ioc, ch, errp);
> - if (!ret) {
> + if (!migration_incoming_setup_channel(ioc, ch, errp)) {
> return false;
> }
>
> @@ -409,6 +391,7 @@ static gboolean migration_incoming_channel_readable(QIOChannel *ioc,
> {
> MigrationIncomingState *mis = opaque;
> Error *local_err = NULL;
> + MigChannelType ch;
>
> /*
> * No need to monitor this channel anymore as long as anything arrived,
> @@ -435,7 +418,12 @@ static gboolean migration_incoming_channel_readable(QIOChannel *ioc,
> goto out;
> }
>
> - if (!migration_incoming_channel_install(mis, ioc, &local_err)) {
> + ch = migration_channel_peek(mis, ioc, &local_err);
> + if (!ch) {
> + goto out;
> + }
> +
> + if (!migration_incoming_channel_install(mis, ioc, ch, &local_err)) {
> goto out;
> }
>
> @@ -522,6 +510,7 @@ void migration_channel_process_incoming(QIOChannel *ioc)
> {
> MigrationIncomingState *mis = migration_incoming_get_current();
> Error *local_err = NULL;
> + MigChannelType ch;
>
> trace_migration_incoming_channel_process(
> ioc, object_get_typename(OBJECT(ioc)));
> @@ -558,7 +547,10 @@ void migration_channel_process_incoming(QIOChannel *ioc)
> if (qio_channel_is_peekable(ioc)) {
> migration_incoming_channel_watch(mis, ioc);
Please carry my "async" comment if possible. It helps when navigating
the code because there's a migration_incoming_channel_install() just
below so we can jump to it without having to read
migration_incoming_channel_watch() definition.
> } else {
> - migration_incoming_channel_install(mis, ioc, &local_err);
> + ch = migration_channel_identify(mis, ioc, &local_err);
> + if (ch) {
> + migration_incoming_channel_install(mis, ioc, ch, &local_err);
> + }
> }
> }
> }
> diff --git a/migration/migration.c b/migration/migration.c
> index adba154e46..242b8ed812 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -854,10 +854,7 @@ out:
> migrate_incoming_unref_outgoing_state();
> }
>
> -/*
> - * Returns whether all the necessary channels to proceed with the
> - * incoming migration have been established without error.
> - */
> +/* Return true if success, false otherwise (with *errp set) */
Probably not needed, this is the expected way.
> bool migration_incoming_setup(QIOChannel *ioc, uint8_t channel, Error **errp)
> {
> MigrationIncomingState *mis = migration_incoming_get_current();
> @@ -886,13 +883,13 @@ bool migration_incoming_setup(QIOChannel *ioc, uint8_t channel, Error **errp)
> assert(!mis->postcopy_qemufile_dst);
> f = qemu_file_new_input(ioc);
> postcopy_preempt_new_channel(mis, f);
> - return false;
> + break;
I see. Maybe a separate preliminary patch for this change?
>
> default:
> g_assert_not_reached();
> }
>
> - return migration_has_main_and_multifd_channels();
> + return true;
> }
>
> void migration_outgoing_setup(QIOChannel *ioc)
> --
> 2.55.0
next prev parent reply other threads:[~2026-09-28 20:39 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-16 21:35 [PATCH 0/7] migration: Fix multifd+postcopy break on qemu-7.2 machine type Peter Xu
2026-09-16 21:35 ` [PATCH 1/7] tests/migration-test: Add multifd+postcopy test for preempt recovery Peter Xu
2026-09-16 21:35 ` [PATCH 2/7] migration: Add two more trace points for incoming channel mgmt Peter Xu
2026-09-16 21:35 ` [PATCH 3/7] migration: Fix socket listen backlog Peter Xu
2026-09-16 21:35 ` [PATCH 4/7] migration: Split migration_channel_process_incoming() Peter Xu
2026-09-16 21:35 ` [PATCH 5/7] migration: Dump type of channel in set_incoming_channel tracepoint Peter Xu
2026-09-16 21:35 ` [PATCH 6/7] migration: Fix incoming channel race with multifd and preempt_pre_7_2 Peter Xu
2026-09-23 15:54 ` Fabiano Rosas
2026-09-23 21:04 ` Peter Xu
2026-09-23 22:15 ` Fabiano Rosas
2026-09-24 21:54 ` Fabiano Rosas
2026-09-28 19:22 ` Peter Xu
2026-09-28 20:38 ` Fabiano Rosas [this message]
2026-09-29 20:00 ` Peter Xu
2026-09-16 21:35 ` [PATCH 7/7] migration: Postpone the outgoing preempt channel for preempt_pre_7_2 Peter Xu
2026-09-22 23:34 ` Fabiano Rosas
2026-09-23 20:24 ` Peter Xu
2026-09-23 22:30 ` Fabiano Rosas
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87cxtx862v.fsf@suse.de \
--to=farosas@suse.de \
--cc=akulhall@redhat.com \
--cc=jmarcin@redhat.com \
--cc=peterx@redhat.com \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.