Subj : Win32: the PREFER_POLL code paths call WSAPoll() in ways it rejects (W To : GitLab issue in main/sbbs From : Rob Swindell Date : Sun Aug 02 2026 20:12:58 reopen https://gitlab.synchro.net/main/sbbs/-/issues/1212 ## Summary `PREFER_POLL` is defined only for non-Win32 builds — `src/build/Common.gmake:272`, added in 663ca552e6 (2021-03-31), which replaced an earlier `#ifdef _WIN32` split. Windows therefore compiles the `select()` half of every `#ifdef PREFER_POLL` in the tree, and the `poll()` half has never been compiled or executed there, even though `src/xpdev/sockwrap.h:187` maps `poll()` → `WSAPoll()` and defines `nfds_t`/`pollfd` for exactly that purpose. This came up because the Win32 `select()` path has a hard 64-socket ceiling (`FD_SETSIZE` defaults to 64 in `winsock2.h`, and nothing in the tree raised it). In `js_internal.cpp`'s JS event loop, `FD_SET()` silently discards every socket past the 64th, and since events are inserted at the head of the list (`js_internal.cpp:784`) while the fill loop walks it forward, the **oldest** event is the first one dropped — for a JS server that's the listening socket registered at startup. Result: a Win32 JS ircd hub with ≥64 sockets stops accepting new connections entirely while every established connection keeps working. That was breaking vert.synchro.net (57 direct server links); the kernel still completes the TCP handshake from the listen backlog, so it presents as a network problem rather than a server bug. Raising `FD_SETSIZE` for the MSBuild projects fixes that, and is what I'd like to ship for the immediate problem. Switching Windows to `poll()` is the other candidate fix, and is arguably the right long-term answer — but the code isn't ready for it. This issue records what's in the way, so nobody flips the define expecting it to just work. ## Confirmed problems Probed on Windows 11 26200, MSVC 19.44, against the real `WSAPoll()`: **1. Empty descriptor set → `WSAEINVAL`.** `js_internal.cpp:1334` calls `poll(fds, cfd, timeout)` with `cfd == 0` (and `fds == NULL`) whenever a script has only timer events pending — the `sc == 0` case at `js_internal.cpp:1202`. On \*nix that's a portable sleep. On Windows it fails immediately, falls into the error branch at `js_internal.cpp:1356-1362`, and `JS_ReportError()`s the event loop dead. Any JS using `setTimeout`/`setInterval` without sockets would break. **2. `POLLPRI` is not supported.** `js_global.cpp:4157` and `js_global.cpp:4182` set `events = POLLPRI` for the except-set of `socket_select()`. `WSAPoll()` rejects it outright rather than degrading. ``` 1. WSAPoll(NULL, 0, 100) = -1 err=10022 <- WSAEINVAL 2. WSAPoll(events=POLLPRI) = -1 err=10022 <- WSAEINVAL 3. WSAPoll(listener, POLLIN) = 1 revents=0x100 <- POLLRDNORM, correct 4. WSAPoll(100 fds) = 1 listener revents=0x100 ``` Cases 3 and 4 are the point of the exercise and they work: a listening socket with a pending connection is correctly reported readable, and >64 descriptors in a single call are fine. The mechanism is sound on Windows; only these two call sites are wrong. ## The documented connect bug did not reproduce `sockwrap.h:183` warns: > NOTE: WSAPoll() has a bug where a non-blocking socket which has connect() called on it that is trying to connect to a closed port will timeout instead of returning a failure, even with POLLOUT specified. Non-blocking `connect()` to a closed loopback port, `WSAPoll()` vs `select()`: ``` WSAPoll(POLLOUT) after 2016 ms: ret=1 revents=0x13 (POLLERR|POLLHUP|POLLOUT) select() after 2016 ms: ret=1 writable=0 except=1 ``` Identical timing, and `WSAPoll()` did report the failure. That is **one** scenario (RST-refused, loopback) on **one** OS build, so it is not evidence the note is obsolete — a dropped-SYN/firewalled destination was not tested, and older Windows versions are still supported. Recording it only so the next person knows the note needs re-verification rather than assuming it holds. ## If someone enables PREFER_POLL on Win32 - Skip the `poll()` call when `nfds == 0` and sleep for the timeout instead. - Give `POLLPRI` a Windows substitute, or let the except-set path degrade. - Re-verify the `sockwrap.h:183` connect behavior on a dropped-SYN destination and on older Windows. - Note the blast radius: defining `PREFER_POLL` project-wide also flips `multisock.c`'s `xpms_accept()` — the accept path for *every* Windows server — plus the `sockwrap` helpers and `js_socket`, not just the JS engine. ## Probe source
wsapoll_test.c — cases 1-4 ```c #include #include #include #pragma comment(lib, "ws2_32.lib") int main(void) { WSADATA wd; WSAPOLLFD pfd; SOCKET lsock, csock; struct sockaddr_in sa; int len, r; u_long nb = 1; WSAStartup(MAKEWORD(2, 2), &wd); /* 1. Empty set - js_internal.cpp calls poll(NULL, 0, timeout) whenever a script has only timer events pending. */ r = WSAPoll(NULL, 0, 100); printf("1. WSAPoll(NULL, 0, 100) = %d err=%d\n", r, WSAGetLastError()); lsock = socket(AF_INET, SOCK_STREAM, 0); memset(&sa, 0, sizeof sa); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sa.sin_port = 0; bind(lsock, (struct sockaddr *)&sa, sizeof sa); listen(lsock, 5); len = sizeof sa; getsockname(lsock, (struct sockaddr *)&sa, &len); /* 2. POLLPRI in events - js_global.cpp:4157,4182 use this for the except-set of socket_select(). */ pfd.fd = lsock; pfd.events = POLLPRI; pfd.revents = 0; r = WSAPoll(&pfd, 1, 100); printf("2. WSAPoll(events=POLLPRI) = %d err=%d revents=0x%x\n", r, WSAGetLastError(), pfd.revents); /* 3. POLLIN on a listening socket with a pending connection - this is what an accept callback needs. */ csock = socket(AF_INET, SOCK_STREAM, 0); ioctlsocket(csock, FIONBIO, &nb); connect(csock, (struct sockaddr *)&sa, sizeof sa); Sleep(200); pfd.fd = lsock; pfd.events = POLLIN; pfd.revents = 0; r = WSAPoll(&pfd, 1, 1000); printf("3. WSAPoll(listener, POLLIN) = %d err=%d revents=0x%x (POLLRDNORM=0x%x)\n", r, WSAGetLastError(), pfd.revents, POLLRDNORM); /* 4. 100 sockets in one call - the case select() cannot express. */ { WSAPOLLFD many[100]; SOCKET pad[100]; int i; for (i = 0; i < 100; i++) { pad[i] = socket(AF_INET, SOCK_DGRAM, 0); memset(&sa, 0, sizeof sa); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sa.sin_port = 0; bind(pad[i], (struct sockaddr *)&sa, sizeof sa); many[i].fd = pad[i]; many[i].events = POLLIN; many[i].revents = 0; } many[99].fd = lsock; /* listener last, still has a pending connect */ r = WSAPoll(many, 100, 1000); printf("4. WSAPoll(100 fds) = %d err=%d listener revents=0x%x\n", r, WSAGetLastError(), many[99].revents); } return 0; } ```
wsapoll_connect.c — the sockwrap.h:183 check ```c #include #include #include #pragma comment(lib, "ws2_32.lib") static SOCKET start_connect(void) { SOCKET s = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in sa; u_long nb = 1; ioctlsocket(s, FIONBIO, &nb); memset(&sa, 0, sizeof sa); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sa.sin_port = htons(1); /* nothing listens here */ connect(s, (struct sockaddr *)&sa, sizeof sa); return s; } int main(void) { WSADATA wd; WSAPOLLFD pfd; SOCKET s; fd_set wfds, efds; struct timeval tv; DWORD t0; int r; WSAStartup(MAKEWORD(2, 2), &wd); s = start_connect(); pfd.fd = s; pfd.events = POLLOUT; pfd.revents = 0; t0 = GetTickCount(); r = WSAPoll(&pfd, 1, 3000); printf("WSAPoll(POLLOUT) after %lu ms: ret=%d err=%d revents=0x%x\n", (unsigned long)(GetTickCount() - t0), r, WSAGetLastError(), pfd.revents); closesocket(s); s = start_connect(); FD_ZERO(&wfds); FD_ZERO(&efds); FD_SET(s, &wfds); FD_SET(s, &efds); tv.tv_sec = 3; tv.tv_usec = 0; t0 = GetTickCount(); r = select(0, NULL, &wfds, &efds, &tv); printf("select() after %lu ms: ret=%d writable=%d except=%d\n", (unsigned long)(GetTickCount() - t0), r, FD_ISSET(s, &wfds) ? 1 : 0, FD_ISSET(s, &efds) ? 1 : 0); closesocket(s); return 0; } ```
— *Authored by Claude (Claude Code), on behalf of @rswindell* --- SBBSecho 3.37-Linux * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705) .