test-qmp-client-qemu: exercise add-fd on the first invoke

Covers the SCM_RIGHTS fd-passing path end-to-end against a real QEMU: open an
eventfd, hand it off via QMP_CLIENT_ARGS_FD() on the very first qmp_client_invoke()
against a fresh client, and verify QEMU's add-fd reply carries the expected
fdset-id. Complements the mock-based unit test with an authoritative check that
QEMU actually consumes the fd from its FIFO receive queue when processing the
command — the AF_UNIX kernel behaviour around non-scm skb absorption into
following scm-bearing recvs is real-traffic-shaped rather than mock-shaped.
This commit is contained in:
Daan De Meyer
2026-04-24 14:29:31 +02:00
committed by Daan De Meyer
parent 466662c8bd
commit 4ac6fc336d
+73
View File
@@ -9,6 +9,7 @@
* Skipped automatically if QEMU is not installed. */
#include <signal.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include "sd-event.h"
@@ -254,6 +255,78 @@ TEST(qmp_client_qemu_query_status) {
pidref_done(&pidref);
}
TEST(qmp_client_qemu_add_fd) {
_cleanup_free_ char *qemu = NULL;
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
_cleanup_close_ int fd_to_pass = -EBADF;
QmpTestResult t = {};
_cleanup_close_pair_ int qmp_fds[2] = EBADF_PAIR;
int r;
if (find_qemu_binary(&qemu) < 0) {
log_tests_skipped("QEMU not found");
return;
}
ASSERT_OK(sd_event_new(&event));
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
ASSERT_OK(start_qemu(qemu, qmp_fds[1], &pidref));
qmp_fds[1] = safe_close(qmp_fds[1]);
r = qmp_client_connect_fd(&client, qmp_fds[0]);
if (r < 0) {
log_tests_skipped_errno(r, "QMP connect failed");
return;
}
TAKE_FD(qmp_fds[0]);
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
fd_to_pass = eventfd(0, EFD_CLOEXEC);
ASSERT_OK_ERRNO(fd_to_pass);
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
/* Pass an fd via SCM_RIGHTS on the very first invoke against a fresh client:
* add-fd lands right after the eagerly-enqueued qmp_capabilities. QEMU processes cap
* first (no fd needed), then add-fd, popping the fd from its FIFO receive queue. */
r = qmp_client_invoke(client, /* ret_slot= */ NULL, "add-fd",
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
on_test_result, &t);
if (r < 0) {
log_tests_skipped_errno(r, "QMP add-fd invoke failed");
return;
}
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
ASSERT_NOT_NULL(t.result);
sd_json_variant *fdset_id = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "fdset-id"));
sd_json_variant *fd_v = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "fd"));
ASSERT_EQ(sd_json_variant_unsigned(fdset_id), (uint64_t) 0);
log_info("add-fd returned fdset-id=%" PRIu64 ", fd=%" PRIu64,
sd_json_variant_unsigned(fdset_id),
sd_json_variant_unsigned(fd_v));
qmp_test_result_done(&t);
/* Clean shutdown */
ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "quit", NULL, on_test_result, &t));
qmp_test_wait(event, &t);
ASSERT_EQ(t.error, 0);
qmp_test_result_done(&t);
siginfo_t si = {};
ASSERT_OK(pidref_wait_for_terminate(&pidref, &si));
ASSERT_EQ(si.si_code, CLD_EXITED);
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
pidref_done(&pidref);
}
static int intro(void) {
/* QEMU dies between our last write and read on the QMP socket — without this we'd
* get killed by the SIGPIPE the kernel raises on write-after-EOF. */