All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v20 00/15] virtio-net: live-TAP local migration
@ 2026-07-29  9:12 Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 01/15] net/tap: rework tap_parse_script Vladimir Sementsov-Ogievskiy
                   ` (14 more replies)
  0 siblings, 15 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland

Hi all!

Here is a migration for TAP net backend, including its properties and
open fds.

With this new feature, management software doesn't need to initialize
new TAP and do a switch to it. Nothing should be done around
virtio-net in local migration: it just migrates and continues to use
same TAP device. So we avoid extra logic in management software, extra
allocations in kernel (for new TAP), and corresponding extra delay in
migration downtime.

v20:

Main changes:

 - mark the new API unstable for now
 - support CPR -- actually only drop UNIX-socket requirement
   when mode is cpr-exec both in documentation (09) and check (10)

Details:

01: - update commit message
    - rename script_arg -> script_arg_value, to simplify 03
    - keep r-bs by Markus and Ben
02: add r-b by Ben
03: - improve warn_report() wording
    - synchronize QAPI and cmdline documentation
    - keep r-bs by Ben and Markus
06-08: add r-b by Ben
09: - drop "on the same OS" from @local documentation [Markus]
    - mention cpr-exec mode
    - mark @local as unstable (to be consistent
      with x-local-migration-supported, which was discussed to be
      unstable, seems better to keep the complete new API as unstable
      for a while)
    - mention why not simply detect "is UNIX socket" in commit message
    - drop r-b/a-b
10: - skip UNIX-socket requirement for cpr-exec migration mode
    - drop r-b
11: add r-b by Ben
12: - rework the patch after discussion with Ben
    - drop r-b
13: - rename local-migration-supported -> x-local-migration-supported and
      mark it unstable in QAPI (we want "x-" because it's not only QAPI
      field but also QOM property)
    - improve documentation and error reporting
    - keep r-b by Ben
15: - improve cleanup (more guarantee to cleanup /dev/shm)
    - some improvements to make further introduction of CPR-tests simpler
       - move some initial things to setUp()
       - drop default nic
       - rework prepare_and_launch_vm() to be only prepare_vm()
       - add migrate() helper
    - drop r-b

v20 is pushed to

    https://gitlab.com/vsementsov/qemu.git
      tag: up-tap-fd-migration-with-bk-opt-v20

To run the test, use sudo, as test needs to configure TAP device:

    sudo PYTHONPATH=python:tests/functional \
    QEMU_TEST_QEMU_BINARY=$PWD/build/qemu-system-x86_64 \
    MESON_BUILD_ROOT=$PWD/build \
    ./build/pyvenv/bin/python3 tests/functional/x86_64/test_tap_migration.py

Or, to test the feature by hand, you may follow the instruction at
the and of this document (search for "How to run local fd migration by hand"
caption).


Why this series, and why do it this way
=======================================

This came up in the v19 discussion, so it's summarized here to avoid
repeating the same questions.
There was a large discussion at v19, covered many questions. I'm
summarizing them below.

v19: https://lore.kernel.org/qemu-devel/20260714154246.1242856-1-vsementsov@yandex-team.ru/

Why local migration matters
---------------------------

Local migration (migrating to a new QEMU process on the same host) is not
a rare operation for in the cloud.  When upgrading QEMU, _all_ VMs on
_all_ hosts are locally migrated.  The freeze time of such update affects
_all_ customers, and some are sensitive to even small freezes.  Remote
migrations, by contrast, are done mostly to release a physical server for
service/redeploy, which is relatively infrequent.

The aggregate impact is large, and reducing the per-VM downtime is
significant.

Why not just create a new TAP and switch to it
----------------------------------------------

It is possible to create a new TAP device on the target and
migrate the usual way.  But the overhead is in switching to the new TAP
in our cloud virtual networking component.  Could the switching itself
be optimized to always be very fast?  Probably yes, but we chose a
different approach: drop extra components from the process and avoid
any switching at all.  Both the cloud networking component and the
storage backend (with the vhost-user-blk live-update series) don't even
know that QEMU is migrating.  This gives a simpler design (fewer
components involved) and shows good results -- several times reduction
in freeze time.

Also, on hosts with many CPUs, TAP queues allocate a noticeable amount of
RAM, so having 2x queues during migration is an overhead.

Why not use a persistent TAP (open the same TAP on both source and target)
-------------------------------------------------------------------------

First, same memory overhead like with "new TAP" case.

Second, MAX_TAP_QUEUES is 256 in the kernel; if the source already has more
  than 128 queues open, the target cannot open the same device.

[Note also, that such persistent TAP migration will not "just work",
it also needs QEMU changes like patch 12/15 to avoid packet lost]

Why not pass FDs externally (e.g. from libvirt / management tool)
-----------------------------------------------------------------

- The management tool would need to store (or retrieve from source QEMU)
  and pass these FDs, requiring it to know about FDs that QEMU already
  owns.  Libvirt may restart, and that shouldn't break migration.
- Adding an API to retrieve FDs from QEMU just to pass them to target QEMU
  is not a clearer solution.
- QEMU owns the TAP fd and has full knowledge of its state.  Pushing
  this responsibility to an external tool means the tool needs to
  understand QEMU internals -- that's a worse separation of concerns.
- QEMU already passes FDs through migration for VFIO devices (CPR), we
  do continue this practice

Looking at wider picture, where we want to migrate not only TAP, but also
vfio devices (already implemented as CPR migration, but may be updated to
use similar approach as in this series, to use one migration channel),
vhost-user-blk (my another series in flight) and vhost-user-fs (not yet
published), vhost-vsock migration ("[PATCH v3 0/7] migration/cpr: support
vhost-vsock devices" in flight-series from Virtuozzo), it seems to be a
good generic approach: simply pass backends (including open FDs) to the
target, not involving mgmt. Qemu has full knowledge about these FDs and
owns the whole state. Migrating them in QEMU looks correct.

Why not use the cpr-transfer approach (pass FDs before device creation)
-----------------------------------------------------------------------

The CPR approach passes FDs _before_ any devices are created on the
target.  While this avoids touching frontend code (patch 11/15), it
means the target starts using (calling ioctls on) FDs that are still
actively used by the running source QEMU.  That works for TAP, but it
does _not_ work with vhost-user-blk, where sharing the fd between
source and running target can break things.  My series uses the same
approach for both net and storage, and it also works with CPR (see
below).

That's not saying about "local" approach provides simpler user
experience than cpr-transfer:

- no need in secondary migration channel
- no need to work with unavailable QMP for target process until
  "migrate" on source
- more like usual migration

Does this work with CPR?
------------------------

Yes.  For cpr-transfer, TAP migrates through the main migration channel
like other (non-CPR) devices.  For cpr-exec, TAP FDs don't have
FD_CLOEXEC set, so they are inherited by the new process as-is; the
same core mechanism (load_fd / save_fd in migration/vmstate-types.c) is
used.  The 'local' migration parameter documentation now also covers
cpr-exec mode (skip the UNIX-socket requirement in that case).
New test cases exercising both cpr-transfer and cpr-exec with TAP
local migration will be sent as separate series.


How to run local fd migration by hand
=====================================

The walkthrough uses four terminals:

  source-cmd  -- source VM console (serial output, guest login)
  source-qmp  -- QMP connection to the source QEMU
  target-cmd  -- target VM console (serial output, guest login after migration)
  target-qmp  -- QMP connection to the target QEMU

1. Prerequisites
----------------

    QEMU=/path/to/your/build/qemu-system-x86_64

# download same image as in test

    wget -O /tmp/alpine.iso "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/x86_64/alpine-standard-3.22.1-x86_64.iso"


# prepare tap device (be careful to not break your own networks)

    sudo ip tuntap add dev tap0 mode tap multi_queue
    sudo ip addr add 192.168.100.1/24 dev tap0
    sudo ip link set tap0 up


2. Start source VM
------------------

In source-cmd, run:

    $QEMU \
        -name source \
        -machine q35 \
        -accel kvm \
        -m 1G \
        -object memory-backend-file,id=ram0,size=1G,mem-path=/dev/shm/qemu_migration_test,share=on \
        -machine memory-backend=ram0 \
        -drive file=/tmp/alpine.iso,media=cdrom,format=raw \
        -device pcie-pci-bridge,id=pci.1,bus=pcie.0 \
        -serial stdio \
        -nographic \
        -S \
        -qmp unix:/tmp/qmp-source.sock,server=on,wait=off

Note: the netdev and virtio-net device are added via QMP below for symmetry
with the target.  On the source you could also pass them on the command line
(with x-local-migration-supported=on).

In source-qmp, connect and add the TAP netdev and virtio-net device:

    socat - UNIX-CONNECT:/tmp/qmp-source.sock

    {"execute": "qmp_capabilities"}

    {"execute": "netdev_add", "arguments": {
        "id": "netdev.1",
        "type": "tap",
        "ifname": "tap0",
        "queues": 4,
        "vnet_hdr": true,
        "script": "no",
        "downscript": "no",
        "x-local-migration-supported": true
    }}

    {"execute": "device_add", "arguments": {
        "driver": "virtio-net-pci",
        "id": "vnet.1",
        "netdev": "netdev.1",
        "bus": "pci.1",
        "mq": true,
        "vectors": 18,
        "romfile": "",
        "disable-legacy": "off"
    }}

    {"execute": "cont"}

Wait for Alpine to boot in source-cmd.  When you see the login prompt, log
in as root (no password):

    localhost login: root

Configure the guest network:

    ip addr add 192.168.100.2/24 dev eth0
    ip link set eth0 up

Verify connectivity from the guest:

    ping -c 3 192.168.100.1

And from the host (in any spare terminal):

    ping -c 3 192.168.100.2


3. Start target VM
------------------

The TAP netdev must be created via QMP after enabling the "local" migration
parameter -- the target will not open tap0 itself; instead it will receive the
TAP file descriptors from the source over the migration channel.

In target-cmd, run:

    $QEMU \
        -name target \
        -machine q35 \
        -accel kvm \
        -m 1G \
        -object memory-backend-file,id=ram0,size=1G,mem-path=/dev/shm/qemu_migration_test,share=on \
        -machine memory-backend=ram0 \
        -drive file=/tmp/alpine.iso,media=cdrom,format=raw \
        -device pcie-pci-bridge,id=pci.1,bus=pcie.0 \
        -serial stdio \
        -nographic \
        -qmp unix:/tmp/qmp-target.sock,server=on,wait=off \
        -incoming defer

In target-qmp, connect, enable local migration, and add the TAP netdev and
virtio-net device.  The "local" parameter must be set before creating the TAP.
Do not pass ifname/fd -- the fd will arrive via the migration channel:

    socat - UNIX-CONNECT:/tmp/qmp-target.sock

    {"execute": "qmp_capabilities"}

    {"execute": "migrate-set-capabilities", "arguments": {
        "capabilities": [
            {"capability": "events",          "state": true},
            {"capability": "x-ignore-shared", "state": true}
        ]
    }}

    {"execute": "migrate-set-parameters", "arguments": {"local": true}}

    {"execute": "netdev_add", "arguments": {
        "id": "netdev.1",
        "type": "tap",
        "queues": 4,
        "script": "",
        "downscript": "",
        "x-local-migration-supported": true
    }}

    {"execute": "device_add", "arguments": {
        "driver": "virtio-net-pci",
        "id": "vnet.1",
        "netdev": "netdev.1",
        "bus": "pci.1",
        "mq": true,
        "vectors": 18,
        "romfile": "",
        "disable-legacy": "off"
    }}

4. Start migration
------------------

In target-qmp, tell the target to listen for the incoming migration:

    {"execute": "migrate-incoming",
     "arguments": {"uri": "unix:/tmp/migration.sock"}}

In source-qmp, configure migration capabilities and parameters:

    {"execute": "migrate-set-capabilities", "arguments": {
        "capabilities": [
            {"capability": "events",          "state": true},
            {"capability": "x-ignore-shared", "state": true}
        ]
    }}

    {"execute": "migrate-set-parameters", "arguments": {"local": true}}

In source-qmp, trigger the migration:

    {"execute": "migrate",
     "arguments": {"uri": "unix:/tmp/migration.sock"}}

Poll migration status until it completes (source-qmp):

    {"execute": "query-migrate"}
    # repeat until "status" == "completed"

Or just wait for the MIGRATION event that QEMU emits automatically:

    # {"event": "MIGRATION", "data": {"status": "completed"}, ...}

Once the source reports "completed", resume the target VM (target-qmp):

    {"execute": "cont"}

The target VM is now running with the migrated state and the TAP file
descriptors that were passed from the source. Still, target console
(in target-cmd) may still be empty, until you at least press Enter in
it.

Verify that the guest is still reachable from the host:

    ping -c 3 192.168.100.2

And from inside the guest in target-cmd:

    ping -c 3 192.168.100.1


5. Cleanup
----------

Shut down the target VM (target-qmp):

    {"execute": "quit"}

Shut down the source VM (source-qmp):

    {"execute": "quit"}

Remove the TAP device:

    sudo ip tuntap del tap0 mode tap multi_queue

Remove the shared memory file:

    rm /dev/shm/qemu_migration_test

Remove leftover sockets if they still exist:

    rm -f /tmp/migration.sock /tmp/qmp-source.sock /tmp/qmp-target.sock

Vladimir Sementsov-Ogievskiy (15):
  net/tap: rework tap_parse_script
  net/tap: improve script/downscript options documentation
  net/tap: deprecate "no" as special value for script/downscript
  net/tap: move vhost-net open() calls to tap_parse_vhost_fds()
  net/tap: move vhost initialization to tap_setup_vhost()
  net/tap: use container_of instead of DO_UPCAST
  net/tap: QOMify tap backend
  net/tap: add TYPE_VMSTATE_IF interface
  qapi: add local migration parameter
  migration/channel: check that transfer is UNIX socket when "local" set
  virtio-net: support local migration of backend
  net/tap: disable read polling for stopped VM
  net/tap: support local migration with virtio-net
  tests/functional: add skipWithoutSudo() decorator
  tests/functional: add test_tap_migration

 docs/about/deprecated.rst                     |  18 +
 docs/system/i386/microvm.rst                  |   4 +-
 docs/system/i386/xenpvh.rst                   |   2 +-
 docs/system/ppc/ppce500.rst                   |   4 +-
 docs/system/riscv/microchip-icicle-kit.rst    |   2 +-
 docs/system/riscv/sifive_u.rst                |   2 +-
 hw/net/virtio-net.c                           |  89 +++-
 include/hw/virtio/virtio-net.h                |   1 +
 include/migration/misc.h                      |   2 +
 include/migration/vmstate.h                   |   2 +
 include/net/net.h                             |   9 +
 include/net/tap.h                             |   2 +
 migration/channel.c                           |  17 +
 migration/options.c                           |  18 +-
 net/net.c                                     |  14 +-
 net/tap.c                                     | 446 ++++++++++++++---
 qapi/migration.json                           |  22 +-
 qapi/net.json                                 |  45 +-
 qemu-options.hx                               |  13 +-
 tests/functional/qemu_test/decorators.py      |  16 +
 tests/functional/x86_64/meson.build           |   1 +
 tests/functional/x86_64/test_tap_migration.py | 461 ++++++++++++++++++
 22 files changed, 1081 insertions(+), 109 deletions(-)
 create mode 100755 tests/functional/x86_64/test_tap_migration.py

-- 
2.43.0



^ permalink raw reply	[flat|nested] 22+ messages in thread

* [PATCH v20 01/15] net/tap: rework tap_parse_script
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 02/15] net/tap: improve script/downscript options documentation Vladimir Sementsov-Ogievskiy
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

Factor out tap_is_explicit_no_script() helper, to simplify
further changes.

Avoid extra copying by simpler code flow: first check for
NULL / empty / "no" cases, then get default or do copying
respectively.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
---
 net/tap.c | 28 ++++++++++++++++++++++------
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index 57ffb09885c..2076f5b7802 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -92,19 +92,35 @@ static void launch_script(const char *setup_script, const char *ifname,
 static void tap_send(void *opaque);
 static void tap_writable(void *opaque);
 
-static char *tap_parse_script(const char *script_arg, const char *default_path)
+static bool tap_is_explicit_no_script(const char *script_arg_value)
 {
-    g_autofree char *res = g_strdup(script_arg);
+    if (!script_arg_value) {
+        return false;
+    }
+
+    if (script_arg_value[0] == '\0') {
+        return true;
+    }
 
-    if (!res) {
-        res = get_relocated_path(default_path);
+    if (strcmp(script_arg_value, "no") == 0) {
+        return true;
     }
 
-    if (res[0] == '\0' || strcmp(res, "no") == 0) {
+    return false;
+}
+
+static char *tap_parse_script(const char *script_arg_value,
+                              const char *default_path)
+{
+    if (tap_is_explicit_no_script(script_arg_value)) {
         return NULL;
     }
 
-    return g_steal_pointer(&res);
+    if (!script_arg_value) {
+        return get_relocated_path(default_path);
+    }
+
+    return g_strdup(script_arg_value);
 }
 
 static void tap_update_fd_handler(TAPState *s)
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 02/15] net/tap: improve script/downscript options documentation
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 01/15] net/tap: rework tap_parse_script Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 03/15] net/tap: deprecate "no" as special value for script/downscript Vladimir Sementsov-Ogievskiy
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Eric Blake, Jason Wang

Properly document defaults and special values of "" and "no".

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 qapi/net.json   | 12 +++++++++---
 qemu-options.hx |  9 +++++----
 2 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/qapi/net.json b/qapi/net.json
index 1a6382825c5..8f0915c4d86 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -399,9 +399,15 @@
 # @fds: multiple file descriptors of already opened multiqueue capable
 #     tap
 #
-# @script: script to initialize the interface
-#
-# @downscript: script to shut down the interface
+# @script: script to initialize the interface.  An empty string or
+#     "no" disables script execution.  Defaults to
+#     ``<sysconfdir>/qemu-ifup``, where ``<sysconfdir>`` is the
+#     system configuration directory at build time (typically /etc).
+#
+# @downscript: script to shut down the interface.  An empty string or
+#     "no" disables script execution.  Defaults to
+#     ``<sysconfdir>/qemu-ifdown``, where ``<sysconfdir>`` is the
+#     system configuration directory at build time (typically /etc).
 #
 # @br: bridge name (since 2.8)
 #
diff --git a/qemu-options.hx b/qemu-options.hx
index 34970fffc94..200949655ea 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3014,7 +3014,7 @@ DEF("netdev", HAS_ARG, QEMU_OPTION_netdev,
     "                use network scripts 'file' (default=" DEFAULT_NETWORK_SCRIPT ")\n"
     "                to configure it and 'dfile' (default=" DEFAULT_NETWORK_DOWN_SCRIPT ")\n"
     "                to deconfigure it\n"
-    "                use '[down]script=no' to disable script execution\n"
+    "                use '[down]script=no' or '[down]script=' to disable script execution\n"
     "                use network helper 'helper' (default=" DEFAULT_BRIDGE_HELPER ") to\n"
     "                configure it\n"
     "                use 'fd=h' to connect to an already opened TAP interface\n"
@@ -3550,9 +3550,10 @@ SRST
     Use the network script file to configure it and the network script
     dfile to deconfigure it. If name is not provided, the OS
     automatically provides one. The default network configure script is
-    ``/etc/qemu-ifup`` and the default network deconfigure script is
-    ``/etc/qemu-ifdown``. Use ``script=no`` or ``downscript=no`` to
-    disable script execution.
+    ``<sysconfdir>/qemu-ifup`` and the default network deconfigure script is
+    ``<sysconfdir>/qemu-ifdown``, where ``<sysconfdir>`` is the system
+    configuration directory at build time (typically ``/etc``).
+    Use ``[down]script=no`` or ``[down]script=`` to disable script execution.
 
     If running QEMU as an unprivileged user, use the network helper
     to configure the TAP interface and attach it to the bridge.
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 03/15] net/tap: deprecate "no" as special value for script/downscript
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 01/15] net/tap: rework tap_parse_script Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 02/15] net/tap: improve script/downscript options documentation Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 04/15] net/tap: move vhost-net open() calls to tap_parse_vhost_fds() Vladimir Sementsov-Ogievskiy
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Pierrick Bouvier, Sergio Lopez, Zhao Liu, Stefano Stabellini,
	Anthony PERARD, Edgar E. Iglesias, Bernhard Beschow, Conor Dooley,
	Sebastian Huber, Alistair Francis, Palmer Dabbelt, Jason Wang,
	Eric Blake, open list:Incompatible changes,
	open list:X86 Xen CPUs, open list:e500,
	open list:Microchip PolarFi...

The interface is ambiguous, as "no" is valid file name. So,
using "no" as a special value to disable script is deprecated.
Use an empty string ("script=" / "downscript=") instead.

In a future version, "no" will be treated as a plain file name, just
like any other non-empty value.

Document the deprecation in docs/about/deprecated.rst, qapi/net.json,
and qemu-options.hx. Update other docs to use empty string instead of
"no". Add a warning.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
---
 docs/about/deprecated.rst                  | 18 ++++++++++++++++++
 docs/system/i386/microvm.rst               |  4 ++--
 docs/system/i386/xenpvh.rst                |  2 +-
 docs/system/ppc/ppce500.rst                |  4 ++--
 docs/system/riscv/microchip-icicle-kit.rst |  2 +-
 docs/system/riscv/sifive_u.rst             |  2 +-
 net/tap.c                                  | 17 +++++++++++------
 qapi/net.json                              | 14 ++++++++++----
 qemu-options.hx                            |  8 ++++++--
 9 files changed, 52 insertions(+), 19 deletions(-)

diff --git a/docs/about/deprecated.rst b/docs/about/deprecated.rst
index 0c656a968fc..c4929317e3a 100644
--- a/docs/about/deprecated.rst
+++ b/docs/about/deprecated.rst
@@ -71,6 +71,15 @@ flexible enough. The monitor objects have been converted to QOM, so
 ``-mon mode=control`` is replaced by ``-object monitor-qmp``. The
 short convenience options are not deprecated, only ``-mon``.
 
+``script=no`` and ``downscript=no`` for ``-netdev tap`` (since 11.2)
+'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+
+The special value ``"no"`` for the ``script`` and ``downscript``
+parameters of ``-netdev tap`` disables script execution.  This special
+treatment of ``"no"`` is deprecated.  Use an empty string (``script=``
+or ``downscript=``) to disable script execution instead.  In a future
+version, ``"no"`` will be treated as a plain file name.
+
 QEMU Machine Protocol (QMP) commands
 ------------------------------------
 
@@ -164,6 +173,15 @@ Use ``job-finalize`` instead.
 
 Use ``query-accelerators`` instead.
 
+``"no"`` as value of ``script``/``downscript`` for tap in ``netdev_add`` (since 11.2)
+'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+
+The special value ``"no"`` for the ``script`` and ``downscript``
+parameters of ``netdev_add`` with ``type=tap`` disables script
+execution.  This special treatment of ``"no"`` is deprecated.  Use an
+empty string instead.  In a future version, ``"no"`` will be treated as
+a plain file name.
+
 Human Machine Protocol (HMP) commands
 -------------------------------------
 
diff --git a/docs/system/i386/microvm.rst b/docs/system/i386/microvm.rst
index 1675e37d3e7..077ea15751e 100644
--- a/docs/system/i386/microvm.rst
+++ b/docs/system/i386/microvm.rst
@@ -79,7 +79,7 @@ legacy ``ISA serial`` device as console::
      -serial stdio \
      -drive id=test,file=test.img,format=raw,if=none \
      -device virtio-blk-device,drive=test \
-     -netdev tap,id=tap0,script=no,downscript=no \
+     -netdev tap,id=tap0,script=,downscript= \
      -device virtio-net-device,netdev=tap0
 
 While the example above works, you might be interested in reducing the
@@ -103,7 +103,7 @@ disabled::
      -device virtconsole,chardev=virtiocon0 \
      -drive id=test,file=test.img,format=raw,if=none \
      -device virtio-blk-device,drive=test \
-     -netdev tap,id=tap0,script=no,downscript=no \
+     -netdev tap,id=tap0,script=,downscript= \
      -device virtio-net-device,netdev=tap0
 
 
diff --git a/docs/system/i386/xenpvh.rst b/docs/system/i386/xenpvh.rst
index 904778e3f5c..862f38830b1 100644
--- a/docs/system/i386/xenpvh.rst
+++ b/docs/system/i386/xenpvh.rst
@@ -42,7 +42,7 @@ case you need to construct one manually:
       -vnc none                                       \
       -display none                                   \
       -device virtio-net-pci,id=nic0,netdev=net0,mac=00:16:3e:5c:81:78 \
-      -netdev type=tap,id=net0,ifname=vif3.0-emu,br=xenbr0,script=no,downscript=no \
+      -netdev type=tap,id=net0,ifname=vif3.0-emu,br=xenbr0,script=,downscript= \
       -smp 4,maxcpus=4                                \
       -nographic                                      \
       -machine xenpvh,ram-low-base=0,ram-low-size=2147483648,ram-high-base=4294967296,ram-high-size=2147483648,pci-ecam-base=824633720832,pci-ecam-size=268435456,pci-mmio-base=4026531840,pci-mmio-size=33554432,pci-mmio-high-base=824902156288,pci-mmio-high-size=68719476736 \
diff --git a/docs/system/ppc/ppce500.rst b/docs/system/ppc/ppce500.rst
index c9fe0915dc5..ec5aaf14fd9 100644
--- a/docs/system/ppc/ppce500.rst
+++ b/docs/system/ppc/ppce500.rst
@@ -158,14 +158,14 @@ interface at PCI address 0.1.0, but we can switch that to an e1000 NIC by:
   $ qemu-system-ppc64 -M ppce500 -smp 4 -m 2G \
                       -display none -serial stdio \
                       -bios u-boot \
-                      -nic tap,ifname=tap0,script=no,downscript=no,model=e1000
+                      -nic tap,ifname=tap0,script=,downscript=,model=e1000
 
 The QEMU ``ppce500`` machine can also dynamically instantiate an eTSEC device
 if “-device eTSEC” is given to QEMU:
 
 .. code-block:: bash
 
-  -netdev tap,ifname=tap0,script=no,downscript=no,id=net0 -device eTSEC,netdev=net0
+  -netdev tap,ifname=tap0,script=,downscript=,id=net0 -device eTSEC,netdev=net0
 
 Root file system on flash drive
 -------------------------------
diff --git a/docs/system/riscv/microchip-icicle-kit.rst b/docs/system/riscv/microchip-icicle-kit.rst
index 9809e94b84b..7fdb96601ad 100644
--- a/docs/system/riscv/microchip-icicle-kit.rst
+++ b/docs/system/riscv/microchip-icicle-kit.rst
@@ -84,7 +84,7 @@ Then we can boot the machine by:
   $ qemu-system-riscv64 -M microchip-icicle-kit -smp 5 -m 2G \
       -sd path/to/sdcard.img \
       -nic user,model=cadence_gem \
-      -nic tap,ifname=tap,model=cadence_gem,script=no \
+      -nic tap,ifname=tap,model=cadence_gem,script= \
       -display none -serial stdio \
       -kernel path/to/u-boot/build/dir/u-boot.bin \
       -dtb path/to/u-boot/build/dir/u-boot.dtb
diff --git a/docs/system/riscv/sifive_u.rst b/docs/system/riscv/sifive_u.rst
index 8f55ae8e313..0e4dcf3e70c 100644
--- a/docs/system/riscv/sifive_u.rst
+++ b/docs/system/riscv/sifive_u.rst
@@ -199,7 +199,7 @@ To boot the VxWorks kernel in QEMU with the ``sifive_u`` machine, use:
 
   $ qemu-system-riscv64 -M sifive_u -smp 5 -m 2G \
       -display none -serial stdio \
-      -nic tap,ifname=tap0,script=no,downscript=no \
+      -nic tap,ifname=tap0,script=,downscript= \
       -kernel /path/to/vxWorks \
       -append "gem(0,0)host:vxWorks h=192.168.200.1 e=192.168.200.2:ffffff00 u=target pw=vxTarget f=0x01"
 
diff --git a/net/tap.c b/net/tap.c
index 2076f5b7802..f4051e8d4b1 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -92,7 +92,8 @@ static void launch_script(const char *setup_script, const char *ifname,
 static void tap_send(void *opaque);
 static void tap_writable(void *opaque);
 
-static bool tap_is_explicit_no_script(const char *script_arg_value)
+static bool tap_is_explicit_no_script(const char *script_arg_name,
+                                      const char *script_arg_value)
 {
     if (!script_arg_value) {
         return false;
@@ -103,16 +104,19 @@ static bool tap_is_explicit_no_script(const char *script_arg_value)
     }
 
     if (strcmp(script_arg_value, "no") == 0) {
+        warn_report("'%s=no' is deprecated; use '%s=' instead",
+                    script_arg_name, script_arg_name);
         return true;
     }
 
     return false;
 }
 
-static char *tap_parse_script(const char *script_arg_value,
+static char *tap_parse_script(const char *script_arg_name,
+                              const char *script_arg_value,
                               const char *default_path)
 {
-    if (tap_is_explicit_no_script(script_arg_value)) {
+    if (tap_is_explicit_no_script(script_arg_name, script_arg_value)) {
         return NULL;
     }
 
@@ -741,7 +745,7 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
         qemu_set_info_str(&s->nc, "helper=%s", tap->helper);
     } else {
         qemu_set_info_str(&s->nc, "ifname=%s,script=%s,downscript=%s", ifname,
-                          script ?: "no", downscript ?: "no");
+                          script ?: "", downscript ?: "");
 
         if (downscript) {
             snprintf(s->down_script, sizeof(s->down_script), "%s", downscript);
@@ -947,9 +951,10 @@ int net_init_tap(const Netdev *netdev, const char *name,
         }
     } else {
         g_autofree char *script =
-            tap_parse_script(tap->script, DEFAULT_NETWORK_SCRIPT);
+            tap_parse_script("script", tap->script, DEFAULT_NETWORK_SCRIPT);
         g_autofree char *downscript =
-            tap_parse_script(tap->downscript, DEFAULT_NETWORK_DOWN_SCRIPT);
+            tap_parse_script("downscript", tap->downscript,
+                             DEFAULT_NETWORK_DOWN_SCRIPT);
 
         if (tap->ifname) {
             pstrcpy(ifname, sizeof ifname, tap->ifname);
diff --git a/qapi/net.json b/qapi/net.json
index 8f0915c4d86..acb8594c952 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -399,15 +399,21 @@
 # @fds: multiple file descriptors of already opened multiqueue capable
 #     tap
 #
-# @script: script to initialize the interface.  An empty string or
-#     "no" disables script execution.  Defaults to
+# @script: script to initialize the interface.  An empty string
+#     disables script execution.  Defaults to
 #     ``<sysconfdir>/qemu-ifup``, where ``<sysconfdir>`` is the
 #     system configuration directory at build time (typically /etc).
+#     Using "no" to disable script execution is deprecated (since
+#     11.2); use an empty string instead.  In a future version, "no"
+#     will be treated as a plain file name.
 #
-# @downscript: script to shut down the interface.  An empty string or
-#     "no" disables script execution.  Defaults to
+# @downscript: script to shut down the interface.  An empty string
+#     disables script execution.  Defaults to
 #     ``<sysconfdir>/qemu-ifdown``, where ``<sysconfdir>`` is the
 #     system configuration directory at build time (typically /etc).
+#     Using "no" to disable script execution is deprecated (since
+#     11.2); use an empty string instead.  In a future version, "no"
+#     will be treated as a plain file name.
 #
 # @br: bridge name (since 2.8)
 #
diff --git a/qemu-options.hx b/qemu-options.hx
index 200949655ea..1efdb8e9860 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3014,7 +3014,8 @@ DEF("netdev", HAS_ARG, QEMU_OPTION_netdev,
     "                use network scripts 'file' (default=" DEFAULT_NETWORK_SCRIPT ")\n"
     "                to configure it and 'dfile' (default=" DEFAULT_NETWORK_DOWN_SCRIPT ")\n"
     "                to deconfigure it\n"
-    "                use '[down]script=no' or '[down]script=' to disable script execution\n"
+    "                use '[down]script=' to disable script execution\n"
+    "                ('[down]script=no' is deprecated and will be treated as a file name in future)\n"
     "                use network helper 'helper' (default=" DEFAULT_BRIDGE_HELPER ") to\n"
     "                configure it\n"
     "                use 'fd=h' to connect to an already opened TAP interface\n"
@@ -3553,7 +3554,10 @@ SRST
     ``<sysconfdir>/qemu-ifup`` and the default network deconfigure script is
     ``<sysconfdir>/qemu-ifdown``, where ``<sysconfdir>`` is the system
     configuration directory at build time (typically ``/etc``).
-    Use ``[down]script=no`` or ``[down]script=`` to disable script execution.
+    Use ``[down]script=`` to disable script execution.
+    Using ``[down]script=no`` is deprecated; it disables script
+    execution now, but in a future version it will be treated as a
+    plain file name.
 
     If running QEMU as an unprivileged user, use the network helper
     to configure the TAP interface and attach it to the bridge.
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 04/15] net/tap: move vhost-net open() calls to tap_parse_vhost_fds()
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (2 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 03/15] net/tap: deprecate "no" as special value for script/downscript Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 05/15] net/tap: move vhost initialization to tap_setup_vhost() Vladimir Sementsov-Ogievskiy
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

1. Simplify code path: get vhostfds for all cases in one function.

2. Prepare for further tap-fd-migraton feature, when we'll need to
postpone vhost initialization up to post-load stage.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 net/tap.c | 39 ++++++++++++++++++++++-----------------
 1 file changed, 22 insertions(+), 17 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index f4051e8d4b1..bec2cd48fd3 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -756,8 +756,7 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
         }
     }
 
-    if (tap->has_vhost ? tap->vhost :
-        (vhostfd != -1) || (tap->has_vhostforce && tap->vhostforce)) {
+    if (vhostfd != -1) {
         VhostNetOptions options;
 
         options.backend_type = VHOST_BACKEND_TYPE_KERNEL;
@@ -767,17 +766,6 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
         } else {
             options.busyloop_timeout = 0;
         }
-
-        if (vhostfd == -1) {
-            vhostfd = open("/dev/vhost-net", O_RDWR);
-            if (vhostfd < 0) {
-                error_setg_file_open(errp, errno, "/dev/vhost-net");
-                goto failed;
-            }
-            if (!qemu_set_blocking(vhostfd, false, errp)) {
-                goto failed;
-            }
-        }
         options.opaque = (void *)(uintptr_t)vhostfd;
         options.nvqs = 2;
         options.feature_bits = kernel_feature_bits;
@@ -863,14 +851,31 @@ static int tap_parse_fds_and_queues(const NetdevTapOptions *tap, int **fds,
 static bool tap_parse_vhost_fds(const NetdevTapOptions *tap, int **vhost_fds,
                                 int queues, Error **errp)
 {
-    if (!(tap->vhostfd || tap->vhostfds)) {
+    bool need_vhost = tap->has_vhost ? tap->vhost :
+        ((tap->vhostfd || tap->vhostfds) ||
+         (tap->has_vhostforce && tap->vhostforce));
+
+    if (!need_vhost) {
         *vhost_fds = NULL;
         return true;
     }
 
-    if (net_parse_fds(tap->vhostfd ?: tap->vhostfds,
-                      vhost_fds, queues, errp) < 0) {
-        return false;
+    if (tap->vhostfd || tap->vhostfds) {
+        if (net_parse_fds(tap->vhostfd ?: tap->vhostfds,
+                          vhost_fds, queues, errp) < 0) {
+            return false;
+        }
+    } else {
+        *vhost_fds = g_new(int, queues);
+        for (int i = 0; i < queues; i++) {
+            int vhostfd = open("/dev/vhost-net", O_RDWR);
+            if (vhostfd < 0) {
+                error_setg_file_open(errp, errno, "/dev/vhost-net");
+                net_free_fds(*vhost_fds, i);
+                return false;
+            }
+            (*vhost_fds)[i] = vhostfd;
+        }
     }
 
     if (!unblock_fds(*vhost_fds, queues, errp)) {
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 05/15] net/tap: move vhost initialization to tap_setup_vhost()
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (3 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 04/15] net/tap: move vhost-net open() calls to tap_parse_vhost_fds() Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 06/15] net/tap: use container_of instead of DO_UPCAST Vladimir Sementsov-Ogievskiy
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

Make a new helper function in a way it can be reused later for
TAP fd-migration feature: we'll need to initialize vhost in a later
point when we doesn't have access to QAPI parameters.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 net/tap.c | 62 ++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 38 insertions(+), 24 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index bec2cd48fd3..f2b571dd116 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -71,6 +71,8 @@ static const int kernel_feature_bits[] = {
 typedef struct TAPState {
     NetClientState nc;
     int fd;
+    int vhostfd;
+    uint32_t vhost_busyloop_timeout;
     char down_script[1024];
     char down_script_arg[128];
     uint8_t buf[NET_BUFSIZE];
@@ -722,6 +724,38 @@ static int net_tap_init(const NetdevTapOptions *tap, int *vnet_hdr,
     return fd;
 }
 
+static bool tap_setup_vhost(TAPState *s, Error **errp)
+{
+    VhostNetOptions options;
+
+    if (s->vhostfd == -1) {
+        return true;
+    }
+
+    options.backend_type = VHOST_BACKEND_TYPE_KERNEL;
+    options.net_backend = &s->nc;
+    options.busyloop_timeout = s->vhost_busyloop_timeout;
+    options.opaque = (void *)(uintptr_t)s->vhostfd;
+    options.nvqs = 2;
+    options.feature_bits = kernel_feature_bits;
+    options.get_acked_features = NULL;
+    options.save_acked_features = NULL;
+    options.max_tx_queue_size = 0;
+    options.is_vhost_user = false;
+
+    s->vhost_net = vhost_net_init(&options);
+    if (!s->vhost_net) {
+        error_setg(errp,
+                   "vhost-net requested but could not be initialized");
+        return false;
+    }
+
+    /* vhostfd ownership is passed to s->vhost_net */
+    s->vhostfd = -1;
+
+    return true;
+}
+
 static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
                              const char *name,
                              const char *ifname, const char *script,
@@ -756,30 +790,10 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
         }
     }
 
-    if (vhostfd != -1) {
-        VhostNetOptions options;
-
-        options.backend_type = VHOST_BACKEND_TYPE_KERNEL;
-        options.net_backend = &s->nc;
-        if (tap->has_poll_us) {
-            options.busyloop_timeout = tap->poll_us;
-        } else {
-            options.busyloop_timeout = 0;
-        }
-        options.opaque = (void *)(uintptr_t)vhostfd;
-        options.nvqs = 2;
-        options.feature_bits = kernel_feature_bits;
-        options.get_acked_features = NULL;
-        options.save_acked_features = NULL;
-        options.max_tx_queue_size = 0;
-        options.is_vhost_user = false;
-
-        s->vhost_net = vhost_net_init(&options);
-        if (!s->vhost_net) {
-            error_setg(errp,
-                       "vhost-net requested but could not be initialized");
-            goto failed;
-        }
+    s->vhostfd = vhostfd;
+    s->vhost_busyloop_timeout = tap->has_poll_us ? tap->poll_us : 0;
+    if (!tap_setup_vhost(s, errp)) {
+        return false;
     }
 
     return true;
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 06/15] net/tap: use container_of instead of DO_UPCAST
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (4 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 05/15] net/tap: move vhost initialization to tap_setup_vhost() Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 07/15] net/tap: QOMify tap backend Vladimir Sementsov-Ogievskiy
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

We are going to QOMify tap backend, which includes deriving TAPState
from Object. So "NetClientState nc" will not be a first member.
Let's parepare for this change, and use container_of(), which will
work regardless position of "nc" field.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 net/tap.c | 36 ++++++++++++++++++------------------
 1 file changed, 18 insertions(+), 18 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index f2b571dd116..bfed74717e4 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -175,7 +175,7 @@ static ssize_t tap_write_packet(TAPState *s, const struct iovec *iov, int iovcnt
 static ssize_t tap_receive_iov(NetClientState *nc, const struct iovec *iov,
                                int iovcnt)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     const struct iovec *iovp = iov;
     g_autofree struct iovec *iov_copy = NULL;
     struct virtio_net_hdr hdr = { };
@@ -211,7 +211,7 @@ ssize_t tap_read_packet(int tapfd, uint8_t *buf, int maxlen)
 
 static void tap_send_completed(NetClientState *nc, ssize_t len)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     tap_read_poll(s, true);
 }
 
@@ -271,7 +271,7 @@ static void tap_send(void *opaque)
 
 static bool tap_has_ufo(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
 
@@ -280,7 +280,7 @@ static bool tap_has_ufo(NetClientState *nc)
 
 static bool tap_has_uso(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
 
@@ -289,7 +289,7 @@ static bool tap_has_uso(NetClientState *nc)
 
 static bool tap_has_tunnel(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
     return s->has_tunnel;
@@ -297,7 +297,7 @@ static bool tap_has_tunnel(NetClientState *nc)
 
 static bool tap_has_vnet_hdr(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
 
@@ -311,7 +311,7 @@ static bool tap_has_vnet_hdr_len(NetClientState *nc, int len)
 
 static void tap_set_vnet_hdr_len(NetClientState *nc, int len)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
 
@@ -322,21 +322,21 @@ static void tap_set_vnet_hdr_len(NetClientState *nc, int len)
 
 static int tap_set_vnet_le(NetClientState *nc, bool is_le)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     return tap_fd_set_vnet_le(s->fd, is_le);
 }
 
 static int tap_set_vnet_be(NetClientState *nc, bool is_be)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     return tap_fd_set_vnet_be(s->fd, is_be);
 }
 
 static void tap_set_offload(NetClientState *nc, const NetOffloads *ol)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     if (s->fd < 0) {
         return;
     }
@@ -357,7 +357,7 @@ static void tap_exit_notify(Notifier *notifier, void *data)
 
 static void tap_cleanup(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
 
     if (s->vhost_net) {
         vhost_net_cleanup(s->vhost_net);
@@ -381,14 +381,14 @@ static void tap_cleanup(NetClientState *nc)
 
 static void tap_poll(NetClientState *nc, bool enable)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     tap_read_poll(s, enable);
     tap_write_poll(s, enable);
 }
 
 static bool tap_set_steering_ebpf(NetClientState *nc, int prog_fd)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
 
     return tap_fd_set_steering_ebpf(s->fd, prog_fd) == 0;
@@ -396,7 +396,7 @@ static bool tap_set_steering_ebpf(NetClientState *nc, int prog_fd)
 
 int tap_get_fd(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
     return s->fd;
 }
@@ -408,7 +408,7 @@ int tap_get_fd(NetClientState *nc)
  */
 static VHostNetState *tap_get_vhost_net(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
     return s->vhost_net;
 }
@@ -447,7 +447,7 @@ static TAPState *net_tap_fd_init(NetClientState *peer,
 
     nc = qemu_new_net_client(&net_tap_info, peer, model, name);
 
-    s = DO_UPCAST(TAPState, nc, nc);
+    s = container_of(nc, TAPState, nc);
 
     s->fd = fd;
     s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0;
@@ -1016,7 +1016,7 @@ fail:
 
 int tap_enable(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     int ret;
 
     if (s->enabled) {
@@ -1033,7 +1033,7 @@ int tap_enable(NetClientState *nc)
 
 int tap_disable(NetClientState *nc)
 {
-    TAPState *s = DO_UPCAST(TAPState, nc, nc);
+    TAPState *s = container_of(nc, TAPState, nc);
     int ret;
 
     if (s->enabled == 0) {
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 07/15] net/tap: QOMify tap backend
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (5 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 06/15] net/tap: use container_of instead of DO_UPCAST Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 08/15] net/tap: add TYPE_VMSTATE_IF interface Vladimir Sementsov-Ogievskiy
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

We prepare for being able to migrate TAP backend. We'll need a
user change-able property for it, which can be set from machine
type. So, let's QOMify it first.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 include/net/net.h |  7 +++++++
 include/net/tap.h |  2 ++
 net/net.c         | 14 +++++++-------
 net/tap.c         | 48 +++++++++++++++++++++++++++++++++++++++--------
 4 files changed, 56 insertions(+), 15 deletions(-)

diff --git a/include/net/net.h b/include/net/net.h
index 9edfacf827c..05d1db71eb8 100644
--- a/include/net/net.h
+++ b/include/net/net.h
@@ -160,6 +160,13 @@ char *qemu_mac_strdup_printf(const uint8_t *macaddr);
 NetClientState *qemu_find_netdev(const char *id);
 int qemu_find_net_clients_except(const char *id, NetClientState **ncs,
                                  NetClientDriver type, int max);
+void qemu_net_client_setup(NetClientState *nc,
+                           NetClientInfo *info,
+                           NetClientState *peer,
+                           const char *model,
+                           const char *name,
+                           NetClientDestructor *destructor,
+                           bool is_datapath);
 NetClientState *qemu_new_net_client(NetClientInfo *info,
                                     NetClientState *peer,
                                     const char *model,
diff --git a/include/net/tap.h b/include/net/tap.h
index 6f34f13eae4..268570571f4 100644
--- a/include/net/tap.h
+++ b/include/net/tap.h
@@ -28,6 +28,8 @@
 
 #include "standard-headers/linux/virtio_net.h"
 
+#define TYPE_TAP_NETDEV "tap-netdev"
+
 int tap_enable(NetClientState *nc);
 int tap_disable(NetClientState *nc);
 
diff --git a/net/net.c b/net/net.c
index 0a30579ca4a..aba7726a6b3 100644
--- a/net/net.c
+++ b/net/net.c
@@ -261,13 +261,13 @@ static ssize_t qemu_deliver_packet_iov(NetClientState *sender,
                                        int iovcnt,
                                        void *opaque);
 
-static void qemu_net_client_setup(NetClientState *nc,
-                                  NetClientInfo *info,
-                                  NetClientState *peer,
-                                  const char *model,
-                                  const char *name,
-                                  NetClientDestructor *destructor,
-                                  bool is_datapath)
+void qemu_net_client_setup(NetClientState *nc,
+                           NetClientInfo *info,
+                           NetClientState *peer,
+                           const char *model,
+                           const char *name,
+                           NetClientDestructor *destructor,
+                           bool is_datapath)
 {
     nc->info = info;
     nc->model = g_strdup(model);
diff --git a/net/tap.c b/net/tap.c
index bfed74717e4..73923442009 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -43,6 +43,7 @@
 #include "qemu/main-loop.h"
 #include "qemu/sockets.h"
 #include "hw/virtio/vhost.h"
+#include "qom/object.h"
 
 #include "net/tap.h"
 #include "net/util.h"
@@ -68,7 +69,11 @@ static const int kernel_feature_bits[] = {
     VHOST_INVALID_FEATURE_BIT
 };
 
-typedef struct TAPState {
+OBJECT_DECLARE_SIMPLE_TYPE(TAPState, TAP_NETDEV)
+
+struct TAPState {
+    Object parent_obj;
+
     NetClientState nc;
     int fd;
     int vhostfd;
@@ -86,7 +91,7 @@ typedef struct TAPState {
     VHostNetState *vhost_net;
     unsigned host_vnet_hdr_len;
     Notifier exit;
-} TAPState;
+};
 
 static void launch_script(const char *setup_script, const char *ifname,
                           int fd, Error **errp);
@@ -413,6 +418,19 @@ static VHostNetState *tap_get_vhost_net(NetClientState *nc)
     return s->vhost_net;
 }
 
+
+static const TypeInfo tap_netdev_info = {
+    .name = TYPE_TAP_NETDEV,
+    .parent = TYPE_OBJECT,
+    .instance_size = sizeof(TAPState),
+};
+
+static void tap_net_client_destructor(NetClientState *nc)
+{
+    TAPState *s = container_of(nc, TAPState, nc);
+    object_unref(OBJECT(s));
+}
+
 /* fd support */
 
 static NetClientInfo net_tap_info = {
@@ -435,6 +453,18 @@ static NetClientInfo net_tap_info = {
     .get_vhost_net = tap_get_vhost_net,
 };
 
+static TAPState *new_tap(NetClientState *peer,
+                         const char *model,
+                         const char *name)
+{
+    TAPState *s = TAP_NETDEV(object_new(TYPE_TAP_NETDEV));
+
+    qemu_net_client_setup(&s->nc, &net_tap_info, peer, model, name,
+                          tap_net_client_destructor, true);
+
+    return s;
+}
+
 static TAPState *net_tap_fd_init(NetClientState *peer,
                                  const char *model,
                                  const char *name,
@@ -442,12 +472,7 @@ static TAPState *net_tap_fd_init(NetClientState *peer,
                                  int vnet_hdr)
 {
     NetOffloads ol = {};
-    NetClientState *nc;
-    TAPState *s;
-
-    nc = qemu_new_net_client(&net_tap_info, peer, model, name);
-
-    s = container_of(nc, TAPState, nc);
+    TAPState *s = new_tap(peer, model, name);
 
     s->fd = fd;
     s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0;
@@ -1048,3 +1073,10 @@ int tap_disable(NetClientState *nc)
         return ret;
     }
 }
+
+static void tap_register_types(void)
+{
+    type_register_static(&tap_netdev_info);
+}
+
+type_init(tap_register_types)
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 08/15] net/tap: add TYPE_VMSTATE_IF interface
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (6 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 07/15] net/tap: QOMify tap backend Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 09/15] qapi: add local migration parameter Vladimir Sementsov-Ogievskiy
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

We'll need it to implement TAP backend live migration.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 net/tap.c | 43 ++++++++++++++++++++++++++++++++++---------
 1 file changed, 34 insertions(+), 9 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index 73923442009..dd0d134875b 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -43,7 +43,6 @@
 #include "qemu/main-loop.h"
 #include "qemu/sockets.h"
 #include "hw/virtio/vhost.h"
-#include "qom/object.h"
 
 #include "net/tap.h"
 #include "net/util.h"
@@ -91,6 +90,8 @@ struct TAPState {
     VHostNetState *vhost_net;
     unsigned host_vnet_hdr_len;
     Notifier exit;
+
+    int queue_index;
 };
 
 static void launch_script(const char *setup_script, const char *ifname,
@@ -419,10 +420,29 @@ static VHostNetState *tap_get_vhost_net(NetClientState *nc)
 }
 
 
+static char *tap_vmstate_if_get_id(VMStateIf *obj)
+{
+    TAPState *s = TAP_NETDEV(obj);
+    char *res = g_strdup_printf("%s/%d", s->nc.name, s->queue_index);
+    return res;
+}
+
+static void tap_class_init(ObjectClass *klass, const void *data)
+{
+    VMStateIfClass *vc = VMSTATE_IF_CLASS(klass);
+
+    vc->get_id = tap_vmstate_if_get_id;
+}
+
 static const TypeInfo tap_netdev_info = {
     .name = TYPE_TAP_NETDEV,
     .parent = TYPE_OBJECT,
     .instance_size = sizeof(TAPState),
+    .class_init = tap_class_init,
+    .interfaces = (const InterfaceInfo[]) {
+        { TYPE_VMSTATE_IF },
+        { }
+    },
 };
 
 static void tap_net_client_destructor(NetClientState *nc)
@@ -455,13 +475,16 @@ static NetClientInfo net_tap_info = {
 
 static TAPState *new_tap(NetClientState *peer,
                          const char *model,
-                         const char *name)
+                         const char *name,
+                         int queue_index)
 {
     TAPState *s = TAP_NETDEV(object_new(TYPE_TAP_NETDEV));
 
     qemu_net_client_setup(&s->nc, &net_tap_info, peer, model, name,
                           tap_net_client_destructor, true);
 
+    s->queue_index = queue_index;
+
     return s;
 }
 
@@ -469,10 +492,11 @@ static TAPState *net_tap_fd_init(NetClientState *peer,
                                  const char *model,
                                  const char *name,
                                  int fd,
-                                 int vnet_hdr)
+                                 int vnet_hdr,
+                                 int queue_index)
 {
     NetOffloads ol = {};
-    TAPState *s = new_tap(peer, model, name);
+    TAPState *s = new_tap(peer, model, name, queue_index);
 
     s->fd = fd;
     s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0;
@@ -709,7 +733,7 @@ int net_init_bridge(const Netdev *netdev, const char *name,
         close(fd);
         return -1;
     }
-    s = net_tap_fd_init(peer, "bridge", name, fd, vnet_hdr);
+    s = net_tap_fd_init(peer, "bridge", name, fd, vnet_hdr, 0);
 
     qemu_set_info_str(&s->nc, "helper=%s,br=%s", helper, br);
 
@@ -785,10 +809,11 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
                              const char *name,
                              const char *ifname, const char *script,
                              const char *downscript, int vhostfd,
-                             int vnet_hdr, int fd, Error **errp)
+                             int vnet_hdr, int fd, int queue_index,
+                             Error **errp)
 {
     TAPState *s = net_tap_fd_init(peer, tap->helper ? "bridge" : "tap",
-                                  name, fd, vnet_hdr);
+                                  name, fd, vnet_hdr, queue_index);
     bool sndbuf_required = tap->has_sndbuf;
     int sndbuf =
         (tap->has_sndbuf && tap->sndbuf) ? MIN(tap->sndbuf, INT_MAX) : INT_MAX;
@@ -989,7 +1014,7 @@ int net_init_tap(const Netdev *netdev, const char *name,
             if (!net_init_tap_one(tap, peer, name, ifname,
                                   NULL, NULL,
                                   vhost_fds ? vhost_fds[i] : -1,
-                                  vnet_hdr, fds[i], errp)) {
+                                  vnet_hdr, fds[i], i, errp)) {
                 goto fail;
             }
         }
@@ -1024,7 +1049,7 @@ int net_init_tap(const Netdev *netdev, const char *name,
                                   i >= 1 ? NULL : script,
                                   i >= 1 ? NULL : downscript,
                                   vhost_fds ? vhost_fds[i] : -1,
-                                  vnet_hdr, fd, errp)) {
+                                  vnet_hdr, fd, i, errp)) {
                 goto fail;
             }
         }
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 09/15] qapi: add local migration parameter
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (7 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 08/15] net/tap: add TYPE_VMSTATE_IF interface Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29 11:10   ` Markus Armbruster
  2026-07-29 14:05   ` Peter Xu
  2026-07-29  9:12 ` [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set Vladimir Sementsov-Ogievskiy
                   ` (5 subsequent siblings)
  14 siblings, 2 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Eric Blake

We are going to implement local-migration feature: some devices will be
able to transfer open file descriptors through migration stream (which
must UNIX domain socket for that purpose).  This allows to transfer the
whole backend state without reconnecting and restarting the backend
service. For example, virtio-net will migrate its attached TAP netdev,
together with its connected file descriptors.

In this commit we introduce a migration parameter, which enables
the feature for devices that support it (none at the moment).

We can't simply auto-detect local migration by checking whether the
migration channel is a UNIX domain socket: the UNIX socket may be only
the first part of the transfer channel to a remote target.  Hence an
explicit parameter is needed. More over, "local" migration parameter
may be useful in cpr-exec mode, when FDs are passed through exec
instead of UNIX socket.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 include/migration/misc.h |  2 ++
 migration/options.c      | 18 +++++++++++++++++-
 qapi/migration.json      | 22 ++++++++++++++++++----
 3 files changed, 37 insertions(+), 5 deletions(-)

diff --git a/include/migration/misc.h b/include/migration/misc.h
index 2b2fbb59a40..019717f4cda 100644
--- a/include/migration/misc.h
+++ b/include/migration/misc.h
@@ -161,4 +161,6 @@ bool multifd_join_device_state_save_threads(void);
 
 void migration_request_switchover_ack_legacy(const char *requester);
 
+bool migrate_local(void);
+
 #endif
diff --git a/migration/options.c b/migration/options.c
index dfce19405d4..5c439f6d724 100644
--- a/migration/options.c
+++ b/migration/options.c
@@ -14,6 +14,7 @@
 #include "qemu/osdep.h"
 #include "qemu/error-report.h"
 #include "qemu/units.h"
+#include "qapi/util.h"
 #include "exec/target_page.h"
 #include "qapi/clone-visitor.h"
 #include "qapi/error.h"
@@ -25,6 +26,7 @@
 #include "migration/colo.h"
 #include "migration/cpr.h"
 #include "migration/misc.h"
+#include "migration/options.h"
 #include "migration.h"
 #include "migration-stats.h"
 #include "qemu-file.h"
@@ -350,6 +352,12 @@ bool migrate_mapped_ram(void)
     return s->capabilities[MIGRATION_CAPABILITY_MAPPED_RAM];
 }
 
+bool migrate_local(void)
+{
+    MigrationState *s = migrate_get_current();
+    return s->parameters.local;
+}
+
 bool migrate_ignore_shared(void)
 {
     MigrationState *s = migrate_get_current();
@@ -1085,7 +1093,7 @@ static void migrate_mark_all_params_present(MigrationParameters *p)
         &p->has_announce_step, &p->has_block_bitmap_mapping,
         &p->has_x_vcpu_dirty_limit_period, &p->has_vcpu_dirty_limit,
         &p->has_mode, &p->has_zero_page_detection, &p->has_direct_io,
-        &p->has_x_rdma_chunk_size, &p->has_cpr_exec_command,
+        &p->has_x_rdma_chunk_size, &p->has_cpr_exec_command, &p->has_local,
     };
 
     len = ARRAY_SIZE(has_fields);
@@ -1433,6 +1441,10 @@ static void migrate_params_test_apply(MigrationParameters *params,
         qapi_free_strList(dest->cpr_exec_command);
         dest->cpr_exec_command = QAPI_CLONE(strList, params->cpr_exec_command);
     }
+
+    if (params->has_local) {
+        dest->local = params->local;
+    }
 }
 
 static void migrate_params_apply(MigrationParameters *params)
@@ -1565,6 +1577,10 @@ static void migrate_params_apply(MigrationParameters *params)
         s->parameters.cpr_exec_command =
             QAPI_CLONE(strList, params->cpr_exec_command);
     }
+
+    if (params->has_local) {
+        s->parameters.local = params->local;
+    }
 }
 
 void qmp_migrate_set_parameters(MigrationParameters *params, Error **errp)
diff --git a/qapi/migration.json b/qapi/migration.json
index b1eaf7b0545..bb744f134ce 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -830,7 +830,8 @@
            'zero-page-detection',
            'direct-io',
            { 'name': 'x-rdma-chunk-size', 'features': [ 'unstable' ] },
-           'cpr-exec-command'] }
+           'cpr-exec-command',
+           'local'] }
 
 ##
 # @migrate-set-parameters:
@@ -1012,10 +1013,21 @@
 #     Must be set to the same value on both source and destination
 #     before migration starts.  (Since 11.1)
 #
+# @local: Permit the use of optimizations for local migration.
+#     This must only be set when either both the source and
+#     destination QEMU processes are directly connected with a UNIX
+#     domain socket as the migration channel (to enable use of file
+#     descriptor passing) or cpr-exec migration mode is enabled
+#     (this way file descriptors are inherited by new process).
+#     Individual device backends may need additional configuration
+#     flags set  to enable local migration optimizations.
+#     This will be documented against the device
+#     backends where it applies.  (Since 11.2)
+#
 # Features:
 #
-# @unstable: Members @x-checkpoint-delay, @x-rdma-chunk-size, and
-#     @x-vcpu-dirty-limit-period are experimental.
+# @unstable: Members @local, @x-checkpoint-delay, @x-rdma-chunk-size,
+#     and @x-vcpu-dirty-limit-period are experimental.
 #
 # Since: 2.4
 ##
@@ -1053,7 +1065,9 @@
             '*direct-io': 'bool',
             '*x-rdma-chunk-size': { 'type': 'uint64',
                                     'features': [ 'unstable' ] },
-            '*cpr-exec-command': [ 'str' ]} }
+            '*cpr-exec-command': [ 'str' ],
+            '*local': { 'type': 'bool',
+                        'features': [ 'unstable' ] } } }
 
 ##
 # @query-migrate-parameters:
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (8 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 09/15] qapi: add local migration parameter Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29 14:09   ` Peter Xu
  2026-07-29  9:12 ` [PATCH v20 11/15] virtio-net: support local migration of backend Vladimir Sementsov-Ogievskiy
                   ` (4 subsequent siblings)
  14 siblings, 1 reply; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland

As documented, for "local", the migration channel must be direct
UNIX socket connection from source to target. We can't check for
it being "direct", but let's at least check that we deal with
UNIX socket (fd-passing supported).

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 migration/channel.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/migration/channel.c b/migration/channel.c
index 1e2935f926b..537673c29e9 100644
--- a/migration/channel.c
+++ b/migration/channel.c
@@ -214,6 +214,13 @@ void migration_channel_process_incoming(QIOChannel *ioc)
     trace_migration_set_incoming_channel(
         ioc, object_get_typename(OBJECT(ioc)));
 
+    if (migrate_local() && migrate_mode() != MIG_MODE_CPR_EXEC &&
+        !qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_FD_PASS)) {
+        error_setg(&local_err,
+                   "local migration requires a UNIX domain socket channel");
+        goto out;
+    }
+
     if (migrate_channel_requires_tls_upgrade(ioc)) {
         migration_tls_channel_process_incoming(ioc, &local_err);
     } else {
@@ -241,6 +248,16 @@ void migration_channel_connect_outgoing(MigrationState *s, QIOChannel *ioc)
 {
     trace_migration_set_outgoing_channel(ioc, object_get_typename(OBJECT(ioc)));
 
+    if (migrate_local() && migrate_mode() != MIG_MODE_CPR_EXEC &&
+        !qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_FD_PASS)) {
+        Error *local_err = NULL;
+
+        error_setg(&local_err,
+                   "local migration requires a UNIX domain socket channel");
+        migration_connect_error_propagate(s, local_err);
+        return;
+    }
+
     if (migrate_channel_requires_tls_upgrade(ioc)) {
         Error *local_err = NULL;
 
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 11/15] virtio-net: support local migration of backend
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (9 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 12/15] net/tap: disable read polling for stopped VM Vladimir Sementsov-Ogievskiy
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

Next commit will introduce live-migration (with fd-passing) for
TAP net backend. So, now we prepare virtio-net for it
Add virtio-net option local-migration, which is true by default,
but false for older machine types, which doesn't support the feature.

We introduce interface for live-migrating backends:

1. ->is_wait_incoming() handler, so that virtio-net knows, that
   backend is not fully intialized, as it waits for incoming migration
   stream.

2. MIG_PRI_BACKEND priority: backends should migrate with higher
   priority than virtio-net, so that we can do final preparations
   here in post-load handlers and be sure, that backends are already
   prepared.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 hw/net/virtio-net.c            | 89 +++++++++++++++++++++++++++++++++-
 include/hw/virtio/virtio-net.h |  1 +
 include/migration/vmstate.h    |  2 +
 include/net/net.h              |  2 +
 4 files changed, 93 insertions(+), 1 deletion(-)

diff --git a/hw/net/virtio-net.c b/hw/net/virtio-net.c
index 814b99a43d2..274fbcb0d60 100644
--- a/hw/net/virtio-net.c
+++ b/hw/net/virtio-net.c
@@ -40,6 +40,7 @@
 #include "migration/misc.h"
 #include "standard-headers/linux/ethtool.h"
 #include "system/system.h"
+#include "system/runstate.h"
 #include "system/replay.h"
 #include "trace.h"
 #include "monitor/qdev.h"
@@ -3079,7 +3080,17 @@ static void virtio_net_set_multiqueue(VirtIONet *n, int multiqueue)
     n->multiqueue = multiqueue;
     virtio_net_change_num_queues(n, max * 2 + 1);
 
-    virtio_net_set_queue_pairs(n);
+    /*
+     * virtio_net_set_multiqueue() called from set_features(0) on early
+     * reset, when peer may wait for incoming (and is not initialized
+     * yet).
+     * Don't worry about it: virtio_net_set_queue_pairs() will be called
+     * later from virtio_net_post_load_device(), and anyway will be
+     * no-op for local incoming migration with live backend passing.
+     */
+    if (!n->peers_wait_incoming) {
+        virtio_net_set_queue_pairs(n);
+    }
 }
 
 static int virtio_net_pre_load_queues(VirtIODevice *vdev, uint32_t n)
