fix(qemu): release the datapath under the execution lock

QEMU's process monitor calls stop() by itself when the process dies, so an
API stop and a process-death stop run concurrently in the normal case — and
the datapath release sat outside _execute_lock, letting both stops sweep the
adapter TAP map at the same time. Whichever finished first cleared
_kernel_taps under the other's iterator, which died with "dictionary changed
size during iteration" (the monitor's task, so it surfaced as "Task exception
was never retrieved") with the rest of its sweep, the orphan-bridge removal
and uBridge's shutdown skipped.

Keep the previous fix's ordering (process side first, then the devices) and
hold the lock across the release: serialized, the second stop finds uBridge
gone and sweeps nothing. Reproduced deterministically with two concurrent
stop() calls, the first parked mid-sweep — it raised at qemu_vm.py:1659
before the change, and every TAP is swept exactly once after it.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent 0b78d313eb
commit a40503e44b
2 files changed
+79 -7

No files matched your search

+16 -7
View File
@@ -1274,13 +1274,22 @@ class QemuVM(KernelDatapathMixin, BaseNode):
await self._export_config()
await super().stop()
# Release the datapath only once QEMU is gone: the process holds the
# adapter TAPs open (it is the guest side), and uBridge's tap delete
# refuses a held device ("Device or resource busy") — deleting them
# while QEMU still ran leaked the persistent TAPs, because that
# best-effort delete is suppressed. Order: process side first, then
# the devices (the same lesson as IOU's reverse stop order).
await self._stop_ubridge()
# Release the datapath only once QEMU is gone: the process holds
# the adapter TAPs open (it is the guest side), and uBridge's tap
# delete refuses a held device ("Device or resource busy") —
# deleting them while QEMU still ran leaked the persistent TAPs,
# because that best-effort delete is suppressed. Order: process
# side first, then the devices (the same lesson as IOU's reverse
# stop order).
#
# It stays under the lock: the process monitor calls stop() on its
# own when QEMU dies, so an API stop and a process-death stop run
# concurrently in the normal case — and the TAP sweep walks the
# adapter map across awaits, which the second stop was free to
# clear underneath it ("dictionary changed size during
# iteration"). Serialized, it finds uBridge already gone and
# sweeps nothing.
await self._stop_ubridge()
async def _open_qemu_monitor_connection_vm(self, timeout=10):
"""