* [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316
@ 2026-08-31 14:06 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 1/6] io/channel-socket: do not treat a zero length write as an error #VSTOR-143316 Denis V. Lunev
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
Backport of the upstream series fixing an unauthenticated crash in the
VNC websocket handshake. A client which can reach a 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 series applies to the branch unchanged, and the new unit test passes
on it.
Upstream posting, reviewed by the graphics maintainer:
https://lore.kernel.org/qemu-devel/20260831100151.914178-1-den@openvz.org/
Signed-off-by: Denis V. Lunev <den@openvz.org>
Denis V. Lunev (6):
io/channel-socket: do not treat a zero length write as an error
#VSTOR-143316
io/channel-websock: send an HTTP 400 when the greeting has no space
#VSTOR-143316
io/channel-websock: handle a blocked write during the handshake
#VSTOR-143316
tests/unit: add websock handshake test #VSTOR-143316
io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
#VSTOR-143316
tests/unit: cover blocked IO during the websock handshake
#VSTOR-143316
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] 8+ messages in thread
* [PATCH hci-8.0 1/6] io/channel-socket: do not treat a zero length write as an error #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space #VSTOR-143316 Denis V. Lunev
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-2-den@openvz.org>
Reviewed-by: 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 f8d7d7d6ecf..094b82b370d 100644
--- a/io/channel-socket.c
+++ b/io/channel-socket.c
@@ -663,7 +663,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 [flat|nested] 8+ messages in thread
* [PATCH hci-8.0 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 1/6] io/channel-socket: do not treat a zero length write as an error #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 3/6] io/channel-websock: handle a blocked write during the handshake #VSTOR-143316 Denis V. Lunev
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-3-den@openvz.org>
Reviewed-by: 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 cb4dafdebb3..806f7ea4935 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 [flat|nested] 8+ messages in thread
* [PATCH hci-8.0 3/6] io/channel-websock: handle a blocked write during the handshake #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 1/6] io/channel-socket: do not treat a zero length write as an error #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 4/6] tests/unit: add websock handshake test #VSTOR-143316 Denis V. Lunev
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-4-den@openvz.org>
Reviewed-by: 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 806f7ea4935..9c4da02ebff 100644
--- a/io/channel-websock.c
+++ b/io/channel-websock.c
@@ -541,6 +541,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 [flat|nested] 8+ messages in thread
* [PATCH hci-8.0 4/6] tests/unit: add websock handshake test #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
` (2 preceding siblings ...)
2026-08-31 14:06 ` [PATCH hci-8.0 3/6] io/channel-websock: handle a blocked write during the handshake #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading #VSTOR-143316 Denis V. Lunev
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-5-den@openvz.org>
Reviewed-by: 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 afe3ab569c9..64641c6bb43 100644
--- a/tests/unit/meson.build
+++ b/tests/unit/meson.build
@@ -91,6 +91,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 00000000000..2a55a4bcdf8
--- /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 [flat|nested] 8+ messages in thread
* [PATCH hci-8.0 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
` (3 preceding siblings ...)
2026-08-31 14:06 ` [PATCH hci-8.0 4/6] tests/unit: add websock handshake test #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 6/6] tests/unit: cover blocked IO during the websock handshake #VSTOR-143316 Denis V. Lunev
2026-09-01 15:51 ` [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the " Andrey Drobyshev
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-6-den@openvz.org>
Reviewed-by: 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 9c4da02ebff..e3351122a3a 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 [flat|nested] 8+ messages in thread
* [PATCH hci-8.0 6/6] tests/unit: cover blocked IO during the websock handshake #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
` (4 preceding siblings ...)
2026-08-31 14:06 ` [PATCH hci-8.0 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading #VSTOR-143316 Denis V. Lunev
@ 2026-08-31 14:06 ` Denis V. Lunev
2026-09-01 15:51 ` [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the " Andrey Drobyshev
6 siblings, 0 replies; 8+ messages in thread
From: Denis V. Lunev @ 2026-08-31 14:06 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
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>
Message-ID: <20260831100151.914178-7-den@openvz.org>
Reviewed-by: 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 2a55a4bcdf8..88da24f993d 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 [flat|nested] 8+ messages in thread
* Re: [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
` (5 preceding siblings ...)
2026-08-31 14:06 ` [PATCH hci-8.0 6/6] tests/unit: cover blocked IO during the websock handshake #VSTOR-143316 Denis V. Lunev
@ 2026-09-01 15:51 ` Andrey Drobyshev
6 siblings, 0 replies; 8+ messages in thread
From: Andrey Drobyshev @ 2026-09-01 15:51 UTC (permalink / raw)
To: Denis V. Lunev, svt-core
Ack
Applied to hci-8.0
On 8/31/26 5:06 PM, Denis V. Lunev wrote:
> Backport of the upstream series fixing an unauthenticated crash in the
> VNC websocket handshake. A client which can reach a 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 series applies to the branch unchanged, and the new unit test passes
> on it.
>
> Upstream posting, reviewed by the graphics maintainer:
> https://lore.kernel.org/qemu-devel/20260831100151.914178-1-den@openvz.org/
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
>
> Denis V. Lunev (6):
> io/channel-socket: do not treat a zero length write as an error
> #VSTOR-143316
> io/channel-websock: send an HTTP 400 when the greeting has no space
> #VSTOR-143316
> io/channel-websock: handle a blocked write during the handshake
> #VSTOR-143316
> tests/unit: add websock handshake test #VSTOR-143316
> io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading
> #VSTOR-143316
> tests/unit: cover blocked IO during the websock handshake
> #VSTOR-143316
>
> 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
>
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-09-01 15:51 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-31 14:06 [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the handshake #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 1/6] io/channel-socket: do not treat a zero length write as an error #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 2/6] io/channel-websock: send an HTTP 400 when the greeting has no space #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 3/6] io/channel-websock: handle a blocked write during the handshake #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 4/6] tests/unit: add websock handshake test #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 5/6] io/channel-websock: do not lose QIO_CHANNEL_ERR_BLOCK while reading #VSTOR-143316 Denis V. Lunev
2026-08-31 14:06 ` [PATCH hci-8.0 6/6] tests/unit: cover blocked IO during the websock handshake #VSTOR-143316 Denis V. Lunev
2026-09-01 15:51 ` [PATCH hci-8.0 0/6] io/channel-websock: fix an unauthenticated crash in the " Andrey Drobyshev
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.