[PATCH 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. -- 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> --- dlls/ws2_32/tests/sock.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index e67e50907b0..ac496bdbfca 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14616,6 +14616,41 @@ static void test_send_buffering(void) closesocket(client); } +static void test_send_many_buffers(void) +{ + static char byte = 'x'; + SOCKET client, server; + unsigned int i, count; + 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; + bufs = malloc(count * sizeof(*bufs)); + for (i = 0; i < count; ++i) + { + bufs[i].buf = &byte; + 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); + + closesocket(client); + closesocket(server); + free(bufs); +} + static void test_valid_handle(void) { HANDLE duplicated, invalid; @@ -15035,6 +15070,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> --- 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 ac496bdbfca..9b23c7424d7 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -14641,7 +14641,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) ok(sent == count, "got %lu.\n", sent); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11316
Simply dropping the extra data doesn't seem like an improvement. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11316#note_144855
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? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11316#note_144856
participants (3)
-
Elizabeth Figura (@zfigura) -
Ivan Savinov -
Ivan Savinov (@savinov)