@@ -3108,6 +3119,17 @@ static void virtio_net_get_features(VirtIODevice *vdev, uint64_t *features,
 
     virtio_add_feature_ex(features, VIRTIO_NET_F_MAC);
 
+    if (n->peers_wait_incoming) {
+        /*
+         * Excessive feature set is OK for early initialization when
+         * we wait for local incoming migration: actual guest-negotiated
+         * features will come with migration stream anyway. And we are sure
+         * that we support same host-features as source, because the backend
+         * is the same (the same TAP device, for example).
+         */
+        return;
+    }
+
     if (!peer_has_vnet_hdr(n)) {
         virtio_clear_feature_ex(features, VIRTIO_NET_F_CSUM);
         virtio_clear_feature_ex(features, VIRTIO_NET_F_HOST_TSO4);
@@ -3204,6 +3226,7 @@ static int virtio_net_post_load_device(void *opaque, int version_id)
     VirtIODevice *vdev = VIRTIO_DEVICE(n);
     int i, link_down;
     bool has_tunnel_hdr = virtio_has_tunnel_hdr(vdev->guest_features_ex);
+    Error *local_err = NULL;
 
     trace_virtio_net_post_load_device();
     virtio_net_set_mrg_rx_bufs(n, n->mergeable_rx_bufs,
@@ -3261,6 +3284,20 @@ static int virtio_net_post_load_device(void *opaque, int version_id)
     }
 
     virtio_net_commit_rss_config(n);
+
+    /*
+     * If live-migration is enabled for some backend, than backend
+     * has already been migrated at higher priority (MIG_PRI_BACKEND)
+     * and virtio_net_vnet_post_load() has already called
+     * peer_test_vnet_hdr().  Recompute host_features so that virtio-net
+     * reflects the capabilities of the restored backend.
+     */
+    virtio_net_get_features(vdev, &vdev->host_features, &local_err);
+    if (local_err) {
+        error_report_err(local_err);
+        return -EINVAL;
+    }
+
     return 0;
 }
 
@@ -3411,6 +3448,14 @@ static int virtio_net_vnet_post_load(void *opaque, int version_id)
 {
     struct VirtIONetMigTmp *tmp = opaque;
 
+    /*
+     * If live-migration is enabled for some backend, than backend
+     * has already been migrated at higher priority (MIG_PRI_BACKEND),
+     * so n->has_vnet_hdr can be refreshed from the live backend right
+     * here.
+     */
+    peer_test_vnet_hdr(tmp->parent);
+
     if (tmp->has_vnet_hdr && !peer_has_vnet_hdr(tmp->parent)) {
         error_report("virtio-net: saved image requires vnet_hdr=on");
         return -EINVAL;
@@ -3890,6 +3935,42 @@ static bool failover_hide_primary_device(DeviceListener *listener,
     return qatomic_read(&n->failover_primary_hidden);
 }
 
+static bool virtio_net_check_peers_wait_incoming(VirtIONet *n, bool *waiting,
+                                                 Error **errp)
+{
+    bool has_waiting = false;
+    bool has_not_waiting = false;
+
+    for (int i = 0; i < n->max_queue_pairs; i++) {
+        NetClientState *peer = n->nic->ncs[i].peer;
+        if (!peer) {
+            continue;
+        }
+
+        if (peer->info->is_wait_incoming &&
+            peer->info->is_wait_incoming(peer)) {
+            has_waiting = true;
+        } else {
+            has_not_waiting = true;
+        }
+
+        if (has_waiting && has_not_waiting) {
+            error_setg(errp, "Mixed peer states: some peers wait for incoming "
+                       "migration while others don't");
+            return false;
+        }
+    }
+
+    if (has_waiting && !runstate_check(RUN_STATE_INMIGRATE)) {
+        error_setg(errp, "Peers wait for incoming, but it's not an incoming "
+                   "migration.");
+        return false;
+    }
+
+    *waiting = has_waiting;
+    return true;
+}
+
 static void virtio_net_device_realize(DeviceState *dev, Error **errp)
 {
     VirtIODevice *vdev = VIRTIO_DEVICE(dev);
@@ -4027,6 +4108,12 @@ static void virtio_net_device_realize(DeviceState *dev, Error **errp)
         n->nic->ncs[i].do_not_pad = true;
     }
 
+    if (!virtio_net_check_peers_wait_incoming(n, &n->peers_wait_incoming,
+                                              errp)) {
+        virtio_cleanup(vdev);
+        return;
+    }
+
     peer_test_vnet_hdr(n);
     if (peer_has_vnet_hdr(n)) {
         n->host_hdr_len = sizeof(struct virtio_net_hdr);
diff --git a/include/hw/virtio/virtio-net.h b/include/hw/virtio/virtio-net.h
index 371e3764282..8c967760c2a 100644
--- a/include/hw/virtio/virtio-net.h
+++ b/include/hw/virtio/virtio-net.h
@@ -230,6 +230,7 @@ struct VirtIONet {
     struct EBPFRSSContext ebpf_rss;
     uint32_t nr_ebpf_rss_fds;
     char **ebpf_rss_fds;
+    bool peers_wait_incoming;
 };
 
 size_t virtio_net_handle_ctrl_iov(VirtIODevice *vdev,
diff --git a/include/migration/vmstate.h b/include/migration/vmstate.h
index 1b7f295417e..bbc569adc52 100644
--- a/include/migration/vmstate.h
+++ b/include/migration/vmstate.h
@@ -178,6 +178,8 @@ typedef enum {
 
     MIG_PRI_LOW,                /* Must happen after default */
     MIG_PRI_DEFAULT,
+    MIG_PRI_BACKEND,            /* Must happen before emulated devices, */
+                                /* e.g. virtio-net */
     MIG_PRI_IOMMU,              /* Must happen before PCI devices */
     MIG_PRI_PCI_BUS,            /* Must happen before IOMMU */
     MIG_PRI_VIRTIO_MEM,         /* Must happen before IOMMU */
diff --git a/include/net/net.h b/include/net/net.h
index 05d1db71eb8..a606d06e46a 100644
--- a/include/net/net.h
+++ b/include/net/net.h
@@ -81,6 +81,7 @@ typedef void (SocketReadStateFinalize)(SocketReadState *rs);
 typedef void (NetAnnounce)(NetClientState *);
 typedef bool (SetSteeringEBPF)(NetClientState *, int);
 typedef bool (NetCheckPeerType)(NetClientState *, ObjectClass *, Error **);
+typedef bool (IsWaitIncoming)(NetClientState *);
 typedef struct vhost_net *(GetVHostNet)(NetClientState *nc);
 
 typedef struct NetClientInfo {
@@ -109,6 +110,7 @@ typedef struct NetClientInfo {
     NetAnnounce *announce;
     SetSteeringEBPF *set_steering_ebpf;
     NetCheckPeerType *check_peer_type;
+    IsWaitIncoming *is_wait_incoming;
     GetVHostNet *get_vhost_net;
 } NetClientInfo;
 
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 12/15] net/tap: disable read polling for stopped VM
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (10 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 11/15] virtio-net: support local migration of backend Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 13/15] net/tap: support local migration with virtio-net Vladimir Sementsov-Ogievskiy
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang

Polling when VM is stopped doesn't make real sense, as stopped VM can't
handle incoming traffic anyway.

And it's critical for introduction of local TAP migration
feature in the next commit: the TAP device will be transferred
to the target (open fd will be passed through migration channel),
and if we continue polling on source, we may get a package, which
we'll never handle on source (already stopped), it will be lost.
Better is save this package for target VM to handle.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 net/tap.c | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/net/tap.c b/net/tap.c
index dd0d134875b..51d028d9567 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -36,6 +36,7 @@
 #include "net/net.h"
 #include "clients.h"
 #include "monitor/monitor.h"
+#include "system/runstate.h"
 #include "system/system.h"
 #include "qapi/error.h"
 #include "qemu/cutils.h"
@@ -92,6 +93,8 @@ struct TAPState {
     Notifier exit;
 
     int queue_index;
+    bool enable_poll_on_resume;
+    VMChangeStateEntry *vmstate;
 };
 
 static void launch_script(const char *setup_script, const char *ifname,
@@ -145,10 +148,31 @@ static void tap_update_fd_handler(TAPState *s)
 
 static void tap_read_poll(TAPState *s, bool enable)
 {
+    if (enable && runstate_check(RUN_STATE_FINISH_MIGRATE)) {
+        s->enable_poll_on_resume = true;
+        return;
+    }
     s->read_poll = enable;
     tap_update_fd_handler(s);
 }
 
+static void tap_vm_state_change(void *opaque, bool running, RunState state)
+{
+    TAPState *s = opaque;
+
+    if (running) {
+        if (s->enable_poll_on_resume) {
+            tap_read_poll(s, true);
+            s->enable_poll_on_resume = false;
+        }
+    } else if (state == RUN_STATE_FINISH_MIGRATE) {
+        if (s->read_poll) {
+            s->enable_poll_on_resume = true;
+            tap_read_poll(s, false);
+        }
+    }
+}
+
 static void tap_write_poll(TAPState *s, bool enable)
 {
     s->write_poll = enable;
@@ -379,6 +403,11 @@ static void tap_cleanup(NetClientState *nc)
         s->exit.notify = NULL;
     }
 
+    if (s->vmstate) {
+        qemu_del_vm_change_state_handler(s->vmstate);
+        s->vmstate = NULL;
+    }
+
     tap_read_poll(s, false);
     tap_write_poll(s, false);
     close(s->fd);
@@ -818,6 +847,9 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
     int sndbuf =
         (tap->has_sndbuf && tap->sndbuf) ? MIN(tap->sndbuf, INT_MAX) : INT_MAX;
 
+    s->enable_poll_on_resume = false;
+    s->vmstate = qemu_add_vm_change_state_handler(tap_vm_state_change, s);
+
     if (!tap_set_sndbuf(fd, sndbuf, sndbuf_required ? errp : NULL) &&
         sndbuf_required) {
         goto failed;
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 13/15] net/tap: support local migration with virtio-net
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (11 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 12/15] net/tap: disable read polling for stopped VM Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29 11:30   ` Markus Armbruster
  2026-07-29  9:12 ` [PATCH v20 14/15] tests/functional: add skipWithoutSudo() decorator Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 15/15] tests/functional: add test_tap_migration Vladimir Sementsov-Ogievskiy
  14 siblings, 1 reply; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang, Eric Blake

Support transferring of TAP state (including open fd).

Add new property "x-local-migration-supported", which defines
whether local-migration is actually supported for this TAP device.
Starting from 11.2 QEMU Machine Types it's enabled by default.

Note that local-migration is enabled by global "local" migration
parameter, but individual devices may have additional options to
enable/disable it per device.

The tricky thing is that we need to know whether to call open/connect in
TAP initialization code, i.e. we need to know the value of migration
parameter "local" when creating the TAP device.  For incoming migration,
we can know only for TAP devices created with QMP after setting the
migration parameter with QMP.

So the full picture is:

On source, to start outgoing "local" migration you need:

 - migration parameter "local" set to true
 - "x-local-migration-supported" TAP option set to true (the
   default, starting from 11.2 QEMU Machine Types)

If at least one of these options is not set, TAP backend
doesn't participate in migration.

On target, things are more difficult:

Same, you need both "local" and "x-local-migration-supported"
be set. And same, if one of them is not set, TAP backend
is initialized as usual, and doesn't accept any incoming
state.

Additionally, if you are going to set "local", it must be
set before creating the TAP device. If TAP device created
with "local" unset, it initializes as usual. If you enable
"local" after it and start incoming migration, it will fail
in .pre_load handler of TAP backend.

Moreover, there are interface restrictions: if you create TAP
device when QEMU is in INCOMING state, and both "local"
and "x-local-migration-supported" set, most of TAP options are
not allowed, and script/downscript are required to be explicitly
unset (set to "" or "no").

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 net/tap.c     | 167 ++++++++++++++++++++++++++++++++++++++++++++++++--
 qapi/net.json |  27 +++++++-
 2 files changed, 187 insertions(+), 7 deletions(-)

diff --git a/net/tap.c b/net/tap.c
index 51d028d9567..e00660713f6 100644
--- a/net/tap.c
+++ b/net/tap.c
@@ -38,12 +38,17 @@
 #include "monitor/monitor.h"
 #include "system/runstate.h"
 #include "system/system.h"
+#include "migration/misc.h"
 #include "qapi/error.h"
 #include "qemu/cutils.h"
 #include "qemu/error-report.h"
 #include "qemu/main-loop.h"
 #include "qemu/sockets.h"
 #include "hw/virtio/vhost.h"
+#include "hw/core/vmstate-if.h"
+#include "migration/vmstate.h"
+#include "qom/object.h"
+#include "qom/compat-properties.h"
 
 #include "net/tap.h"
 #include "net/util.h"
@@ -71,6 +76,8 @@ static const int kernel_feature_bits[] = {
 
 OBJECT_DECLARE_SIMPLE_TYPE(TAPState, TAP_NETDEV)
 
+static const VMStateDescription vmstate_tap;
+
 struct TAPState {
     Object parent_obj;
 
@@ -95,6 +102,7 @@ struct TAPState {
     int queue_index;
     bool enable_poll_on_resume;
     VMChangeStateEntry *vmstate;
+    bool local_migration_supported;
 };
 
 static void launch_script(const char *setup_script, const char *ifname,
@@ -412,6 +420,8 @@ static void tap_cleanup(NetClientState *nc)
     tap_write_poll(s, false);
     close(s->fd);
     s->fd = -1;
+
+    vmstate_unregister(VMSTATE_IF(s), &vmstate_tap, s);
 }
 
 static void tap_poll(NetClientState *nc, bool enable)
@@ -448,6 +458,78 @@ static VHostNetState *tap_get_vhost_net(NetClientState *nc)
     return s->vhost_net;
 }
 
+static bool tap_is_wait_incoming(NetClientState *nc)
+{
+    TAPState *s = container_of(nc, TAPState, nc);
+    assert(nc->info->type == NET_CLIENT_DRIVER_TAP);
+    return s->fd == -1;
+}
+
+static bool tap_pre_load(void *opaque, Error **errp)
+{
+    ERRP_GUARD();
+    TAPState *s = opaque;
+
+    if (s->fd != -1) {
+        error_setg(errp,
+                   "TAP is already initialized and cannot receive "
+                   "incoming fd");
+        error_append_hint(errp,
+                          "Migration parameter 'local' must be set"
+                          " before creating the TAP device.");
+        return false;
+    }
+
+    return true;
+}
+
+static bool tap_setup_vhost(TAPState *s, Error **errp);
+
+static bool tap_post_load(void *opaque, int version_id, Error **errp)
+{
+    ERRP_GUARD();
+    TAPState *s = opaque;
+
+    tap_read_poll(s, true);
+
+    if (s->fd < 0) {
+        error_setg(errp, "FD was not loaded during incoming migration");
+        return false;
+    }
+
+    if (!tap_setup_vhost(s, errp)) {
+        error_prepend(errp,
+                      "Failed to setup vhost during TAP post-load: ");
+        return false;
+    }
+
+    return true;
+}
+
+static bool tap_needed(void *opaque)
+{
+    TAPState *s = opaque;
+
+    return s->local_migration_supported && migrate_local();
+}
+
+static const VMStateDescription vmstate_tap = {
+    .name = "net-tap",
+    .priority = MIG_PRI_BACKEND,
+    .pre_load_errp = tap_pre_load,
+    .post_load_errp = tap_post_load,
+    .needed = tap_needed,
+    .fields = (const VMStateField[]) {
+        VMSTATE_FD(fd, TAPState),
+        VMSTATE_BOOL(using_vnet_hdr, TAPState),
+        VMSTATE_BOOL(has_ufo, TAPState),
+        VMSTATE_BOOL(has_uso, TAPState),
+        VMSTATE_BOOL(has_tunnel, TAPState),
+        VMSTATE_BOOL(enabled, TAPState),
+        VMSTATE_UINT32(host_vnet_hdr_len, TAPState),
+        VMSTATE_END_OF_LIST()
+    }
+};
 
 static char *tap_vmstate_if_get_id(VMStateIf *obj)
 {
@@ -456,17 +538,42 @@ static char *tap_vmstate_if_get_id(VMStateIf *obj)
     return res;
 }
 
+static bool tap_get_local_migration_supported_prop(Object *obj, Error **errp)
+{
+    TAPState *s = TAP_NETDEV(obj);
+    return s->local_migration_supported;
+}
+
+static void tap_set_local_migration_supported_prop(Object *obj, bool value,
+                                                   Error **errp)
+{
+    TAPState *s = TAP_NETDEV(obj);
+    s->local_migration_supported = value;
+}
+
+static void tap_instance_init(Object *obj)
+{
+    TAPState *s = TAP_NETDEV(obj);
+    s->local_migration_supported = false;
+}
+
 static void tap_class_init(ObjectClass *klass, const void *data)
 {
     VMStateIfClass *vc = VMSTATE_IF_CLASS(klass);
 
     vc->get_id = tap_vmstate_if_get_id;
+
+    object_class_property_add_bool(klass, "x-local-migration-supported",
+                                   tap_get_local_migration_supported_prop,
+                                   tap_set_local_migration_supported_prop);
 }
 
 static const TypeInfo tap_netdev_info = {
     .name = TYPE_TAP_NETDEV,
     .parent = TYPE_OBJECT,
     .instance_size = sizeof(TAPState),
+    .instance_init = tap_instance_init,
+    .instance_post_init = object_apply_compat_props,
     .class_init = tap_class_init,
     .interfaces = (const InterfaceInfo[]) {
         { TYPE_VMSTATE_IF },
@@ -499,13 +606,16 @@ static NetClientInfo net_tap_info = {
     .set_vnet_le = tap_set_vnet_le,
     .set_vnet_be = tap_set_vnet_be,
     .set_steering_ebpf = tap_set_steering_ebpf,
+    .is_wait_incoming = tap_is_wait_incoming,
     .get_vhost_net = tap_get_vhost_net,
 };
 
 static TAPState *new_tap(NetClientState *peer,
                          const char *model,
                          const char *name,
-                         int queue_index)
+                         int queue_index,
+                         bool has_local_migration_supported,
+                         bool local_migration_supported)
 {
     TAPState *s = TAP_NETDEV(object_new(TYPE_TAP_NETDEV));
 
@@ -514,6 +624,12 @@ static TAPState *new_tap(NetClientState *peer,
 
     s->queue_index = queue_index;
 
+    if (has_local_migration_supported) {
+        s->local_migration_supported = local_migration_supported;
+    }
+
+    vmstate_register(VMSTATE_IF(s), VMSTATE_INSTANCE_ID_ANY, &vmstate_tap, s);
+
     return s;
 }
 
@@ -522,10 +638,14 @@ static TAPState *net_tap_fd_init(NetClientState *peer,
                                  const char *name,
                                  int fd,
                                  int vnet_hdr,
-                                 int queue_index)
+                                 int queue_index,
+                                 bool has_local_migration_supported,
+                                 bool local_migration_supported)
 {
     NetOffloads ol = {};
-    TAPState *s = new_tap(peer, model, name, queue_index);
+    TAPState *s = new_tap(peer, model, name, queue_index,
+                          has_local_migration_supported,
+                          local_migration_supported);
 
     s->fd = fd;
     s->host_vnet_hdr_len = vnet_hdr ? sizeof(struct virtio_net_hdr) : 0;
@@ -762,7 +882,7 @@ int net_init_bridge(const Netdev *netdev, const char *name,
         close(fd);
         return -1;
     }
-    s = net_tap_fd_init(peer, "bridge", name, fd, vnet_hdr, 0);
+    s = net_tap_fd_init(peer, "bridge", name, fd, vnet_hdr, 0, true, false);
 
     qemu_set_info_str(&s->nc, "helper=%s,br=%s", helper, br);
 
@@ -842,7 +962,9 @@ static bool net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
                              Error **errp)
 {
     TAPState *s = net_tap_fd_init(peer, tap->helper ? "bridge" : "tap",
-                                  name, fd, vnet_hdr, queue_index);
+                                  name, fd, vnet_hdr, queue_index,
+                                  tap->has_x_local_migration_supported,
+                                  tap->x_local_migration_supported);
     bool sndbuf_required = tap->has_sndbuf;
     int sndbuf =
         (tap->has_sndbuf && tap->sndbuf) ? MIN(tap->sndbuf, INT_MAX) : INT_MAX;
@@ -990,6 +1112,7 @@ int net_init_tap(const Netdev *netdev, const char *name,
     /* for the no-fd, no-helper case */
     char ifname[128];
     int *fds = NULL, *vhost_fds = NULL;
+    bool incoming_fds;
 
     assert(netdev->type == NET_CLIENT_DRIVER_TAP);
     tap = &netdev->u.tap;
@@ -1012,6 +1135,23 @@ int net_init_tap(const Netdev *netdev, const char *name,
         return -1;
     }
 
+    incoming_fds = tap->x_local_migration_supported && migrate_local() &&
+                   runstate_check(RUN_STATE_INMIGRATE);
+
+    if (incoming_fds &&
+        (tap->fd || tap->fds || tap->helper || tap->br || tap->ifname ||
+         tap->has_sndbuf || tap->has_vnet_hdr ||
+         !tap_is_explicit_no_script("script", tap->script) ||
+         !tap_is_explicit_no_script("downscript", tap->downscript))) {
+        error_setg(errp, "Local incoming migration of TAP device (-incoming, "
+                   "migration parameter @local is set, "
+                   "TAP parameter @x-local-migration-supported is set) "
+                   "is incompatible with "
+                   "fd=, fds=, helper=, br=, ifname=, sndbuf= and vnet_hdr=, "
+                   "and requires explicit empty script= and downscript=");
+        return -1;
+    }
+
     queues = tap_parse_fds_and_queues(tap, &fds, errp);
     if (queues < 0) {
         return -1;
@@ -1030,7 +1170,22 @@ int net_init_tap(const Netdev *netdev, const char *name,
         goto fail;
     }
 
-    if (fds) {
+    if (incoming_fds) {
+        for (i = 0; i < queues; i++) {
+            TAPState *s = new_tap(peer, "tap", name, i,
+                                  tap->has_x_local_migration_supported,
+                                  tap->x_local_migration_supported);
+            qemu_set_info_str(&s->nc, "incoming");
+
+            s->fd = -1;
+            if (vhost_fds) {
+                s->vhostfd = vhost_fds[i];
+                s->vhost_busyloop_timeout = tap->has_poll_us ? tap->poll_us : 0;
+            } else {
+                s->vhostfd = -1;
+            }
+        }
+    } else if (fds) {
         for (i = 0; i < queues; i++) {
             if (i == 0) {
                 vnet_hdr = tap_probe_vnet_hdr(fds[i], errp);
diff --git a/qapi/net.json b/qapi/net.json
index acb8594c952..e6921912e12 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -437,6 +437,29 @@
 # @poll-us: maximum number of microseconds that could be spent on busy
 #     polling for tap (since 2.7)
 #
+# @x-local-migration-supported: enable local migration for this TAP
+#     backend.  When set, local migration is enabled/disabled by
+#     migration parameter @local for this TAP backend.  When unset,
+#     migration parameter @local is ignored for this TAP backend.
+#     To be able to do incoming local migration of a TAP backend,
+#     migration parameter @local must be set _before_ creating the
+#     TAP backend.  Otherwise, TAP backend is initialized as usual,
+#     opening/creating TAP devices in kernel.  In this case further
+#     local incoming migration (with migration parameter @local set
+#     after creating TAP backend with @x-local-migration-supporeted
+#     parameter set) will simply fail.
+#     Moreover, when QEMU is in incoming migration state, migration
+#     parameter @local is set and @x-local-migration-supported is set,
+#     the following options are not supported and must not be set:
+#     @fd, @fds, @helper, @br, @ifname, @sndbuf, @vnet_hdr.
+#     Additionally in this case @script and @downscipt must be
+#     explicitly disabled (empty strings or "no").
+#     (default: false, since 11.2)
+#
+# Features:
+#
+# @unstable: Member @x-local-migration-supported is experimental.
+#
 # Since: 1.2
 ##
 { 'struct': 'NetdevTapOptions',
@@ -455,7 +478,9 @@
     '*vhostfds':   'str',
     '*vhostforce': 'bool',
     '*queues':     'uint32',
-    '*poll-us':    'uint32'} }
+    '*poll-us':    'uint32',
+    '*x-local-migration-supported': {
+      'type': 'bool', 'features' : [ 'unstable'] } } }
 
 ##
 # @NetdevSocketOptions:
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 14/15] tests/functional: add skipWithoutSudo() decorator
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (12 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 13/15] net/tap: support local migration with virtio-net Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  2026-07-29  9:12 ` [PATCH v20 15/15] tests/functional: add test_tap_migration Vladimir Sementsov-Ogievskiy
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Thomas Huth, Lei Yang, Maksim Davydov, Thomas Huth,
	Philippe Mathieu-Daudé

To be used in the next commit: that would be a test for TAP
networking, and it will need to setup TAP device.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
Reviewed-by: Thomas Huth <thuth@redhat.com>
Tested-by: Lei Yang <leiyang@redhat.com>
Reviewed-by: Maksim Davydov <davydov-max@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 tests/functional/qemu_test/decorators.py | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/tests/functional/qemu_test/decorators.py b/tests/functional/qemu_test/decorators.py
index fcf236ecfdf..aa135acc785 100644
--- a/tests/functional/qemu_test/decorators.py
+++ b/tests/functional/qemu_test/decorators.py
@@ -6,6 +6,7 @@
 import os
 import platform
 import resource
+import subprocess
 from unittest import skipIf, skipUnless
 
 from .cmd import which
@@ -177,3 +178,18 @@ def skipLockedMemoryTest(locked_memory):
         ulimit_memory == resource.RLIM_INFINITY or ulimit_memory >= locked_memory * 1024,
         f'Test required {locked_memory} kB of available locked memory',
     )
+
+'''
+Decorator to skip execution of a test if passwordless
+sudo command is not available.
+'''
+def skipWithoutSudo():
+    proc = subprocess.run(["sudo", "-n", "/bin/true"],
+                          stdin=subprocess.PIPE,
+                          stdout=subprocess.PIPE,
+                          stderr=subprocess.STDOUT,
+                          universal_newlines=True,
+                          check=False)
+
+    return skipUnless(proc.returncode == 0,
+                      f'requires password-less sudo access: {proc.stdout}')
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v20 15/15] tests/functional: add test_tap_migration
  2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
                   ` (13 preceding siblings ...)
  2026-07-29  9:12 ` [PATCH v20 14/15] tests/functional: add skipWithoutSudo() decorator Vladimir Sementsov-Ogievskiy
@ 2026-07-29  9:12 ` Vladimir Sementsov-Ogievskiy
  14 siblings, 0 replies; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29  9:12 UTC (permalink / raw)
  To: jasowang, mst
  Cc: armbru, peterx, farosas, raphael.s.norwitz, bchaney, vsementsov,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Zhao Liu

Add test for a new local-migration migration of virtio-net/tap, with fd
passing through UNIX socket.

Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Ben Chaney <bchaney@akamai.com>
---
 tests/functional/x86_64/meson.build           |   1 +
 tests/functional/x86_64/test_tap_migration.py | 461 ++++++++++++++++++
 2 files changed, 462 insertions(+)
 create mode 100755 tests/functional/x86_64/test_tap_migration.py

diff --git a/tests/functional/x86_64/meson.build b/tests/functional/x86_64/meson.build
index 27b31f2e966..eaf2ec0005a 100644
--- a/tests/functional/x86_64/meson.build
+++ b/tests/functional/x86_64/meson.build
@@ -47,4 +47,5 @@ tests_x86_64_system_thorough = [
   'virtio_balloon',
   'virtio_gpu',
   'rebuild_vmfd',
+  'tap_migration',
 ]
diff --git a/tests/functional/x86_64/test_tap_migration.py b/tests/functional/x86_64/test_tap_migration.py
new file mode 100755
index 00000000000..0506b01926e
--- /dev/null
+++ b/tests/functional/x86_64/test_tap_migration.py
@@ -0,0 +1,461 @@
+#!/usr/bin/env python3
+#
+# Functional test that tests TAP local migration
+# with fd passing
+#
+# Copyright (c) Yandex Technologies LLC, 2026
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import time
+import subprocess
+from subprocess import run
+import signal
+import ctypes
+import ctypes.util
+import unittest
+from contextlib import contextmanager, ExitStack
+from typing import Tuple
+
+from qemu_test import (
+    LinuxKernelTest,
+    Asset,
+    exec_command_and_wait_for_pattern,
+)
+from qemu_test.decorators import skipWithoutSudo
+
+
+GUEST_IP = "192.168.100.2"
+GUEST_IP_MASK = f"{GUEST_IP}/24"
+GUEST_MAC = "d6:0d:75:f8:0f:b7"
+HOST_IP = "192.168.100.1"
+HOST_IP_MASK = f"{HOST_IP}/24"
+TAP_ID = "tap0"
+TAP_ID2 = "tap1"
+TAP_MAC = "e6:1d:44:b5:03:5d"
+NETNS = f"qemu_test_ns_{os.getpid()}"
+
+
+def ip(args, check=True) -> None:
+    """Run ip command with sudo"""
+    run(["sudo", "ip"] + args, check=check)
+
+
+@contextmanager
+def switch_netns(netns_name):
+    libc = ctypes.CDLL(ctypes.util.find_library("c"))
+    netns_path = f"/var/run/netns/{netns_name}"
+
+    def switch_to_fd(fd, check: bool = False):
+        """Switch to netns by file descriptor"""
+        SYS_setns = 308
+        CLONE_NEWNET = 0x40000000
+        ret = libc.syscall(SYS_setns, fd, CLONE_NEWNET)
+        if check and ret != 0:
+            raise RuntimeError("syscall SETNS failed")
+
+    with ExitStack() as stack:
+        original_netns_fd = os.open("/proc/self/ns/net", os.O_RDONLY)
+        stack.callback(os.close, original_netns_fd)
+
+        ip(["netns", "add", netns_name])
+        stack.callback(ip, ["netns", "del", netns_name], check=False)
+
+        new_netns_fd = os.open(netns_path, os.O_RDONLY)
+        stack.callback(os.close, new_netns_fd)
+
+        switch_to_fd(new_netns_fd)
+        stack.callback(switch_to_fd, original_netns_fd, check=False)
+
+        yield
+
+
+def del_tap(tap_name: str = TAP_ID) -> None:
+    ip(["tuntap", "del", tap_name, "mode", "tap", "multi_queue"], check=False)
+
+
+def init_tap(tap_name: str = TAP_ID, with_ip: bool = True) -> None:
+    ip(["tuntap", "add", "dev", tap_name, "mode", "tap", "multi_queue"])
+    if with_ip:
+        ip(["link", "set", "dev", tap_name, "address", TAP_MAC])
+        ip(["addr", "add", HOST_IP_MASK, "dev", tap_name])
+    ip(["link", "set", tap_name, "up"])
+
+
+def switch_network_to_tap2() -> None:
+    ip(["link", "set", TAP_ID2, "down"])
+    ip(["link", "set", TAP_ID, "down"])
+    ip(["addr", "delete", HOST_IP_MASK, "dev", TAP_ID])
+    ip(["link", "set", "dev", TAP_ID2, "address", TAP_MAC])
+    ip(["addr", "add", HOST_IP_MASK, "dev", TAP_ID2])
+    ip(["link", "set", TAP_ID2, "up"])
+
+
+def parse_ping_line(line: str) -> float:
+    # suspect lines like
+    # [1748524876.590509] 64 bytes from 94.245.155.3 \
+    #      (94.245.155.3): icmp_seq=1 ttl=250 time=101 ms
+    spl = line.split()
+    return float(spl[0][1:-1])
+
+
+def parse_ping_output(out) -> Tuple[bool, float, float]:
+    lines = [x for x in out.split("\n") if x.startswith("[")]
+
+    try:
+        first_no_ans = next(
+            (ind for ind in range(len(lines)) if lines[ind][20:26] == "no ans")
+        )
+    except StopIteration:
+        return False, parse_ping_line(lines[0]), parse_ping_line(lines[-1])
+
+    last_no_ans = next(
+        ind
+        for ind in range(len(lines) - 1, -1, -1)
+        if lines[ind][20:26] == "no ans"
+    )
+
+    return (
+        True,
+        parse_ping_line(lines[first_no_ans]),
+        parse_ping_line(lines[last_no_ans]),
+    )
+
+
+def wait_migration_finish(source_vm, target_vm):
+    migr_events = (
+        ("MIGRATION", {"data": {"status": "completed"}}),
+        ("MIGRATION", {"data": {"status": "failed"}}),
+    )
+
+    source_e = source_vm.events_wait(migr_events)["data"]
+    target_e = target_vm.events_wait(migr_events)["data"]
+
+    source_s = source_vm.cmd("query-status")["status"]
+    target_s = target_vm.cmd("query-status")["status"]
+
+    assert (
+        source_e["status"] == "completed"
+        and target_e["status"] == "completed"
+        and source_s == "postmigrate"
+        and target_s == "paused"
+    ), f"""Migration failed:
+    SRC status: {source_s}
+    SRC event: {source_e}
+    TGT status: {target_s}
+    TGT event:{target_e}"""
+
+
+@skipWithoutSudo()
+class TAPFdMigration(LinuxKernelTest):
+
+    ASSET_KERNEL = Asset(
+        (
+            "https://archives.fedoraproject.org/pub/archive/fedora/linux/releases"
+            "/31/Server/x86_64/os/images/pxeboot/vmlinuz"
+        ),
+        "d4738d03dbbe083ca610d0821d0a8f1488bebbdccef54ce33e3adb35fda00129",
+    )
+
+    ASSET_INITRD = Asset(
+        (
+            "https://archives.fedoraproject.org/pub/archive/fedora/linux/releases"
+            "/31/Server/x86_64/os/images/pxeboot/initrd.img"
+        ),
+        "277cd6c7adf77c7e63d73bbb2cded8ef9e2d3a2f100000e92ff1f8396513cd8b",
+    )
+
+    ASSET_ALPINE_ISO = Asset(
+        (
+            "https://dl-cdn.alpinelinux.org/"
+            "alpine/v3.22/releases/x86_64/alpine-standard-3.22.1-x86_64.iso"
+        ),
+        "96d1b44ea1b8a5a884f193526d92edb4676054e9fa903ad2f016441a0fe13089",
+    )
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+
+        try:
+            cls.netns_context = switch_netns(NETNS)
+            cls.netns_context.__enter__()
+        except (OSError, subprocess.CalledProcessError) as e:
+            raise unittest.SkipTest(f"can't switch network namespace: {e}")
+
+    @classmethod
+    def tearDownClass(cls):
+        if hasattr(cls, "netns_context"):
+            cls.netns_context.__exit__(None, None, None)
+        super().tearDownClass()
+
+    def setUp(self):
+        super().setUp()
+
+        self.require_accelerator("kvm")
+        self.set_machine("q35")
+
+        self.setup_shared_memory()
+
+        init_tap()
+
+        self.outer_ping_proc = None
+        self.shm_path = None
+
+    def tearDown(self):
+        with ExitStack() as stack:
+            stack.callback(super().tearDown)
+
+            if self.shm_path:
+                stack.callback(lambda p: (os.unlink(p) if os.path.exists(p)
+                                         else None),
+                               self.shm_path)
+                self.shm_path = None
+
+            stack.callback(del_tap, TAP_ID2)
+            stack.callback(del_tap, TAP_ID)
+
+            if self.outer_ping_proc:
+                self.stop_outer_ping()
+
+    def start_outer_ping(self) -> None:
+        assert self.outer_ping_proc is None
+        self.outer_ping_log = self.scratch_file("ping.log")
+        with open(self.outer_ping_log, "w") as f:
+            self.outer_ping_proc = subprocess.Popen(
+                ["ping", "-i", "0", "-O", "-D", GUEST_IP],
+                text=True,
+                stdout=f,
+            )
+
+    def stop_outer_ping(self) -> str:
+        assert self.outer_ping_proc
+        self.outer_ping_proc.send_signal(signal.SIGINT)
+
+        self.outer_ping_proc.communicate(timeout=5)
+        self.outer_ping_proc = None
+
+        with open(self.outer_ping_log) as f:
+            return f.read()
+
+    def stop_ping_and_check(self, stop_time, resume_time):
+        ping_res = self.stop_outer_ping()
+
+        discon, a, b = parse_ping_output(ping_res)
+
+        if not discon:
+            text = (
+                f"STOP: {stop_time}, RESUME: {resume_time}," f"PING: {a} - {b}"
+            )
+            if a > stop_time or b < resume_time:
+                self.fail(f"PING failed: {text}")
+            self.log.info(f"PING: no packets lost: {text}")
+            return
+
+        text = (
+            f"STOP: {stop_time}, RESUME: {resume_time},"
+            f"PING: disconnect: {a} - {b}"
+        )
+        self.log.info(text)
+        eps = 0.05
+        if a < stop_time - eps or b > resume_time + eps:
+            self.fail(text)
+
+    def one_ping_from_guest(self, vm) -> None:
+        exec_command_and_wait_for_pattern(
+            self,
+            f"ping -c 1 -W 1 {HOST_IP}",
+            "1 packets transmitted, 1 packets received",
+            "1 packets transmitted, 0 packets received",
+            vm=vm,
+        )
+        self.wait_for_console_pattern("# ", vm=vm)
+
+    def one_ping_from_host(self) -> None:
+        run(
+            ["ping", "-c", "1", "-W", "1", GUEST_IP],
+            stdout=subprocess.DEVNULL,
+            check=True,
+        )
+
+    def setup_shared_memory(self):
+        self.shm_path = f"/dev/shm/qemu_test_{os.getpid()}"
+
+        try:
+            with open(self.shm_path, "wb") as f:
+                f.write(b"\0" * (1024 * 1024 * 1024))  # 1GB
+        except Exception as e:
+            self.fail(f"Failed to create shared memory file: {e}")
+
+    def prepare_vm(
+        self, shm_path, vhost, incoming=False, vm=None, local=True
+    ):
+        if not vm:
+            vm = self.vm
+
+        vm.set_console()
+        vm.add_args("-accel", "kvm")
+        vm.add_args("-device", "pcie-pci-bridge,id=pci.1,bus=pcie.0")
+        vm.add_args("-m", "1G")
+        vm.add_args("-net", "none")
+
+        vm.add_args(
+            "-object",
+            f"memory-backend-file,id=ram0,size=1G,mem-path={shm_path},share=on",
+        )
+        vm.add_args("-machine", "memory-backend=ram0")
+
+        vm.add_args(
+            "-drive",
+            f"file={self.ASSET_ALPINE_ISO.fetch()},media=cdrom,format=raw",
+        )
+
+        vm.add_args("-S")
+
+        if incoming:
+            vm.add_args("-incoming", "defer")
+
+    def add_virtio_net(
+        self, vm, vhost: bool, tap_name: str, local: bool, incoming: bool
+    ):
+        netdev_params = {
+            "id": "netdev.1",
+            "vhost": vhost,
+            "type": "tap",
+            "queues": 4,
+            "script": "no",
+            "downscript": "no",
+            "x-local-migration-supported": local,
+        }
+
+        if not (local and incoming):
+            netdev_params["vnet_hdr"] = True
+            netdev_params["ifname"] = tap_name
+
+        vm.cmd("netdev_add", netdev_params)
+
+        vm.cmd(
+            "device_add",
+            driver="virtio-net-pci",
+            romfile="",
+            id="vnet.1",
+            netdev="netdev.1",
+            mq=True,
+            vectors=18,
+            bus="pci.1",
+            mac=GUEST_MAC,
+            disable_legacy="off",
+        )
+
+    def set_migration_capabilities(self, vm, local):
+        vm.cmd(
+            "migrate-set-capabilities",
+            {
+                "capabilities": [
+                    {"capability": "events", "state": True},
+                    {"capability": "x-ignore-shared", "state": True},
+                ]
+            },
+        )
+        vm.cmd("migrate-set-parameters", {"local": local})
+
+    def setup_guest_network(self) -> None:
+        exec_command_and_wait_for_pattern(self, "ip addr", "# ")
+        exec_command_and_wait_for_pattern(
+            self,
+            f"ip addr add {GUEST_IP_MASK} dev eth0 && "
+            "ip link set eth0 up && echo OK",
+            "OK",
+        )
+        self.wait_for_console_pattern("# ")
+
+    def migrate(self, vm, mig_sock):
+        vm.cmd("migrate", uri=f"unix:{mig_sock}")
+    def do_test_tap_fd_migration(self, vhost, local=True):
+        socket_dir = self.socket_dir()
+        mig_sock = os.path.join(socket_dir.name, "mig.sock")
+
+        # Setup second TAP if needed
+        if not local:
+            del_tap(TAP_ID2)
+            init_tap(TAP_ID2, with_ip=False)
+
+        self.prepare_vm(self.shm_path, vhost, local=local)
+        self.vm.launch()
+        self.set_migration_capabilities(self.vm, local=local)
+        self.add_virtio_net(self.vm, vhost, TAP_ID, local, incoming=False)
+
+        self.vm.cmd("cont")
+        self.wait_for_console_pattern("login:")
+        exec_command_and_wait_for_pattern(self, "root", "# ")
+
+        self.setup_guest_network()
+
+        self.one_ping_from_guest(self.vm)
+        self.one_ping_from_host()
+        self.start_outer_ping()
+
+        # Get some successful pings before migration
+        time.sleep(0.5)
+
+        target_vm = self.get_vm(name="target")
+        self.prepare_vm(
+            self.shm_path,
+            vhost,
+            incoming=True,
+            vm=target_vm,
+            local=local,
+        )
+
+        target_vm.launch()
+        if not local:
+            tap_name = TAP_ID2
+        else:
+            tap_name = TAP_ID
+        self.set_migration_capabilities(target_vm, local=local)
+        self.add_virtio_net(target_vm, vhost, tap_name, local, incoming=True)
+
+        target_vm.cmd("migrate-incoming", {"uri": f"unix:{mig_sock}"})
+
+        self.log.info("Starting migration")
+        freeze_start = time.time()
+        self.migrate(self.vm, mig_sock)
+
+        self.log.info("Waiting for migration completion")
+        wait_migration_finish(self.vm, target_vm)
+
+        # Switch network to tap1 if not using local-migration
+        if not local:
+            switch_network_to_tap2()
+
+        target_vm.cmd("cont")
+        freeze_end = time.time()
+
+        self.vm.shutdown()
+
+        self.log.info("Verifying PING on target VM after migration")
+        self.one_ping_from_guest(target_vm)
+        self.one_ping_from_host()
+
+        # And a bit more pings after source shutdown
+        time.sleep(0.3)
+        self.stop_ping_and_check(freeze_start, freeze_end)
+
+        target_vm.shutdown()
+
+    def test_tap_fd_migration(self):
+        self.do_test_tap_fd_migration(False)
+
+    def test_tap_fd_migration_vhost(self):
+        self.do_test_tap_fd_migration(True)
+
+    def test_tap_new_tap_migration(self):
+        self.do_test_tap_fd_migration(False, local=False)
+
+    def test_tap_new_tap_migration_vhost(self):
+        self.do_test_tap_fd_migration(True, local=False)
+
+
+if __name__ == "__main__":
+    LinuxKernelTest.main()
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 09/15] qapi: add local migration parameter
  2026-07-29  9:12 ` [PATCH v20 09/15] qapi: add local migration parameter Vladimir Sementsov-Ogievskiy
@ 2026-07-29 11:10   ` Markus Armbruster
  2026-07-29 14:05   ` Peter Xu
  1 sibling, 0 replies; 22+ messages in thread
From: Markus Armbruster @ 2026-07-29 11:10 UTC (permalink / raw)
  To: Vladimir Sementsov-Ogievskiy
  Cc: jasowang, mst, peterx, farosas, raphael.s.norwitz, bchaney,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Eric Blake

Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:

> We are going to implement local-migration feature: some devices will be
> able to transfer open file descriptors through migration stream (which
> must UNIX domain socket for that purpose).  This allows to transfer the
> whole backend state without reconnecting and restarting the backend
> service. For example, virtio-net will migrate its attached TAP netdev,
> together with its connected file descriptors.
>
> In this commit we introduce a migration parameter, which enables
> the feature for devices that support it (none at the moment).
>
> We can't simply auto-detect local migration by checking whether the
> migration channel is a UNIX domain socket: the UNIX socket may be only
> the first part of the transfer channel to a remote target.  Hence an
> explicit parameter is needed. More over, "local" migration parameter

Moreover

> may be useful in cpr-exec mode, when FDs are passed through exec
> instead of UNIX socket.
>
> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> ---
>  include/migration/misc.h |  2 ++
>  migration/options.c      | 18 +++++++++++++++++-
>  qapi/migration.json      | 22 ++++++++++++++++++----
>  3 files changed, 37 insertions(+), 5 deletions(-)
>
> diff --git a/include/migration/misc.h b/include/migration/misc.h
> index 2b2fbb59a40..019717f4cda 100644
> --- a/include/migration/misc.h
> +++ b/include/migration/misc.h
> @@ -161,4 +161,6 @@ bool multifd_join_device_state_save_threads(void);
>  
>  void migration_request_switchover_ack_legacy(const char *requester);
>  
> +bool migrate_local(void);
> +
>  #endif
> diff --git a/migration/options.c b/migration/options.c
> index dfce19405d4..5c439f6d724 100644
> --- a/migration/options.c
> +++ b/migration/options.c
> @@ -14,6 +14,7 @@
>  #include "qemu/osdep.h"
>  #include "qemu/error-report.h"
>  #include "qemu/units.h"
> +#include "qapi/util.h"
>  #include "exec/target_page.h"
>  #include "qapi/clone-visitor.h"
>  #include "qapi/error.h"
> @@ -25,6 +26,7 @@
>  #include "migration/colo.h"
>  #include "migration/cpr.h"
>  #include "migration/misc.h"
> +#include "migration/options.h"
>  #include "migration.h"
>  #include "migration-stats.h"
>  #include "qemu-file.h"
> @@ -350,6 +352,12 @@ bool migrate_mapped_ram(void)
>      return s->capabilities[MIGRATION_CAPABILITY_MAPPED_RAM];
>  }
>  
> +bool migrate_local(void)
> +{
> +    MigrationState *s = migrate_get_current();
> +    return s->parameters.local;
> +}
> +
>  bool migrate_ignore_shared(void)
>  {
>      MigrationState *s = migrate_get_current();
> @@ -1085,7 +1093,7 @@ static void migrate_mark_all_params_present(MigrationParameters *p)
>          &p->has_announce_step, &p->has_block_bitmap_mapping,
>          &p->has_x_vcpu_dirty_limit_period, &p->has_vcpu_dirty_limit,
>          &p->has_mode, &p->has_zero_page_detection, &p->has_direct_io,
> -        &p->has_x_rdma_chunk_size, &p->has_cpr_exec_command,
> +        &p->has_x_rdma_chunk_size, &p->has_cpr_exec_command, &p->has_local,
>      };
>  
>      len = ARRAY_SIZE(has_fields);
> @@ -1433,6 +1441,10 @@ static void migrate_params_test_apply(MigrationParameters *params,
>          qapi_free_strList(dest->cpr_exec_command);
>          dest->cpr_exec_command = QAPI_CLONE(strList, params->cpr_exec_command);
>      }
> +
> +    if (params->has_local) {
> +        dest->local = params->local;
> +    }
>  }
>  
>  static void migrate_params_apply(MigrationParameters *params)
> @@ -1565,6 +1577,10 @@ static void migrate_params_apply(MigrationParameters *params)
>          s->parameters.cpr_exec_command =
>              QAPI_CLONE(strList, params->cpr_exec_command);
>      }
> +
> +    if (params->has_local) {
> +        s->parameters.local = params->local;
> +    }
>  }
>  
>  void qmp_migrate_set_parameters(MigrationParameters *params, Error **errp)
> diff --git a/qapi/migration.json b/qapi/migration.json
> index b1eaf7b0545..bb744f134ce 100644
> --- a/qapi/migration.json
> +++ b/qapi/migration.json
> @@ -830,7 +830,8 @@
>             'zero-page-detection',
>             'direct-io',
>             { 'name': 'x-rdma-chunk-size', 'features': [ 'unstable' ] },
> -           'cpr-exec-command'] }
> +           'cpr-exec-command',
> +           'local'] }
>  
>  ##
>  # @migrate-set-parameters:
> @@ -1012,10 +1013,21 @@
>  #     Must be set to the same value on both source and destination
>  #     before migration starts.  (Since 11.1)
>  #
> +# @local: Permit the use of optimizations for local migration.
> +#     This must only be set when either both the source and
> +#     destination QEMU processes are directly connected with a UNIX
> +#     domain socket as the migration channel (to enable use of file
> +#     descriptor passing) or cpr-exec migration mode is enabled
> +#     (this way file descriptors are inherited by new process).
> +#     Individual device backends may need additional configuration
> +#     flags set  to enable local migration optimizations.

Extra space between "set" and "to".

> +#     This will be documented against the device
> +#     backends where it applies.  (Since 11.2)

Reflow while there:

   # @local: Permit the use of optimizations for local migration.  This
   #     must only be set when either both the source and destination
   #     QEMU processes are directly connected with a UNIX domain socket
   #     as the migration channel (to enable use of file descriptor
   #     passing) or cpr-exec migration mode is enabled (this way file
   #     descriptors are inherited by new process).  Individual device
   #     backends may need additional configuration flags set to enable
   #     local migration optimizations.  This will be documented against
   #     the device backends where it applies.  (Since 11.2)

> +#
>  # Features:
>  #
> -# @unstable: Members @x-checkpoint-delay, @x-rdma-chunk-size, and
> -#     @x-vcpu-dirty-limit-period are experimental.
> +# @unstable: Members @local, @x-checkpoint-delay, @x-rdma-chunk-size,
> +#     and @x-vcpu-dirty-limit-period are experimental.
>  #
>  # Since: 2.4
>  ##
> @@ -1053,7 +1065,9 @@
>              '*direct-io': 'bool',
>              '*x-rdma-chunk-size': { 'type': 'uint64',
>                                      'features': [ 'unstable' ] },
> -            '*cpr-exec-command': [ 'str' ]} }
> +            '*cpr-exec-command': [ 'str' ],
> +            '*local': { 'type': 'bool',
> +                        'features': [ 'unstable' ] } } }
>  
>  ##
>  # @query-migrate-parameters:

With these cosmetic tweaks
Acked-by: Markus Armbruster <armbru@redhat.com>



^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 13/15] net/tap: support local migration with virtio-net
  2026-07-29  9:12 ` [PATCH v20 13/15] net/tap: support local migration with virtio-net Vladimir Sementsov-Ogievskiy
@ 2026-07-29 11:30   ` Markus Armbruster
  2026-07-29 12:25     ` Vladimir Sementsov-Ogievskiy
  0 siblings, 1 reply; 22+ messages in thread
From: Markus Armbruster @ 2026-07-29 11:30 UTC (permalink / raw)
  To: Vladimir Sementsov-Ogievskiy
  Cc: jasowang, mst, armbru, peterx, farosas, raphael.s.norwitz,
	bchaney, qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang, Eric Blake

Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:

> Support transferring of TAP state (including open fd).
>
> Add new property "x-local-migration-supported", which defines
> whether local-migration is actually supported for this TAP device.
> Starting from 11.2 QEMU Machine Types it's enabled by default.
>
> Note that local-migration is enabled by global "local" migration
> parameter, but individual devices may have additional options to
> enable/disable it per device.
>
> The tricky thing is that we need to know whether to call open/connect in
> TAP initialization code, i.e. we need to know the value of migration
> parameter "local" when creating the TAP device.  For incoming migration,
> we can know only for TAP devices created with QMP after setting the
> migration parameter with QMP.
>
> So the full picture is:
>
> On source, to start outgoing "local" migration you need:
>
>  - migration parameter "local" set to true
>  - "x-local-migration-supported" TAP option set to true (the
>    default, starting from 11.2 QEMU Machine Types)

Is the machine type part still accurate?  The description in the QAPI
schema has (default: false, since 11.2).

>
> If at least one of these options is not set, TAP backend
> doesn't participate in migration.
>
> On target, things are more difficult:
>
> Same, you need both "local" and "x-local-migration-supported"
> be set. And same, if one of them is not set, TAP backend
> is initialized as usual, and doesn't accept any incoming
> state.
>
> Additionally, if you are going to set "local", it must be
> set before creating the TAP device. If TAP device created
> with "local" unset, it initializes as usual. If you enable
> "local" after it and start incoming migration, it will fail
> in .pre_load handler of TAP backend.
>
> Moreover, there are interface restrictions: if you create TAP
> device when QEMU is in INCOMING state, and both "local"
> and "x-local-migration-supported" set, most of TAP options are
> not allowed, and script/downscript are required to be explicitly
> unset (set to "" or "no").
>
> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> Reviewed-by: Ben Chaney <bchaney@akamai.com>

[...]

> diff --git a/qapi/net.json b/qapi/net.json
> index acb8594c952..e6921912e12 100644
> --- a/qapi/net.json
> +++ b/qapi/net.json
> @@ -437,6 +437,29 @@
>  # @poll-us: maximum number of microseconds that could be spent on busy
>  #     polling for tap (since 2.7)
>  #
> +# @x-local-migration-supported: enable local migration for this TAP
> +#     backend.  When set, local migration is enabled/disabled by
> +#     migration parameter @local for this TAP backend.  When unset,
> +#     migration parameter @local is ignored for this TAP backend.
> +#     To be able to do incoming local migration of a TAP backend,
> +#     migration parameter @local must be set _before_ creating the
> +#     TAP backend.  Otherwise, TAP backend is initialized as usual,
> +#     opening/creating TAP devices in kernel.  In this case further
> +#     local incoming migration (with migration parameter @local set
> +#     after creating TAP backend with @x-local-migration-supporeted
> +#     parameter set) will simply fail.

Either have a a blank line here so you actually get two paragraphs, or
refill the entire description to avoid the illusion of two paragraphs.

> +#     Moreover, when QEMU is in incoming migration state, migration
> +#     parameter @local is set and @x-local-migration-supported is set,
> +#     the following options are not supported and must not be set:
> +#     @fd, @fds, @helper, @br, @ifname, @sndbuf, @vnet_hdr.
> +#     Additionally in this case @script and @downscipt must be

@downscript

> +#     explicitly disabled (empty strings or "no").

"no" is deprecated [PATCH 3].  We'll have to remember deleting 'or "no"'
here when remove it.  Easy to forget.  Delete it now?

Maybe

   #     Additionally, @script and @downscript must be explicitly disabled
   #     then.

> +#     (default: false, since 11.2)
> +#
> +# Features:
> +#
> +# @unstable: Member @x-local-migration-supported is experimental.
> +#
>  # Since: 1.2
>  ##
>  { 'struct': 'NetdevTapOptions',
> @@ -455,7 +478,9 @@
>      '*vhostfds':   'str',
>      '*vhostforce': 'bool',
>      '*queues':     'uint32',
> -    '*poll-us':    'uint32'} }
> +    '*poll-us':    'uint32',
> +    '*x-local-migration-supported': {
> +      'type': 'bool', 'features' : [ 'unstable'] } } }
>  
>  ##
>  # @NetdevSocketOptions:

Naming is hard...

"Supported" sounds like a property of the QEMU process.  That's not what
this is.  It's an on/off switch that happens to be in series with
another on/off switch, namely migration parameter @local.

Maybe

    @permit-local-migration: permit local migration for this TAP
        backend.  When set, local migration is enabled/disabled by
        migration parameter @local for this TAP backend.  ...



^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 13/15] net/tap: support local migration with virtio-net
  2026-07-29 11:30   ` Markus Armbruster
@ 2026-07-29 12:25     ` Vladimir Sementsov-Ogievskiy
  2026-07-29 12:45       ` Markus Armbruster
  0 siblings, 1 reply; 22+ messages in thread
From: Vladimir Sementsov-Ogievskiy @ 2026-07-29 12:25 UTC (permalink / raw)
  To: Markus Armbruster
  Cc: jasowang, mst, peterx, farosas, raphael.s.norwitz, bchaney,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang, Eric Blake

On 29.07.26 14:30, Markus Armbruster wrote:
> Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:
> 
>> Support transferring of TAP state (including open fd).
>>
>> Add new property "x-local-migration-supported", which defines
>> whether local-migration is actually supported for this TAP device.
>> Starting from 11.2 QEMU Machine Types it's enabled by default.
>>
>> Note that local-migration is enabled by global "local" migration
>> parameter, but individual devices may have additional options to
>> enable/disable it per device.
>>
>> The tricky thing is that we need to know whether to call open/connect in
>> TAP initialization code, i.e. we need to know the value of migration
>> parameter "local" when creating the TAP device.  For incoming migration,
>> we can know only for TAP devices created with QMP after setting the
>> migration parameter with QMP.
>>
>> So the full picture is:
>>
>> On source, to start outgoing "local" migration you need:
>>
>>   - migration parameter "local" set to true
>>   - "x-local-migration-supported" TAP option set to true (the
>>     default, starting from 11.2 QEMU Machine Types)
> 
> Is the machine type part still accurate?  The description in the QAPI
> schema has (default: false, since 11.2).

Oops, need to update commit message.

> 
>>
>> If at least one of these options is not set, TAP backend
>> doesn't participate in migration.
>>
>> On target, things are more difficult:
>>
>> Same, you need both "local" and "x-local-migration-supported"
>> be set. And same, if one of them is not set, TAP backend
>> is initialized as usual, and doesn't accept any incoming
>> state.
>>
>> Additionally, if you are going to set "local", it must be
>> set before creating the TAP device. If TAP device created
>> with "local" unset, it initializes as usual. If you enable
>> "local" after it and start incoming migration, it will fail
>> in .pre_load handler of TAP backend.
>>
>> Moreover, there are interface restrictions: if you create TAP
>> device when QEMU is in INCOMING state, and both "local"
>> and "x-local-migration-supported" set, most of TAP options are
>> not allowed, and script/downscript are required to be explicitly
>> unset (set to "" or "no").
>>
>> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
>> Reviewed-by: Ben Chaney <bchaney@akamai.com>
> 
> [...]
> 
>> diff --git a/qapi/net.json b/qapi/net.json
>> index acb8594c952..e6921912e12 100644
>> --- a/qapi/net.json
>> +++ b/qapi/net.json
>> @@ -437,6 +437,29 @@
>>   # @poll-us: maximum number of microseconds that could be spent on busy
>>   #     polling for tap (since 2.7)
>>   #
>> +# @x-local-migration-supported: enable local migration for this TAP
>> +#     backend.  When set, local migration is enabled/disabled by
>> +#     migration parameter @local for this TAP backend.  When unset,
>> +#     migration parameter @local is ignored for this TAP backend.
>> +#     To be able to do incoming local migration of a TAP backend,
>> +#     migration parameter @local must be set _before_ creating the
>> +#     TAP backend.  Otherwise, TAP backend is initialized as usual,
>> +#     opening/creating TAP devices in kernel.  In this case further
>> +#     local incoming migration (with migration parameter @local set
>> +#     after creating TAP backend with @x-local-migration-supporeted
>> +#     parameter set) will simply fail.
> 
> Either have a a blank line here so you actually get two paragraphs, or
> refill the entire description to avoid the illusion of two paragraphs.
> 
>> +#     Moreover, when QEMU is in incoming migration state, migration
>> +#     parameter @local is set and @x-local-migration-supported is set,
>> +#     the following options are not supported and must not be set:
>> +#     @fd, @fds, @helper, @br, @ifname, @sndbuf, @vnet_hdr.
>> +#     Additionally in this case @script and @downscipt must be
> 
> @downscript
> 
>> +#     explicitly disabled (empty strings or "no").
> 
> "no" is deprecated [PATCH 3].  We'll have to remember deleting 'or "no"'
> here when remove it.  Easy to forget.  Delete it now?
> 
> Maybe
> 
>     #     Additionally, @script and @downscript must be explicitly disabled
>     #     then.


Agree.

> 
>> +#     (default: false, since 11.2)
>> +#
>> +# Features:
>> +#
>> +# @unstable: Member @x-local-migration-supported is experimental.
>> +#
>>   # Since: 1.2
>>   ##
>>   { 'struct': 'NetdevTapOptions',
>> @@ -455,7 +478,9 @@
>>       '*vhostfds':   'str',
>>       '*vhostforce': 'bool',
>>       '*queues':     'uint32',
>> -    '*poll-us':    'uint32'} }
>> +    '*poll-us':    'uint32',
>> +    '*x-local-migration-supported': {
>> +      'type': 'bool', 'features' : [ 'unstable'] } } }
>>   
>>   ##
>>   # @NetdevSocketOptions:
> 
> Naming is hard...
> 
> "Supported" sounds like a property of the QEMU process.  That's not what
> this is.  It's an on/off switch that happens to be in series with
> another on/off switch, namely migration parameter @local.
> 
> Maybe
> 
>      @permit-local-migration: permit local migration for this TAP
>          backend.  When set, local migration is enabled/disabled by
>          migration parameter @local for this TAP backend.  ...
> 

Good for me. Will use, if no more opinions.

Or x-permit-local-migration? I've added "x-" prefix as Michael suggested.
And it seems reasonable, as it's not only QAPI field, but also QOM property,
and we don't have "unstable" features for properties.

-- 
Best regards,
Vladimir


^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 13/15] net/tap: support local migration with virtio-net
  2026-07-29 12:25     ` Vladimir Sementsov-Ogievskiy
@ 2026-07-29 12:45       ` Markus Armbruster
  0 siblings, 0 replies; 22+ messages in thread
From: Markus Armbruster @ 2026-07-29 12:45 UTC (permalink / raw)
  To: Vladimir Sementsov-Ogievskiy
  Cc: jasowang, mst, peterx, farosas, raphael.s.norwitz, bchaney,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Jason Wang, Eric Blake

Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> writes:

[...]

> Or x-permit-local-migration? I've added "x-" prefix as Michael suggested.
> And it seems reasonable, as it's not only QAPI field, but also QOM property,
> and we don't have "unstable" features for properties.

I'm fine with "x-".



^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 09/15] qapi: add local migration parameter
  2026-07-29  9:12 ` [PATCH v20 09/15] qapi: add local migration parameter Vladimir Sementsov-Ogievskiy
  2026-07-29 11:10   ` Markus Armbruster
@ 2026-07-29 14:05   ` Peter Xu
  1 sibling, 0 replies; 22+ messages in thread
From: Peter Xu @ 2026-07-29 14:05 UTC (permalink / raw)
  To: Vladimir Sementsov-Ogievskiy
  Cc: jasowang, mst, armbru, farosas, raphael.s.norwitz, bchaney,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland,
	Eric Blake

On Wed, Jul 29, 2026 at 12:12:47PM +0300, Vladimir Sementsov-Ogievskiy wrote:
> We are going to implement local-migration feature: some devices will be
> able to transfer open file descriptors through migration stream (which
> must UNIX domain socket for that purpose).  This allows to transfer the
> whole backend state without reconnecting and restarting the backend
> service. For example, virtio-net will migrate its attached TAP netdev,
> together with its connected file descriptors.
> 
> In this commit we introduce a migration parameter, which enables
> the feature for devices that support it (none at the moment).
> 
> We can't simply auto-detect local migration by checking whether the
> migration channel is a UNIX domain socket: the UNIX socket may be only
> the first part of the transfer channel to a remote target.  Hence an
> explicit parameter is needed. More over, "local" migration parameter
> may be useful in cpr-exec mode, when FDs are passed through exec
> instead of UNIX socket.
> 
> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>

Reviewed-by: Peter Xu <peterx@redhat.com>

-- 
Peter Xu



^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set
  2026-07-29  9:12 ` [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set Vladimir Sementsov-Ogievskiy
@ 2026-07-29 14:09   ` Peter Xu
  0 siblings, 0 replies; 22+ messages in thread
From: Peter Xu @ 2026-07-29 14:09 UTC (permalink / raw)
  To: Vladimir Sementsov-Ogievskiy
  Cc: jasowang, mst, armbru, farosas, raphael.s.norwitz, bchaney,
	qemu-devel, berrange, pbonzini, yc-core, mark.caveayland

On Wed, Jul 29, 2026 at 12:12:48PM +0300, Vladimir Sementsov-Ogievskiy wrote:
> As documented, for "local", the migration channel must be direct
> UNIX socket connection from source to target. We can't check for
> it being "direct", but let's at least check that we deal with
> UNIX socket (fd-passing supported).
> 
> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> ---
>  migration/channel.c | 17 +++++++++++++++++
>  1 file changed, 17 insertions(+)
> 
> diff --git a/migration/channel.c b/migration/channel.c
> index 1e2935f926b..537673c29e9 100644
> --- a/migration/channel.c
> +++ b/migration/channel.c
> @@ -214,6 +214,13 @@ void migration_channel_process_incoming(QIOChannel *ioc)
>      trace_migration_set_incoming_channel(
>          ioc, object_get_typename(OBJECT(ioc)));
>  
> +    if (migrate_local() && migrate_mode() != MIG_MODE_CPR_EXEC &&
> +        !qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_FD_PASS)) {
> +        error_setg(&local_err,
> +                   "local migration requires a UNIX domain socket channel");
> +        goto out;
> +    }
> +
>      if (migrate_channel_requires_tls_upgrade(ioc)) {
>          migration_tls_channel_process_incoming(ioc, &local_err);
>      } else {
> @@ -241,6 +248,16 @@ void migration_channel_connect_outgoing(MigrationState *s, QIOChannel *ioc)
>  {
>      trace_migration_set_outgoing_channel(ioc, object_get_typename(OBJECT(ioc)));
>  
> +    if (migrate_local() && migrate_mode() != MIG_MODE_CPR_EXEC &&
> +        !qio_channel_has_feature(ioc, QIO_CHANNEL_FEATURE_FD_PASS)) {
> +        Error *local_err = NULL;
> +
> +        error_setg(&local_err,
> +                   "local migration requires a UNIX domain socket channel");
> +        migration_connect_error_propagate(s, local_err);
> +        return;
> +    }
> +

We only need to touch migration_capabilities_and_transport_compatible()?
It should work for both, and fail QMP commands directly might also be
slightly better.

>      if (migrate_channel_requires_tls_upgrade(ioc)) {
>          Error *local_err = NULL;
>  
> -- 
> 2.43.0
> 

-- 
Peter Xu



^ permalink raw reply	[flat|nested] 22+ messages in thread

end of thread, other threads:[~2026-07-29 14:10 UTC | newest]

Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-29  9:12 [PATCH v20 00/15] virtio-net: live-TAP local migration Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 01/15] net/tap: rework tap_parse_script Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 02/15] net/tap: improve script/downscript options documentation Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 03/15] net/tap: deprecate "no" as special value for script/downscript Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 04/15] net/tap: move vhost-net open() calls to tap_parse_vhost_fds() Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 05/15] net/tap: move vhost initialization to tap_setup_vhost() Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 06/15] net/tap: use container_of instead of DO_UPCAST Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 07/15] net/tap: QOMify tap backend Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 08/15] net/tap: add TYPE_VMSTATE_IF interface Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 09/15] qapi: add local migration parameter Vladimir Sementsov-Ogievskiy
2026-07-29 11:10   ` Markus Armbruster
2026-07-29 14:05   ` Peter Xu
2026-07-29  9:12 ` [PATCH v20 10/15] migration/channel: check that transfer is UNIX socket when "local" set Vladimir Sementsov-Ogievskiy
2026-07-29 14:09   ` Peter Xu
2026-07-29  9:12 ` [PATCH v20 11/15] virtio-net: support local migration of backend Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 12/15] net/tap: disable read polling for stopped VM Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 13/15] net/tap: support local migration with virtio-net Vladimir Sementsov-Ogievskiy
2026-07-29 11:30   ` Markus Armbruster
2026-07-29 12:25     ` Vladimir Sementsov-Ogievskiy
2026-07-29 12:45       ` Markus Armbruster
2026-07-29  9:12 ` [PATCH v20 14/15] tests/functional: add skipWithoutSudo() decorator Vladimir Sementsov-Ogievskiy
2026-07-29  9:12 ` [PATCH v20 15/15] tests/functional: add test_tap_migration Vladimir Sementsov-Ogievskiy

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.