Commit e867e2047c for qemu.org
commit e867e2047c6cf8c1e59e8e1155291cc0a0b9dd29
Author: Denis V. Lunev <den@openvz.org>
Date: Mon Aug 31 12:01:49 2026 +0200
tests/unit: add websock handshake test
Check that malformed HTTP greetings are answered with an HTTP 400
rather than an empty response. The no-space case is the one which used
to leave the response buffer empty.
Fixes: CVE-2026-84788
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
[DB: exclude test from Windows since it depends on AF_UNIX
which is not universally available]
Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
diff --git a/tests/unit/meson.build b/tests/unit/meson.build
index 3a9866c1f2..e47bc7225a 100644
--- a/tests/unit/meson.build
+++ b/tests/unit/meson.build
@@ -115,6 +115,7 @@ if have_block
endif
if host_os != 'windows'
tests += {
+ 'test-io-channel-websock': [io],
'test-image-locking': [testblock],
'test-nested-aio-poll': [],
}
diff --git a/tests/unit/test-io-channel-websock.c b/tests/unit/test-io-channel-websock.c
new file mode 100644
index 0000000000..2a55a4bcdf
--- /dev/null
+++ b/tests/unit/test-io-channel-websock.c
@@ -0,0 +1,105 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * QEMU I/O channel websock test
+ *
+ * Copyright (c) 2026 Virtuozzo International GmbH
+ */
+
+#include "qemu/osdep.h"
+#include "io/channel-websock.h"
+#include "io/channel-socket.h"
+#include "qapi/error.h"
+#include "qemu/module.h"
+#include "qemu/sockets.h"
+
+typedef struct {
+ bool finished;
+ bool failed;
+} QIOChannelWebsockHandshake;
+
+static void test_websock_handshake_done(QIOTask *task, gpointer opaque)
+{
+ QIOChannelWebsockHandshake *res = opaque;
+
+ res->finished = true;
+ res->failed = qio_task_propagate_error(task, NULL);
+}
+
+/*
+ * Drives a server-side handshake against @request and returns whatever
+ * the server wrote back, NUL terminated. The handshake is expected to
+ * fail; the point of the test is the HTTP response that goes with it.
+ */
+static char *test_websock_handshake_reply(const char *request)
+{
+ QIOChannelWebsockHandshake res = { false, false };
+ QIOChannelSocket *cli, *srv;
+ QIOChannelWebsock *wioc;
+ GMainContext *mainloop;
+ int channel[2];
+ char *reply;
+ ssize_t got;
+
+ g_assert(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, channel) == 0);
+
+ cli = qio_channel_socket_new_fd(channel[0], &error_abort);
+ srv = qio_channel_socket_new_fd(channel[1], &error_abort);
+ qio_channel_set_blocking(QIO_CHANNEL(srv), false, &error_abort);
+ qio_channel_set_blocking(QIO_CHANNEL(cli), false, &error_abort);
+
+ wioc = qio_channel_websock_new_server(QIO_CHANNEL(srv));
+ qio_channel_websock_handshake(wioc, test_websock_handshake_done,
+ &res, NULL);
+
+ qio_channel_write_all(QIO_CHANNEL(cli), request, strlen(request),
+ &error_abort);
+
+ mainloop = g_main_context_default();
+ while (!res.finished) {
+ g_main_context_iteration(mainloop, TRUE);
+ }
+ g_assert(res.failed);
+
+ reply = g_malloc0(1024);
+ got = qio_channel_read(QIO_CHANNEL(cli), reply, 1023, &error_abort);
+ if (got > 0) {
+ reply[got] = '\0';
+ }
+
+ object_unref(OBJECT(wioc));
+ object_unref(OBJECT(srv));
+ object_unref(OBJECT(cli));
+
+ return reply;
+}
+
+static void test_websock_bad_request(const void *opaque)
+{
+ const char *request = opaque;
+ g_autofree char *reply = test_websock_handshake_reply(request);
+
+ g_assert_true(g_str_has_prefix(reply, "HTTP/1.1 400 Bad Request\r\n"));
+}
+
+int main(int argc, char **argv)
+{
+ module_call_init(MODULE_INIT_QOM);
+ g_test_init(&argc, &argv, NULL);
+
+#define TEST_BAD_REQUEST(name, request) \
+ g_test_add_data_func("/io/channel/websock/bad-request/" name, \
+ request, test_websock_bad_request)
+
+ /*
+ * A greeting with no space at all used to leave the response buffer
+ * empty, which drove the handshake into a zero length write.
+ */
+ TEST_BAD_REQUEST("no-space", "stats\r\nx\r\n\r\n");
+ TEST_BAD_REQUEST("method-only", "GET\r\nx\r\n\r\n");
+ TEST_BAD_REQUEST("no-version", "GET /\r\nx\r\n\r\n");
+ TEST_BAD_REQUEST("bad-method", "POST / HTTP/1.1\r\nx: y\r\n\r\n");
+ TEST_BAD_REQUEST("bad-version", "GET / HTTP/1.0\r\nx: y\r\n\r\n");
+
+ return g_test_run();
+}