From 7ab6119b1f965ce264899245f909ed2474d2328d Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Wed, 24 Jun 2026 22:01:12 +0300 Subject: [PATCH] fix(input): re-add the input-linux bridge object idempotently A bare object-add fails with "duplicate property ''" when a prior daemon instance left the object live: its best-effort object-del on teardown has no round-trip and can race a fast restart, so device B (the relative mouse) stays orphaned and motion is not forwarded. Fire object-del for the same id before each object-add. QMP is sequential per connection, so the del is applied before the add; on a clean first attach it no-ops (DeviceNotFound, silently dropped). Re-attach is now idempotent. --- src/adapter/vmhost/vmhost.c | 18 +++++++++++++++--- src/test/test_vmhost.c | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/adapter/vmhost/vmhost.c b/src/adapter/vmhost/vmhost.c index 1033cf8..e929657 100644 --- a/src/adapter/vmhost/vmhost.c +++ b/src/adapter/vmhost/vmhost.c @@ -121,6 +121,9 @@ static void bridge_id(char* out, size_t cap, uint32_t ep, char ab) { snprintf(out, cap, "vmsig-in-%c-%u", ab, ep); } +/* Best-effort object-del of a possibly-stale object id (defined below; fwd for bridge_add). */ +static void bridge_del_fire(struct vmsig_adapter* a, char ab); + /* Add one input-linux object forwarding an evdev node into the guest. grab_all toggles the * device-grab for every input-linux on this endpoint (set on A only — one is enough). The * reply is correlated through the existing pend[] table under VH_OP_BRIDGE_ADD and consumed @@ -130,6 +133,13 @@ static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int g if (!p) return -1; char id[32]; bridge_id(id, sizeof id, a->endpoint, ab); + /* Idempotent re-attach: fire object-del for this id FIRST. A prior daemon instance tears the + * bridge down best-effort WITHOUT a round-trip (bridge_del_fire in vh_close), and a fast + * restart/redeploy can reach here before QEMU processed that del — leaving the object live, + * so a bare object-add fails with "duplicate property ''" (observed for device B). QMP is + * sequential per connection, so this del is applied before the add below; on a clean first + * attach it just no-ops (DeviceNotFound, silently dropped — that frame carries no QMP id). */ + bridge_del_fire(a, ab); uint32_t qid = ++a->next_id; char line[320]; int len = snprintf(line, sizeof line, @@ -144,9 +154,11 @@ static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int g return 0; } -/* Best-effort object_del fired on teardown before the fd closes (see vh_close). No reply is - * awaited; QEMU also drops these objects when the VM powers off, so del matters only on a - * detach without power-off (daemon restart / endpoint move). */ +/* Best-effort object-del, no reply awaited. Fired in TWO places: on teardown before the fd + * closes (vh_close) AND before every object-add (bridge_add, idempotent re-attach). QEMU drops + * these objects when the VM powers off, so del matters on a detach/re-attach without power-off + * (daemon restart / endpoint move). A del of an absent id no-ops (DeviceNotFound, silently + * dropped — this frame carries no QMP id, so handle_line finds no pend). */ static void bridge_del_fire(struct vmsig_adapter* a, char ab) { char id[32]; bridge_id(id, sizeof id, a->endpoint, ab); diff --git a/src/test/test_vmhost.c b/src/test/test_vmhost.c index dbf0012..a062496 100644 --- a/src/test/test_vmhost.c +++ b/src/test/test_vmhost.c @@ -5,8 +5,10 @@ * * It also verifies the host->guest input bridge: with bridge_evdev_a/b set in cfg, on reaching * READY the seam adds two input-linux objects (A with grab_all, B without) over its own - * connection, with neutral per-endpoint ids and the evdev paths from cfg; the bridge replies - * never surface as ACK/VM_LIFECYCLE to control; on teardown it fires object_del for both. */ + * connection, with neutral per-endpoint ids and the evdev paths from cfg; each add is preceded + * by an idempotent object-del of the same id (clears a stale object from a crashed/racing prior + * daemon); the bridge replies never surface as ACK/VM_LIFECYCLE to control; on teardown it + * fires object-del for both. */ #define _GNU_SOURCE #include "vmsig.h" #include "vmhost.h" /* private cfg (CMake provides the include path) */ @@ -148,6 +150,10 @@ int main(void) { CHECK(srv_expect(c, "\"vmsig-in-b-0\""), "bridge B has neutral per-endpoint id"); CHECK(srv_expect(c, EVDEV_A), "bridge A carries the cfg evdev path for A"); CHECK(srv_expect(c, EVDEV_B), "bridge B carries the cfg evdev path for B"); + /* Idempotent re-attach: each add is preceded by an object-del of the same id. The EOF + * teardown below skips del (seam DEAD), so this object-del can ONLY originate from the + * del-before-add path. */ + CHECK(srv_expect(c, "object-del"), "bridge fires object-del before add (idempotent re-attach)"); srv_send(c, "{\"return\": {}, \"id\": 1}\r\n"); /* ack bridge A (consumed silently) */ srv_send(c, "{\"return\": {}, \"id\": 2}\r\n"); /* ack bridge B (consumed silently) */ @@ -219,6 +225,10 @@ int main(void) { srv_send(c2, "{\"return\": {}, \"id\": 1}\r\n"); srv_send(c2, "{\"return\": {}, \"id\": 2}\r\n"); + /* Attach already emitted object-del (del-before-add). Reset the accumulator so the + * teardown del below is verified in ISOLATION, not satisfied by the attach del. */ + rx_reset(); + /* Clean reap WITHOUT EOF: stop the loop then free (vh_close fires del). */ vmsig_core_stop(core2); pthread_join(th2, NULL);