* [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
@ 2026-08-31 10:01 Denis V. Lunev
2026-08-31 10:01 ` [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error Denis V. Lunev
` (7 more replies)
0 siblings, 8 replies; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel; +Cc: den, Daniel P. Berrangé, Marc-André Lureau
A client which can reach a VNC websocket port crashes QEMU before it has
authenticated, by sending an HTTP greeting whose request line holds no
space:
printf 'stats\r\nx\r\n\r\n' | nc $host $port
Three defects line up to produce it. The greeting is rejected without
queueing a response, so the handshake goes on to flush an empty buffer.
A zero length sendmsg() succeeds and returns 0, which
qio_channel_socket_writev() mistakes for failure and reports as
QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
negative return as fatal and hands that NULL Error to
error_get_pretty(). Patches 1 to 3 close the three links.
Patch 5 is the same NULL Error on the read side of the handshake, where
ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
master channel is then a TLS channel: a wakeup carrying only part of a
record makes gnutls report EAGAIN.
The tests drive the handshake through a channel which reports ERR_BLOCK
on demand, covering both directions. No test reproduces the original
crash itself, which turns on a stale errno and is not reliably
reproducible in a unit test. What they pin is that a 400 is emitted and
that ERR_BLOCK no longer reaches error_get_pretty().
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Daniel P. Berrangé <berrange@redhat.com>
CC: Marc-André Lureau <marcandre.lureau@redhat.com>
Denis V. Lunev (6):
io/channel-socket: do not treat a zero length write as an error
io/channel-websock: send an HTTP 400 when the greeting has no space
io/channel-websock: handle a blocked write during the handshake
tests/unit: add websock handshake test
io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
tests/unit: cover blocked IO during the websock handshake
io/channel-socket.c | 2 +-
io/channel-websock.c | 10 +-
tests/unit/meson.build | 1 +
tests/unit/test-io-channel-websock.c | 249 +++++++++++++++++++++++++++
4 files changed, 260 insertions(+), 2 deletions(-)
create mode 100644 tests/unit/test-io-channel-websock.c
--
2.53.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:31 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space Denis V. Lunev
` (6 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel
Cc: den, qemu-stable, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
qio_channel_socket_writev() checks "ret <= 0" after sendmsg(). A zero
length iovec is written successfully and returns 0, so the success
falls into the errno switch, which acts on whatever the last failing
syscall left in errno. A stale EAGAIN turns it into
QIO_CHANNEL_ERR_BLOCK with errp untouched, and a caller which treats
every negative return as fatal then passes a NULL Error to
error_get_pretty(). The websocket handshake does exactly that, so an
unauthenticated client crashes QEMU during the greeting.
Returning 0 is safe for callers which loop until everything is
written. qio_channel_writev_full_all() has no zero progress guard, but
iov_copy() yields no entries for a zero length write, so that loop is
never entered. A connected stream socket returns 0 only when there is
nothing to send.
The WIN32 implementation in the same file uses "ret < 0".
Fixes: 559607ea173a ("io: add QIOChannelSocket class")
Cc: qemu-stable@nongnu.org
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
io/channel-socket.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/io/channel-socket.c b/io/channel-socket.c
index 12773b832c..7920cee639 100644
--- a/io/channel-socket.c
+++ b/io/channel-socket.c
@@ -667,7 +667,7 @@ static ssize_t qio_channel_socket_writev(QIOChannel *ioc,
retry:
ret = sendmsg(sioc->fd, &msg, sflags);
- if (ret <= 0) {
+ if (ret < 0) {
switch (errno) {
case EAGAIN:
return QIO_CHANNEL_ERR_BLOCK;
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
2026-08-31 10:01 ` [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:23 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake Denis V. Lunev
` (5 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel
Cc: den, qemu-stable, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
qio_channel_websock_extract_headers() returns 0 without queueing a
response when the request line contains no space, unlike every sibling
check which jumps to bad_request. encoutput stays empty, yet
qio_channel_websock_handshake_read() still reports success and the
caller arms a G_IO_OUT watch to flush nothing.
Flushing that empty buffer is where QEMU crashes. Any client can
trigger it before authentication on a VNC websocket port:
printf 'stats\r\nx\r\n\r\n' | nc $host $port
Fixes: 07e95cd529af ("io: fully parse & validate HTTP headers for websocket protocol handshake")
Cc: qemu-stable@nongnu.org
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
io/channel-websock.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/io/channel-websock.c b/io/channel-websock.c
index 1929abf56a..66c91ed2a2 100644
--- a/io/channel-websock.c
+++ b/io/channel-websock.c
@@ -230,7 +230,7 @@ qio_channel_websock_extract_headers(QIOChannelWebsock *ioc,
tmp = strchr(buffer, ' ');
if (!tmp) {
error_setg(errp, "Missing HTTP path delimiter");
- return 0;
+ goto bad_request;
}
*tmp = '\0';
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
2026-08-31 10:01 ` [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error Denis V. Lunev
2026-08-31 10:01 ` [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:24 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 4/6] tests/unit: add websock handshake test Denis V. Lunev
` (4 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel
Cc: den, qemu-stable, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
qio_channel_websock_handshake_send() treats every negative return from
qio_channel_write() as fatal and passes err to error_get_pretty().
QIO_CHANNEL_ERR_BLOCK is negative but leaves err NULL, so a socket
which cannot take the response immediately crashes QEMU before the
client has authenticated.
Keep the G_IO_OUT watch armed and retry instead.
Fixes: 2d1d0e70cf3e ("io: add QIOChannelWebsock class")
Cc: qemu-stable@nongnu.org
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
io/channel-websock.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/io/channel-websock.c b/io/channel-websock.c
index 66c91ed2a2..8f27b1f12b 100644
--- a/io/channel-websock.c
+++ b/io/channel-websock.c
@@ -562,6 +562,11 @@ static gboolean qio_channel_websock_handshake_send(QIOChannel *ioc,
wioc->encoutput.offset,
&err);
+ if (ret == QIO_CHANNEL_ERR_BLOCK) {
+ /* Socket buffer is full, the G_IO_OUT watch stays armed */
+ return TRUE;
+ }
+
if (ret < 0) {
trace_qio_channel_websock_handshake_fail(ioc, error_get_pretty(err));
qio_task_set_error(task, err);
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 4/6] tests/unit: add websock handshake test
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
` (2 preceding siblings ...)
2026-08-31 10:01 ` [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:25 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading Denis V. Lunev
` (3 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel; +Cc: den, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
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.
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
tests/unit/meson.build | 1 +
tests/unit/test-io-channel-websock.c | 105 +++++++++++++++++++++++++++
2 files changed, 106 insertions(+)
create mode 100644 tests/unit/test-io-channel-websock.c
diff --git a/tests/unit/meson.build b/tests/unit/meson.build
index 3a9866c1f2..6a11f07112 100644
--- a/tests/unit/meson.build
+++ b/tests/unit/meson.build
@@ -93,6 +93,7 @@ if have_block
'test-io-channel-command': ['io-channel-helpers.c', io],
'test-io-channel-buffer': ['io-channel-helpers.c', io],
'test-io-channel-null': [io],
+ 'test-io-channel-websock': [io],
'test-crypto-ivgen': [io],
'test-crypto-afsplit': [io],
'test-crypto-block': [io],
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();
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
` (3 preceding siblings ...)
2026-08-31 10:01 ` [PATCH 4/6] tests/unit: add websock handshake test Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:26 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake Denis V. Lunev
` (2 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel
Cc: den, qemu-stable, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
qio_channel_websock_handshake_read() folds every negative return from
qio_channel_read() into -1. QIO_CHANNEL_ERR_BLOCK leaves errp unset, so
qio_channel_websock_handshake_io() then hands a NULL Error to
error_get_pretty() and QEMU dies.
The master channel is non-blocking and, for a wss:// client, is a TLS
channel. A G_IO_IN wakeup carrying only part of a TLS record makes
gnutls report EAGAIN, which is all it takes to reach this before the
client has authenticated.
ERR_BLOCK here means the headers are not complete yet, which is what a
0 return already tells the caller. Report it that way and keep waiting.
The watch is level triggered, so an incomplete record sitting in the
socket spins the main loop until the rest of it arrives. That is
bounded by the round trip and is what every reader layered over TLS
already does.
Fixes: 2d1d0e70cf3e ("io: add QIOChannelWebsock class")
Cc: qemu-stable@nongnu.org
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
io/channel-websock.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/io/channel-websock.c b/io/channel-websock.c
index 8f27b1f12b..461abcae48 100644
--- a/io/channel-websock.c
+++ b/io/channel-websock.c
@@ -492,6 +492,9 @@ static int qio_channel_websock_handshake_read(QIOChannelWebsock *ioc,
buffer_reserve(&ioc->encinput, want);
ret = qio_channel_read(ioc->master,
(char *)buffer_end(&ioc->encinput), want, errp);
+ if (ret == QIO_CHANNEL_ERR_BLOCK) {
+ return 0;
+ }
if (ret < 0) {
return -1;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
` (4 preceding siblings ...)
2026-08-31 10:01 ` [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading Denis V. Lunev
@ 2026-08-31 10:01 ` Denis V. Lunev
2026-09-01 17:30 ` Daniel P. Berrangé
2026-08-31 11:29 ` [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake marcandre.lureau
2026-09-01 17:43 ` Daniel P. Berrangé
7 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 10:01 UTC (permalink / raw)
To: qemu-devel; +Cc: den, Daniel P. Berrangé, Marc-André Lureau
From: Denis V. Lunev <den@openvz.org>
Add a channel which reports QIO_CHANNEL_ERR_BLOCK on demand, the way a
TLS channel does when a record arrives split across segments or when
the socket cannot take the whole reply at once, and drive the server
handshake through it in both directions. Without the fixes each
direction dereferences a NULL Error and the test dies on SIGSEGV.
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
tests/unit/test-io-channel-websock.c | 150 ++++++++++++++++++++++++++-
1 file changed, 147 insertions(+), 3 deletions(-)
diff --git a/tests/unit/test-io-channel-websock.c b/tests/unit/test-io-channel-websock.c
index 2a55a4bcdf..88da24f993 100644
--- a/tests/unit/test-io-channel-websock.c
+++ b/tests/unit/test-io-channel-websock.c
@@ -12,6 +12,123 @@
#include "qapi/error.h"
#include "qemu/module.h"
#include "qemu/sockets.h"
+#include "qom/object.h"
+
+#define TYPE_QIO_CHANNEL_STALL "qio-channel-stall"
+OBJECT_DECLARE_SIMPLE_TYPE(QIOChannelStall, QIO_CHANNEL_STALL)
+
+/*
+ * Reports QIO_CHANNEL_ERR_BLOCK for the first @rstalls reads and @wstalls
+ * writes, the way a TLS channel does when a record arrives split across TCP
+ * segments or the socket cannot take the whole reply at once.
+ */
+struct QIOChannelStall {
+ QIOChannel parent;
+ QIOChannel *master;
+ unsigned rstalls;
+ unsigned wstalls;
+};
+
+static ssize_t qio_channel_stall_readv(QIOChannel *ioc,
+ const struct iovec *iov,
+ size_t niov,
+ int **fds,
+ size_t *nfds,
+ int flags,
+ Error **errp)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(ioc);
+
+ if (sioc->rstalls) {
+ sioc->rstalls--;
+ return QIO_CHANNEL_ERR_BLOCK;
+ }
+ return qio_channel_readv_full(sioc->master, iov, niov, fds, nfds,
+ flags, errp);
+}
+
+static ssize_t qio_channel_stall_writev(QIOChannel *ioc,
+ const struct iovec *iov,
+ size_t niov,
+ int *fds,
+ size_t nfds,
+ int flags,
+ Error **errp)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(ioc);
+
+ if (sioc->wstalls) {
+ sioc->wstalls--;
+ return QIO_CHANNEL_ERR_BLOCK;
+ }
+ return qio_channel_writev_full(sioc->master, iov, niov, fds, nfds,
+ flags, errp);
+}
+
+static int qio_channel_stall_set_blocking(QIOChannel *ioc, bool enabled,
+ Error **errp)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(ioc);
+
+ return qio_channel_set_blocking(sioc->master, enabled, errp) ? 0 : -1;
+}
+
+static int qio_channel_stall_close(QIOChannel *ioc, Error **errp)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(ioc);
+
+ return qio_channel_close(sioc->master, errp);
+}
+
+static GSource *qio_channel_stall_create_watch(QIOChannel *ioc,
+ GIOCondition condition)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(ioc);
+
+ return qio_channel_create_watch(sioc->master, condition);
+}
+
+static void qio_channel_stall_finalize(Object *obj)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(obj);
+
+ object_unref(OBJECT(sioc->master));
+}
+
+static void qio_channel_stall_class_init(ObjectClass *klass,
+ const void *class_data G_GNUC_UNUSED)
+{
+ QIOChannelClass *ioc_klass = QIO_CHANNEL_CLASS(klass);
+
+ ioc_klass->io_writev = qio_channel_stall_writev;
+ ioc_klass->io_readv = qio_channel_stall_readv;
+ ioc_klass->io_set_blocking = qio_channel_stall_set_blocking;
+ ioc_klass->io_close = qio_channel_stall_close;
+ ioc_klass->io_create_watch = qio_channel_stall_create_watch;
+}
+
+static const TypeInfo qio_channel_stall_info = {
+ .parent = TYPE_QIO_CHANNEL,
+ .name = TYPE_QIO_CHANNEL_STALL,
+ .instance_size = sizeof(QIOChannelStall),
+ .instance_finalize = qio_channel_stall_finalize,
+ .class_init = qio_channel_stall_class_init,
+};
+
+static QIOChannelStall *qio_channel_stall_new(QIOChannel *master,
+ unsigned rstalls,
+ unsigned wstalls)
+{
+ QIOChannelStall *sioc = QIO_CHANNEL_STALL(
+ object_new(TYPE_QIO_CHANNEL_STALL));
+
+ object_ref(OBJECT(master));
+ sioc->master = master;
+ sioc->rstalls = rstalls;
+ sioc->wstalls = wstalls;
+
+ return sioc;
+}
typedef struct {
bool finished;
@@ -31,10 +148,12 @@ static void test_websock_handshake_done(QIOTask *task, gpointer opaque)
* 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)
+static char *test_websock_handshake_reply(const char *request,
+ unsigned rstalls, unsigned wstalls)
{
QIOChannelWebsockHandshake res = { false, false };
QIOChannelSocket *cli, *srv;
+ QIOChannelStall *stall;
QIOChannelWebsock *wioc;
GMainContext *mainloop;
int channel[2];
@@ -48,7 +167,8 @@ static char *test_websock_handshake_reply(const char *request)
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));
+ stall = qio_channel_stall_new(QIO_CHANNEL(srv), rstalls, wstalls);
+ wioc = qio_channel_websock_new_server(QIO_CHANNEL(stall));
qio_channel_websock_handshake(wioc, test_websock_handshake_done,
&res, NULL);
@@ -68,6 +188,7 @@ static char *test_websock_handshake_reply(const char *request)
}
object_unref(OBJECT(wioc));
+ object_unref(OBJECT(stall));
object_unref(OBJECT(srv));
object_unref(OBJECT(cli));
@@ -77,7 +198,23 @@ static char *test_websock_handshake_reply(const char *request)
static void test_websock_bad_request(const void *opaque)
{
const char *request = opaque;
- g_autofree char *reply = test_websock_handshake_reply(request);
+ g_autofree char *reply = test_websock_handshake_reply(request, 0, 0);
+
+ g_assert_true(g_str_has_prefix(reply, "HTTP/1.1 400 Bad Request\r\n"));
+}
+
+static void test_websock_stalled_read(const void *opaque)
+{
+ const char *request = opaque;
+ g_autofree char *reply = test_websock_handshake_reply(request, 1, 0);
+
+ g_assert_true(g_str_has_prefix(reply, "HTTP/1.1 400 Bad Request\r\n"));
+}
+
+static void test_websock_stalled_write(const void *opaque)
+{
+ const char *request = opaque;
+ g_autofree char *reply = test_websock_handshake_reply(request, 0, 1);
g_assert_true(g_str_has_prefix(reply, "HTTP/1.1 400 Bad Request\r\n"));
}
@@ -85,6 +222,7 @@ static void test_websock_bad_request(const void *opaque)
int main(int argc, char **argv)
{
module_call_init(MODULE_INIT_QOM);
+ type_register_static(&qio_channel_stall_info);
g_test_init(&argc, &argv, NULL);
#define TEST_BAD_REQUEST(name, request) \
@@ -101,5 +239,11 @@ int main(int argc, char **argv)
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");
+ /* A read which blocks before any header arrives is not a fatal error. */
+ g_test_add_data_func("/io/channel/websock/stalled-read",
+ "stats\r\nx\r\n\r\n", test_websock_stalled_read);
+ g_test_add_data_func("/io/channel/websock/stalled-write",
+ "stats\r\nx\r\n\r\n", test_websock_stalled_write);
+
return g_test_run();
}
--
2.53.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
` (5 preceding siblings ...)
2026-08-31 10:01 ` [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake Denis V. Lunev
@ 2026-08-31 11:29 ` marcandre.lureau
2026-08-31 11:33 ` Denis V. Lunev
2026-09-01 17:43 ` Daniel P. Berrangé
7 siblings, 1 reply; 18+ messages in thread
From: marcandre.lureau @ 2026-08-31 11:29 UTC (permalink / raw)
To: Denis V. Lunev
Cc: qemu-devel, Daniel P. Berrangé, Marc-André Lureau
> A client which can reach a VNC websocket port crashes QEMU before it has
> authenticated, by sending an HTTP greeting whose request line holds no
> space:
>
> printf 'stats\r\nx\r\n\r\n' | nc $host $port
>
> Three defects line up to produce it. The greeting is rejected without
> queueing a response, so the handshake goes on to flush an empty buffer.
> A zero length sendmsg() succeeds and returns 0, which
> qio_channel_socket_writev() mistakes for failure and reports as
> QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
> negative return as fatal and hands that NULL Error to
> error_get_pretty(). Patches 1 to 3 close the three links.
>
> Patch 5 is the same NULL Error on the read side of the handshake, where
> ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
> master channel is then a TLS channel: a wakeup carrying only part of a
> record makes gnutls report EAGAIN.
>
> The tests drive the handshake through a channel which reports ERR_BLOCK
> on demand, covering both directions. No test reproduces the original
> crash itself, which turns on a stale errno and is not reliably
> reproducible in a unit test. What they pin is that a 400 is emitted and
> that ERR_BLOCK no longer reaches error_get_pretty().
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> CC: Daniel P. Berrangé <berrange@redhat.com>
> CC: Marc-André Lureau <marcandre.lureau@redhat.com>
>
> Denis V. Lunev (6):
> io/channel-socket: do not treat a zero length write as an error
> io/channel-websock: send an HTTP 400 when the greeting has no space
> io/channel-websock: handle a blocked write during the handshake
> tests/unit: add websock handshake test
> io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
> tests/unit: cover blocked IO during the websock handshake
>
> io/channel-socket.c | 2 +-
> io/channel-websock.c | 10 +-
> tests/unit/meson.build | 1 +
> tests/unit/test-io-channel-websock.c | 249 +++++++++++++++++++++++++++
> 4 files changed, 260 insertions(+), 2 deletions(-)
> create mode 100644 tests/unit/test-io-channel-websock.c
>
Patch series lgtm. You cc stable, why didn't you file a CVE?
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
--
Marc-André Lureau <marcandre.lureau@redhat.com>
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
2026-08-31 11:29 ` [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake marcandre.lureau
@ 2026-08-31 11:33 ` Denis V. Lunev
2026-09-01 17:54 ` Daniel P. Berrangé
0 siblings, 1 reply; 18+ messages in thread
From: Denis V. Lunev @ 2026-08-31 11:33 UTC (permalink / raw)
To: marcandre.lureau, Denis V. Lunev; +Cc: qemu-devel, Daniel P. Berrangé
On 8/31/26 13:29, marcandre.lureau@redhat.com wrote:
>> A client which can reach a VNC websocket port crashes QEMU before it has
>> authenticated, by sending an HTTP greeting whose request line holds no
>> space:
>>
>> printf 'stats\r\nx\r\n\r\n' | nc $host $port
>>
>> Three defects line up to produce it. The greeting is rejected without
>> queueing a response, so the handshake goes on to flush an empty buffer.
>> A zero length sendmsg() succeeds and returns 0, which
>> qio_channel_socket_writev() mistakes for failure and reports as
>> QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
>> negative return as fatal and hands that NULL Error to
>> error_get_pretty(). Patches 1 to 3 close the three links.
>>
>> Patch 5 is the same NULL Error on the read side of the handshake, where
>> ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
>> master channel is then a TLS channel: a wakeup carrying only part of a
>> record makes gnutls report EAGAIN.
>>
>> The tests drive the handshake through a channel which reports ERR_BLOCK
>> on demand, covering both directions. No test reproduces the original
>> crash itself, which turns on a stale errno and is not reliably
>> reproducible in a unit test. What they pin is that a 400 is emitted and
>> that ERR_BLOCK no longer reaches error_get_pretty().
>>
>> Signed-off-by: Denis V. Lunev <den@openvz.org>
>> CC: Daniel P. Berrangé <berrange@redhat.com>
>> CC: Marc-André Lureau <marcandre.lureau@redhat.com>
>>
>> Denis V. Lunev (6):
>> io/channel-socket: do not treat a zero length write as an error
>> io/channel-websock: send an HTTP 400 when the greeting has no space
>> io/channel-websock: handle a blocked write during the handshake
>> tests/unit: add websock handshake test
>> io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
>> tests/unit: cover blocked IO during the websock handshake
>>
>> io/channel-socket.c | 2 +-
>> io/channel-websock.c | 10 +-
>> tests/unit/meson.build | 1 +
>> tests/unit/test-io-channel-websock.c | 249 +++++++++++++++++++++++++++
>> 4 files changed, 260 insertions(+), 2 deletions(-)
>> create mode 100644 tests/unit/test-io-channel-websock.c
>>
> Patch series lgtm. You cc stable, why didn't you file a CVE?
>
> Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
>
I have calculated the scope and get around 6.2 which is
not looking too impressive to spend a time.
There are toooooo many CVEs circulating around :-)
If this is wrong approach - let me know. I'll change
the approach.
Thanks for review,
Den
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space
2026-08-31 10:01 ` [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space Denis V. Lunev
@ 2026-09-01 17:23 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:23 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, qemu-stable, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:47PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> qio_channel_websock_extract_headers() returns 0 without queueing a
> response when the request line contains no space, unlike every sibling
> check which jumps to bad_request. encoutput stays empty, yet
> qio_channel_websock_handshake_read() still reports success and the
> caller arms a G_IO_OUT watch to flush nothing.
>
> Flushing that empty buffer is where QEMU crashes. Any client can
> trigger it before authentication on a VNC websocket port:
>
> printf 'stats\r\nx\r\n\r\n' | nc $host $port
>
> Fixes: 07e95cd529af ("io: fully parse & validate HTTP headers for websocket protocol handshake")
"bad_request" was introduced in f69a8bde29354493ff8aea64cc9cb3b531d16337
which intended to fix 07e95cd529af, but missed that one return statement.
So I'd suggest "Fixes: f69a8bde29", or even both commits.
> Cc: qemu-stable@nongnu.org
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> io/channel-websock.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
>
> diff --git a/io/channel-websock.c b/io/channel-websock.c
> index 1929abf56a..66c91ed2a2 100644
> --- a/io/channel-websock.c
> +++ b/io/channel-websock.c
> @@ -230,7 +230,7 @@ qio_channel_websock_extract_headers(QIOChannelWebsock *ioc,
> tmp = strchr(buffer, ' ');
> if (!tmp) {
> error_setg(errp, "Missing HTTP path delimiter");
> - return 0;
> + goto bad_request;
> }
> *tmp = '\0';
>
> --
> 2.53.0
>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake
2026-08-31 10:01 ` [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake Denis V. Lunev
@ 2026-09-01 17:24 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:24 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, qemu-stable, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:48PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> qio_channel_websock_handshake_send() treats every negative return from
> qio_channel_write() as fatal and passes err to error_get_pretty().
> QIO_CHANNEL_ERR_BLOCK is negative but leaves err NULL, so a socket
> which cannot take the response immediately crashes QEMU before the
> client has authenticated.
>
> Keep the G_IO_OUT watch armed and retry instead.
>
> Fixes: 2d1d0e70cf3e ("io: add QIOChannelWebsock class")
> Cc: qemu-stable@nongnu.org
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> io/channel-websock.c | 5 +++++
> 1 file changed, 5 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 4/6] tests/unit: add websock handshake test
2026-08-31 10:01 ` [PATCH 4/6] tests/unit: add websock handshake test Denis V. Lunev
@ 2026-09-01 17:25 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:25 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:49PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> 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.
>
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> tests/unit/meson.build | 1 +
> tests/unit/test-io-channel-websock.c | 105 +++++++++++++++++++++++++++
> 2 files changed, 106 insertions(+)
> create mode 100644 tests/unit/test-io-channel-websock.c
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
2026-08-31 10:01 ` [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading Denis V. Lunev
@ 2026-09-01 17:26 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:26 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, qemu-stable, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:50PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> qio_channel_websock_handshake_read() folds every negative return from
> qio_channel_read() into -1. QIO_CHANNEL_ERR_BLOCK leaves errp unset, so
> qio_channel_websock_handshake_io() then hands a NULL Error to
> error_get_pretty() and QEMU dies.
>
> The master channel is non-blocking and, for a wss:// client, is a TLS
> channel. A G_IO_IN wakeup carrying only part of a TLS record makes
> gnutls report EAGAIN, which is all it takes to reach this before the
> client has authenticated.
>
> ERR_BLOCK here means the headers are not complete yet, which is what a
> 0 return already tells the caller. Report it that way and keep waiting.
> The watch is level triggered, so an incomplete record sitting in the
> socket spins the main loop until the rest of it arrives. That is
> bounded by the round trip and is what every reader layered over TLS
> already does.
>
> Fixes: 2d1d0e70cf3e ("io: add QIOChannelWebsock class")
> Cc: qemu-stable@nongnu.org
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> io/channel-websock.c | 3 +++
> 1 file changed, 3 insertions(+)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake
2026-08-31 10:01 ` [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake Denis V. Lunev
@ 2026-09-01 17:30 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:30 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:51PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> Add a channel which reports QIO_CHANNEL_ERR_BLOCK on demand, the way a
> TLS channel does when a record arrives split across segments or when
> the socket cannot take the whole reply at once, and drive the server
> handshake through it in both directions. Without the fixes each
> direction dereferences a NULL Error and the test dies on SIGSEGV.
>
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> tests/unit/test-io-channel-websock.c | 150 ++++++++++++++++++++++++++-
> 1 file changed, 147 insertions(+), 3 deletions(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error
2026-08-31 10:01 ` [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error Denis V. Lunev
@ 2026-09-01 17:31 ` Daniel P. Berrangé
0 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:31 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, qemu-stable, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:46PM +0200, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> qio_channel_socket_writev() checks "ret <= 0" after sendmsg(). A zero
> length iovec is written successfully and returns 0, so the success
> falls into the errno switch, which acts on whatever the last failing
> syscall left in errno. A stale EAGAIN turns it into
> QIO_CHANNEL_ERR_BLOCK with errp untouched, and a caller which treats
> every negative return as fatal then passes a NULL Error to
> error_get_pretty(). The websocket handshake does exactly that, so an
> unauthenticated client crashes QEMU during the greeting.
>
> Returning 0 is safe for callers which loop until everything is
> written. qio_channel_writev_full_all() has no zero progress guard, but
> iov_copy() yields no entries for a zero length write, so that loop is
> never entered. A connected stream socket returns 0 only when there is
> nothing to send.
>
> The WIN32 implementation in the same file uses "ret < 0".
>
> Fixes: 559607ea173a ("io: add QIOChannelSocket class")
> Cc: qemu-stable@nongnu.org
> Cc: Daniel P. Berrangé <berrange@redhat.com>
> Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> ---
> io/channel-socket.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
Though IMHO it is also a bug in the caller to request writing
of zero bytes.
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
` (6 preceding siblings ...)
2026-08-31 11:29 ` [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake marcandre.lureau
@ 2026-09-01 17:43 ` Daniel P. Berrangé
7 siblings, 0 replies; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:43 UTC (permalink / raw)
To: Denis V. Lunev; +Cc: qemu-devel, Marc-André Lureau
On Mon, Aug 31, 2026 at 12:01:45PM +0200, Denis V. Lunev wrote:
> A client which can reach a VNC websocket port crashes QEMU before it has
> authenticated, by sending an HTTP greeting whose request line holds no
> space:
>
> printf 'stats\r\nx\r\n\r\n' | nc $host $port
>
> Three defects line up to produce it. The greeting is rejected without
> queueing a response, so the handshake goes on to flush an empty buffer.
> A zero length sendmsg() succeeds and returns 0, which
> qio_channel_socket_writev() mistakes for failure and reports as
> QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
> negative return as fatal and hands that NULL Error to
> error_get_pretty(). Patches 1 to 3 close the three links.
>
> Patch 5 is the same NULL Error on the read side of the handshake, where
> ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
> master channel is then a TLS channel: a wakeup carrying only part of a
> record makes gnutls report EAGAIN.
>
> The tests drive the handshake through a channel which reports ERR_BLOCK
> on demand, covering both directions. No test reproduces the original
> crash itself, which turns on a stale errno and is not reliably
> reproducible in a unit test. What they pin is that a 400 is emitted and
> that ERR_BLOCK no longer reaches error_get_pretty().
Thanks for the various fixes.
While we're on this topic though, way back when Websocket support was
first introduced to QEMU, I was pretty sceptical that it was a good
idea for QEMU to be implementing the HTTP protocol directly. Some of
these bugs (possibly even all) were likely a direct result of me
porting the websockets code into QIOChannel, so not neccessarily the
original impl.
None the less, I still feel pretty uncomfortable about the idea of
QEMU implementing websockets/HTTP support directly. The lack of
TLS support is a big flag that makes the whole thing questionable.
Although you could put TLS in at the VNC level with VeNCrypt,
IMHO doing it at the HTTP level is the right approach so the VNC
protocol handshake is fully covered.
Having it inside QEMU also means it missed the biggest benefit of
using websockets, which is that you can have a single TCP port
hosting all VMs and select them dynamically from the HTTP request,
instead of one TCP port per VM.
So I've thought about proposing its deprecation & deletion several
times, on the basis that it is better to put an external websockets
proxy in front of QEMU's VNC server instead of inside QEMU.
None the less, I'll queue all these patches.
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
2026-08-31 11:33 ` Denis V. Lunev
@ 2026-09-01 17:54 ` Daniel P. Berrangé
2026-09-02 10:19 ` Mauro Matteo Cascella
0 siblings, 1 reply; 18+ messages in thread
From: Daniel P. Berrangé @ 2026-09-01 17:54 UTC (permalink / raw)
To: Denis V. Lunev, Mauro Matteo Cascella
Cc: marcandre.lureau, Denis V. Lunev, qemu-devel
On Mon, Aug 31, 2026 at 01:33:14PM +0200, Denis V. Lunev wrote:
> On 8/31/26 13:29, marcandre.lureau@redhat.com wrote:
> >> A client which can reach a VNC websocket port crashes QEMU before it has
> >> authenticated, by sending an HTTP greeting whose request line holds no
> >> space:
> >>
> >> printf 'stats\r\nx\r\n\r\n' | nc $host $port
> >>
> >> Three defects line up to produce it. The greeting is rejected without
> >> queueing a response, so the handshake goes on to flush an empty buffer.
> >> A zero length sendmsg() succeeds and returns 0, which
> >> qio_channel_socket_writev() mistakes for failure and reports as
> >> QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
> >> negative return as fatal and hands that NULL Error to
> >> error_get_pretty(). Patches 1 to 3 close the three links.
> >>
> >> Patch 5 is the same NULL Error on the read side of the handshake, where
> >> ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
> >> master channel is then a TLS channel: a wakeup carrying only part of a
> >> record makes gnutls report EAGAIN.
> >>
> >> The tests drive the handshake through a channel which reports ERR_BLOCK
> >> on demand, covering both directions. No test reproduces the original
> >> crash itself, which turns on a stale errno and is not reliably
> >> reproducible in a unit test. What they pin is that a 400 is emitted and
> >> that ERR_BLOCK no longer reaches error_get_pretty().
> >>
> >> Signed-off-by: Denis V. Lunev <den@openvz.org>
> >> CC: Daniel P. Berrangé <berrange@redhat.com>
> >> CC: Marc-André Lureau <marcandre.lureau@redhat.com>
> >>
> >> Denis V. Lunev (6):
> >> io/channel-socket: do not treat a zero length write as an error
> >> io/channel-websock: send an HTTP 400 when the greeting has no space
> >> io/channel-websock: handle a blocked write during the handshake
> >> tests/unit: add websock handshake test
> >> io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
> >> tests/unit: cover blocked IO during the websock handshake
> >>
> >> io/channel-socket.c | 2 +-
> >> io/channel-websock.c | 10 +-
> >> tests/unit/meson.build | 1 +
> >> tests/unit/test-io-channel-websock.c | 249 +++++++++++++++++++++++++++
> >> 4 files changed, 260 insertions(+), 2 deletions(-)
> >> create mode 100644 tests/unit/test-io-channel-websock.c
> >>
> > Patch series lgtm. You cc stable, why didn't you file a CVE?
> >
> > Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> >
> I have calculated the scope and get around 6.2 which is
> not looking too impressive to spend a time.
In the context of a VNC server enabling websocket. If we assume the
VNC server has authentication enabled, including the TLS extension
VeNCrypt, then the deployment can be said to be protecting against
a malicious client.
The flaws mean the malicious client can inflict a denial of service
attack on the VM before getting to the VNC authentication step. IMHO
that is enough to justify a CVE assignment.
CC'ing Mauro to double check my view & assign a CVE if appropriate
With regards,
Daniel
--
|: https://berrange.com ~~ https://hachyderm.io/@berrange :|
|: https://libvirt.org ~~ https://entangle-photo.org :|
|: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake
2026-09-01 17:54 ` Daniel P. Berrangé
@ 2026-09-02 10:19 ` Mauro Matteo Cascella
0 siblings, 0 replies; 18+ messages in thread
From: Mauro Matteo Cascella @ 2026-09-02 10:19 UTC (permalink / raw)
To: Daniel P. Berrangé
Cc: Denis V. Lunev, marcandre.lureau, Denis V. Lunev, qemu-devel
On Tue, Sep 1, 2026 at 7:54 PM Daniel P. Berrangé <berrange@redhat.com> wrote:
>
> On Mon, Aug 31, 2026 at 01:33:14PM +0200, Denis V. Lunev wrote:
> > On 8/31/26 13:29, marcandre.lureau@redhat.com wrote:
> > >> A client which can reach a VNC websocket port crashes QEMU before it has
> > >> authenticated, by sending an HTTP greeting whose request line holds no
> > >> space:
> > >>
> > >> printf 'stats\r\nx\r\n\r\n' | nc $host $port
> > >>
> > >> Three defects line up to produce it. The greeting is rejected without
> > >> queueing a response, so the handshake goes on to flush an empty buffer.
> > >> A zero length sendmsg() succeeds and returns 0, which
> > >> qio_channel_socket_writev() mistakes for failure and reports as
> > >> QIO_CHANNEL_ERR_BLOCK with errp left unset. The handshake treats every
> > >> negative return as fatal and hands that NULL Error to
> > >> error_get_pretty(). Patches 1 to 3 close the three links.
> > >>
> > >> Patch 5 is the same NULL Error on the read side of the handshake, where
> > >> ERR_BLOCK is folded into -1. It is reachable for a wss:// client, whose
> > >> master channel is then a TLS channel: a wakeup carrying only part of a
> > >> record makes gnutls report EAGAIN.
> > >>
> > >> The tests drive the handshake through a channel which reports ERR_BLOCK
> > >> on demand, covering both directions. No test reproduces the original
> > >> crash itself, which turns on a stale errno and is not reliably
> > >> reproducible in a unit test. What they pin is that a 400 is emitted and
> > >> that ERR_BLOCK no longer reaches error_get_pretty().
> > >>
> > >> Signed-off-by: Denis V. Lunev <den@openvz.org>
> > >> CC: Daniel P. Berrangé <berrange@redhat.com>
> > >> CC: Marc-André Lureau <marcandre.lureau@redhat.com>
> > >>
> > >> Denis V. Lunev (6):
> > >> io/channel-socket: do not treat a zero length write as an error
> > >> io/channel-websock: send an HTTP 400 when the greeting has no space
> > >> io/channel-websock: handle a blocked write during the handshake
> > >> tests/unit: add websock handshake test
> > >> io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
> > >> tests/unit: cover blocked IO during the websock handshake
> > >>
> > >> io/channel-socket.c | 2 +-
> > >> io/channel-websock.c | 10 +-
> > >> tests/unit/meson.build | 1 +
> > >> tests/unit/test-io-channel-websock.c | 249 +++++++++++++++++++++++++++
> > >> 4 files changed, 260 insertions(+), 2 deletions(-)
> > >> create mode 100644 tests/unit/test-io-channel-websock.c
> > >>
> > > Patch series lgtm. You cc stable, why didn't you file a CVE?
> > >
> > > Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> > >
> > I have calculated the scope and get around 6.2 which is
> > not looking too impressive to spend a time.
>
> In the context of a VNC server enabling websocket. If we assume the
> VNC server has authentication enabled, including the TLS extension
> VeNCrypt, then the deployment can be said to be protecting against
> a malicious client.
>
> The flaws mean the malicious client can inflict a denial of service
> attack on the VM before getting to the VNC authentication step. IMHO
> that is enough to justify a CVE assignment.
>
> CC'ing Mauro to double check my view & assign a CVE if appropriate
Agreed; a pre-auth VNC crash warrants CVE assignment. This is similar
to https://access.redhat.com/security/cve/cve-2025-11234
Please use CVE-2026-84788 for this one.
Thanks,
> With regards,
> Daniel
> --
> |: https://berrange.com ~~ https://hachyderm.io/@berrange :|
> |: https://libvirt.org ~~ https://entangle-photo.org :|
> |: https://pixelfed.art/berrange ~~ https://fstop138.berrange.com :|
>
--
Mauro Matteo Cascella
Red Hat Product Security
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-09-02 10:20 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 10:01 [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake Denis V. Lunev
2026-08-31 10:01 ` [PATCH 1/6] io/channel-socket: do not treat a zero length write as an error Denis V. Lunev
2026-09-01 17:31 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space Denis V. Lunev
2026-09-01 17:23 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 3/6] io/channel-websock: handle a blocked write during the handshake Denis V. Lunev
2026-09-01 17:24 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 4/6] tests/unit: add websock handshake test Denis V. Lunev
2026-09-01 17:25 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading Denis V. Lunev
2026-09-01 17:26 ` Daniel P. Berrangé
2026-08-31 10:01 ` [PATCH 6/6] tests/unit: cover blocked IO during the websock handshake Denis V. Lunev
2026-09-01 17:30 ` Daniel P. Berrangé
2026-08-31 11:29 ` [PATCH 0/6] io/channel-websock: fix an unauthenticated crash in the handshake marcandre.lureau
2026-08-31 11:33 ` Denis V. Lunev
2026-09-01 17:54 ` Daniel P. Berrangé
2026-09-02 10:19 ` Mauro Matteo Cascella
2026-09-01 17:43 ` Daniel P. Berrangé
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.