[PATCH v2 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. -- v2: ntdll: Clamp the sendmsg() iovec count to IOV_MAX in try_send(). ws2_32/tests: Add a test 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 for a stream socket, 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). Signed-off-by: Ivan Savinov <isavinov@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> --- dlls/ws2_32/tests/sock.c | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index e67e50907b0..22f45e3bd4d 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14616,6 +14616,55 @@ 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; + SOCKET client, server; + WSABUF *bufs; + DWORD sent; + int ret; + + /* 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); + free(bufs); + free(send_data); +} + static void test_valid_handle(void) { HANDLE duplicated, invalid; @@ -15035,6 +15084,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
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. 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 22f45e3bd4d..93d41c1a703 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14643,7 +14643,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
On Sat Jul 4 20:48:15 2026 +0000, Elizabeth Figura wrote:
Meh, I can't read, the rest is supposed to be handled by short write handling. That makes sense. Though it would be nice to have a test that makes sure all the data comes through. Why are we limiting this to TCP? 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). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11316#note_144861
participants (2)
-
Ivan Savinov -
Ivan Savinov (@savinov)