[PATCH v3 0/2] MR11316: ntdll: Clamp the sendmsg() iovec count to IOV_MAX in try_send().
Linux sendmsg() rejects msg_iovlen > IOV_MAX (1024) with EMSGSIZE, but native WSASend() imposes no practical limit on the number of WSABUF elements for a stream socket. Wine forwards the whole WSABUF array to a single sendmsg() call, so any WSASend() with more than 1024 buffers fails with an unexpected WSAEMSGSIZE. Real-world impact: a gRPC/C++ server (grpc 1.71.0) running under Wine drops its TCP connection under a storm of concurrent unary calls. gRPC's chttp2 transport puts each HTTP/2 frame slice into its own WSABUF and flushes the whole outbound buffer with one WSASend() — under load the batch easily exceeds 1024 slices (observed batches of 20000+). The failed send is treated as a fatal transport error and the connection is closed, failing every in-flight call with `Unavailable: error reading from server: EOF`. The fix clamps msg_iovlen to IOV_MAX for stream sockets in try_send() and lets the existing short-write handling (iov_cursor) resend the remaining buffers in a subsequent sendmsg() call. Datagram sockets are intentionally left untouched, since splitting a datagram would change semantics. The first commit adds a conformance test (WSASend() with 2000 one-byte WSABUFs on a TCP socketpair), wrapped in todo_wine; the second commit implements the clamp and removes the todo_wine. The test passes on native Windows. Verification, beyond the conformance test: * Minimal repro (plain Winsock, no gRPC): WSASend() with 1024 buffers passes, 1025 fails with WSAEMSGSIZE on every unpatched Wine tried (10.0, 10.9, 10.10, 11.0); passes with the patch. * Rebuilt ntdll.so from patched source and dropped it into a stock distribution Wine installation (both wine-10.0 and wine-11.0): the previously failing gRPC server now survives storms of 12000 concurrent calls across repeated rounds, with WSASend() batches observed up to ~6000 WSABUFs and zero EMSGSIZE occurrences (WINEDEBUG=+winsock). Without the patch the very first batch above 1024 buffers kills the connection. -- v3: ntdll: Clamp the sendmsg() iovec count to IOV_MAX in try_send(). ws2_32/tests: Add tests for WSASend() with more than IOV_MAX buffers. https://gitlab.winehq.org/wine/wine/-/merge_requests/11316
From: Ivan Savinov <isavinov@gmail.com> Native WSASend() imposes no practical limit on the number of WSABUF elements, unlike Wine, which forwards the whole array to a single sendmsg() call. This currently fails once the array exceeds IOV_MAX (1024 elements on Linux). For datagram sockets the buffers are coalesced into a single datagram. Signed-off-by: Ivan Savinov <isavinov@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> --- dlls/ws2_32/tests/sock.c | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index e67e50907b0..b6d5bcefdad 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14616,6 +14616,88 @@ static void test_send_buffering(void) closesocket(client); } +static void test_send_many_buffers(void) +{ + char *send_data, *recv_data; + unsigned int i, count, recv_size; + struct sockaddr_in addr; + SOCKET client, server; + int addrlen, ret; + WSABUF *bufs; + DWORD sent; + + /* Native WSASend() imposes no practical limit on the number of WSABUF + * elements for a stream socket. Wine used to forward the whole array to + * a single sendmsg() call, which Linux rejects with EMSGSIZE once + * msg_iovlen exceeds IOV_MAX (1024 on Linux). */ + count = 2000; + send_data = malloc(count); + bufs = malloc(count * sizeof(*bufs)); + for (i = 0; i < count; ++i) + { + send_data[i] = i; + bufs[i].buf = &send_data[i]; + bufs[i].len = 1; + } + + tcp_socketpair(&client, &server); + + sent = 0xdeadbeef; + ret = WSASend(client, bufs, count, &sent, 0, NULL, NULL); + todo_wine + ok(!ret, "got %d, error %u.\n", ret, WSAGetLastError()); + if (!ret) + { + ok(sent == count, "got %lu.\n", sent); + + recv_data = calloc(1, count); + recv_size = 0; + while (recv_size < count + && (ret = recv(server, recv_data + recv_size, count - recv_size, 0)) > 0) + recv_size += ret; + ok(recv_size == count, "got %u, expected %u.\n", recv_size, count); + ok(!memcmp(recv_data, send_data, count), "data mismatch.\n"); + free(recv_data); + } + + closesocket(client); + closesocket(server); + + /* Native WSASendTo() has no such limit for datagram sockets either; the + * buffers are coalesced into a single datagram. */ + client = socket(AF_INET, SOCK_DGRAM, 0); + server = socket(AF_INET, SOCK_DGRAM, 0); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ret = bind(server, (struct sockaddr *)&addr, sizeof(addr)); + ok(!ret, "got error %u.\n", WSAGetLastError()); + addrlen = sizeof(addr); + ret = getsockname(server, (struct sockaddr *)&addr, &addrlen); + ok(!ret, "got error %u.\n", WSAGetLastError()); + + sent = 0xdeadbeef; + ret = WSASendTo(client, bufs, count, &sent, 0, (struct sockaddr *)&addr, sizeof(addr), NULL, NULL); + todo_wine + ok(!ret, "got %d, error %u.\n", ret, WSAGetLastError()); + if (!ret) + { + ok(sent == count, "got %lu.\n", sent); + + recv_data = calloc(1, count); + ret = recv(server, recv_data, count, 0); + ok(ret == (int)count, "got %d, error %u.\n", ret, WSAGetLastError()); + ok(!memcmp(recv_data, send_data, count), "data mismatch.\n"); + free(recv_data); + } + + closesocket(client); + closesocket(server); + free(bufs); + free(send_data); +} + static void test_valid_handle(void) { HANDLE duplicated, invalid; @@ -15035,6 +15117,7 @@ START_TEST( sock ) test_tcp_sendto_recvfrom(); test_broadcast(); test_send_buffering(); + test_send_many_buffers(); test_valid_handle(); test_afunix(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11316
On Sat Jul 4 20:48:15 2026 +0000, Ivan Savinov wrote:
Added receive-side verification: the test now reads everything back on the peer socket and checks both the total size and the byte content (each buffer carries a distinct byte value, so reordering or dropped tails would be caught). Regarding limiting this to stream sockets: for SOCK_STREAM a short send is transparent — the clamped tail is resent by the short-write handling and the byte stream on the wire is unchanged. For message-oriented sockets each sendmsg() produces one datagram, so clamping the iovec count would split a single datagram into several, changing what appears on the wire. Supporting >IOV_MAX buffers for datagrams would instead require coalescing the payload into a temporary buffer, which seemed better left to a separate change if there's demand — the real-world breakage here was on stream sockets (gRPC's HTTP/2 transport batching one WSABUF per frame slice). Added a todo_wine test documenting the datagram case as well: native WSASendTo() with 2000 one-byte WSABUFs sends a single 2000-byte datagram, while Wine fails with WSAEMSGSIZE — the clamp deliberately doesn't apply to message-oriented sockets, since it would split the datagram. A proper fix there would require coalescing the payload into a temporary buffer, which I'd prefer to leave for a separate change.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11316#note_144883
From: Ivan Savinov <isavinov@gmail.com> Linux sendmsg() rejects a msg_iovlen greater than IOV_MAX (1024) with EMSGSIZE, but native WSASend() has no such restriction for stream sockets. Wine currently forwards the whole WSABUF array unmodified, so any application issuing a WSASend() with more buffers than that gets an unexpected WSAEMSGSIZE and typically tears down the connection (observed with a gRPC/C++ server batching HTTP/2 frame slices into a single WSASend()). Clamp msg_iovlen to IOV_MAX for SOCK_STREAM sockets and let the existing short write handling resend the remaining buffers in a subsequent sendmsg() call. Datagram sockets are left alone, since clamping would split the datagram; supporting more than IOV_MAX buffers there would require coalescing the payload instead. Signed-off-by: Ivan Savinov <isavinov@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> --- dlls/ntdll/unix/socket.c | 5 +++++ dlls/ws2_32/tests/sock.c | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dlls/ntdll/unix/socket.c b/dlls/ntdll/unix/socket.c index ab301f13949..529f9b689f1 100644 --- a/dlls/ntdll/unix/socket.c +++ b/dlls/ntdll/unix/socket.c @@ -25,6 +25,7 @@ #include "config.h" #include <assert.h> #include <errno.h> +#include <limits.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> @@ -1087,6 +1088,10 @@ static NTSTATUS try_send( int fd, struct async_send_ioctl *async ) hdr.msg_iov = async->iov + async->iov_cursor; hdr.msg_iovlen = async->count - async->iov_cursor; + /* Linux sendmsg() rejects msg_iovlen > IOV_MAX with EMSGSIZE, unlike native + * WSASend(), which imposes no such limit. Clamp for stream sockets and let + * the short write handling below resend the remaining buffers. */ + if (sock_type == SOCK_STREAM && hdr.msg_iovlen > IOV_MAX) hdr.msg_iovlen = IOV_MAX; while ((ret = sendmsg( fd, &hdr, async->unix_flags )) == -1) { diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index b6d5bcefdad..27219e93bab 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14644,7 +14644,6 @@ static void test_send_many_buffers(void) sent = 0xdeadbeef; ret = WSASend(client, bufs, count, &sent, 0, NULL, NULL); - todo_wine ok(!ret, "got %d, error %u.\n", ret, WSAGetLastError()); if (!ret) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11316
participants (2)
-
Ivan Savinov -
Ivan Savinov (@savinov)