libvirtd aborts with "free(): invalid pointer" whenever a stream write fails, and then crash-loops until systemd's start limit is reached, leaving the host with no management plane. Running guests are unaffected but become unmanageable.
The cause is an ambiguous return contract. daemonStreamHandleWriteData() reports a failed stream write through virNetServerProgramSendReplyError(), which takes ownership of the message and queues it on client->tx, and then returns that function's result - 0 on success. Its caller reads 0 as "no reply was sent" and, for VIR_NET_CONTINUE, clears the message and queues it a second time. Since the message is by then the sole element of client->tx, virNetMessageQueuePush() walks to the tail - itself - and links it to itself. virNetMessageQueueServe() then hands out the same pointer twice and virNetServerClientDispatchWrite() frees it twice. Patch 1 is the fix: give the handlers a distinct return value meaning "already queued, do not touch msg again". daemonStreamHandleHole() had the same defect and is fixed alongside. Note the requeue test has to change from "ret > 0" to "ret == 1", otherwise the new value would requeue a message which is already on client->tx. Patch 2 is independent hardening: make virNetMessageQueuePush() refuse a push that would corrupt the list, so a caller bug of this shape surfaces as a log message rather than as heap corruption. It includes a regression test that reproduces the self-cycle deterministically. Note that virNetMessageClear() memsets the whole message including ->next, so a queued message can appear unlinked. That is why patch 2 walks the queue rather than testing msg->next, and why this class of bug is easy to miss by inspection. There is no ABI change; src/libvirt_remote.syms is untouched. Evidence -------- - valgrind memcheck: "Invalid free()" with the previous free at the *same* call site, plus 13 invalid reads and 6 invalid writes as the "while (client->tx)" loop re-reads the freed block. Exactly one invalid free per occurrence, as a self-cycle predicts. - A production core dump whose crash IP is the return address of the same call to virNetMessageFree(), resolving to virnetserverclient.c:1374. - Instrumented builds logging both push sites: virNetServerProgramSendError:168 followed by daemonStreamHandleWrite:797. Two cautions for anyone reproducing this: Under valgrind the daemon does *not* abort - memcheck replaces the allocator, so glibc's malloc_printerr never runs. It logs and continues, and the fault is easy to mistake for "works fine". Freed virNetMessage blocks are promptly reused by malloc, so the same address legitimately reappears as a new message moments after being freed. In RPC debug logs that closely resembles a use-after-free and is not one. Testing ------- Reproduced and fixed on three production hypervisors running 12.5.0 with these patches backported. One host had logged 5197 aborts beforehand; across all three there have been zero aborts and zero hardening warnings since. The hardening warning firing before the patch 1 fix and never firing after it is the direct evidence that the root cause, and not just the symptom, is addressed. Full test suite passes on master (306 ok, 1 expected fail, 0 failures). CI on a personal fork is green: 22 jobs passed, 0 failed, covering Fedora 43/44/rawhide, CentOS Stream 9/10, Ubuntu 24.04/26.04 (including clang), openSUSE Tumbleweed/Leap 16, Debian 13, armv7l and mingw32/64, plus check-dco and codestyle. Reported as https://gitlab.com/libvirt/libvirt/-/issues/902 Ross Golder (2): remote: don't queue the stream error reply twice rpc: refuse to queue a message that is already queued src/remote/remote_daemon_stream.c | 51 +++++++++++++----- src/rpc/virnetmessage.c | 43 +++++++++++++++ tests/virnetmessagetest.c | 87 +++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 12 deletions(-) -- 2.53.0
