diff --git a/ChangeLog.md b/ChangeLog.md index c593b7a9e7869..b4bb47697cae1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,13 @@ See docs/process.md for more on how version tagging works. 6.0.9 (in development) ---------------------- +- Blocking `accept`, `accept4`, `recv`, `recvfrom` and `recvmsg` on sockets + are now supported wherever the calling stack can suspend: on secondary + threads under `-pthread` (including `main()` under `PROXY_TO_PTHREAD`), and + under `ASYNCIFY` and `JSPI`. Instead of returning `EAGAIN`, a would-block call + on a blocking socket waits until ready. On a main browser thread that cannot + suspend, `EAGAIN` still surfaces. `accept4` also now honors `SOCK_NONBLOCK` + on the accepted socket. (#27342) 6.0.8 - 08/20/26 ---------------- diff --git a/src/lib/libfs.js b/src/lib/libfs.js index 4907180698353..c6e2df2ec975e 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -1963,6 +1963,41 @@ FS.staticInit();`; }, }, + // The wait primitive behind blocking socket data ops. The data syscalls are + // strictly synchronous (single attempt, -EAGAIN when they would block); on + // EAGAIN libc's wrappers call this and retry when it returns true. Returns + // false when `fd` is non-blocking or there is no stack to suspend (the + // event-loop thread outside ASYNCIFY/JSPI), so the EAGAIN surfaces. Otherwise + // waits until `fd` reports one of `events` (POLL* flags; error/hangup/close + // always wake): a proxied pthread worker parks on its sync-proxy, an + // ASYNCIFY/JSPI stack suspends. + _emscripten_fd_wait__proxy: 'sync', + _emscripten_fd_wait__async: 'auto', + _emscripten_fd_wait__deps: ['$FS', '$pollOne'], + _emscripten_fd_wait: (fd, events) => { +#if PTHREADS + if (!PThread.currentProxiedOperationCallerThread) return 0; +#elif !ASYNCIFY + return 0; +#endif +#if PTHREADS || ASYNCIFY + // Always a Promise from here: a proxied caller's sync-proxy awaits a + // thenable. + var stream = FS.getStream(fd); + if (!stream) return Promise.resolve(1); // closed: let the retry surface EBADF + if (stream.flags & {{{ cDefs.O_NONBLOCK }}}) return Promise.resolve(0); + return new Promise((resolve) => { + if (pollOne(fd, events)) return resolve(1); + var reg = stream.node.addListener(() => { + if (pollOne(fd, events)) { + reg.listeners.delete(reg.entry); + resolve(1); + } + }); + }); +#endif + }, + $FS_mkdirTree__docs: ` /** * @param {number=} mode Optionally, the mode to create in. Uses mkdir's diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 89e5cba52aa9b..4fc8158476bd4 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -330,6 +330,7 @@ sigs = { _emscripten_create_wasm_worker__sig: 'iipip', _emscripten_dlopen_js__sig: 'vpppp', _emscripten_dlsync_threads__sig: 'v', + _emscripten_fd_wait__sig: 'iii', _emscripten_fetch_get_response_headers__sig: 'pipp', _emscripten_fetch_get_response_headers_length__sig: 'pi', _emscripten_fs_load_embedded_files__sig: 'vp', diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 34e65d3435c1f..7b021bd4676b3 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -420,6 +420,13 @@ var SyscallsLibrary = { assert(!errno); #endif } + // Honor SOCK_NONBLOCK on the accepted fd (SOCK_CLOEXEC is a no-op for a + // single process, matching F_SETFD). Without this the new fd only inherits + // the listener's flags, so a SOCK_NONBLOCK accept off a blocking listener + // would wrongly yield a blocking socket. + if (flags & {{{ cDefs.SOCK_NONBLOCK }}}) { + newsock.stream.flags |= {{{ cDefs.O_NONBLOCK }}}; + } return newsock.stream.fd; }, __syscall_bind__deps: ['$getSocketFromFD', '$getSocketAddress'], diff --git a/system/lib/libc/emscripten_internal.h b/system/lib/libc/emscripten_internal.h index 55ca0fa09dc2a..e6ac50584708c 100644 --- a/system/lib/libc/emscripten_internal.h +++ b/system/lib/libc/emscripten_internal.h @@ -102,6 +102,9 @@ void* _dlsym_catchup_js(struct dso* handle, int sym_index); int _setitimer_js(int which, double timeout); +// Wait for fd readiness if the calling stack can suspend; see libfs.js. +int _emscripten_fd_wait(int fd, int events); + // Synchronize loaded modules across threads. // Runs _emscripten_dlsync_self on each of the threads that are running at // the time of the call. diff --git a/system/lib/libc/musl/src/internal/emscripten_fd_wait.h b/system/lib/libc/musl/src/internal/emscripten_fd_wait.h new file mode 100644 index 0000000000000..40643f736531d --- /dev/null +++ b/system/lib/libc/musl/src/internal/emscripten_fd_wait.h @@ -0,0 +1,36 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#ifndef EMSCRIPTEN_FD_WAIT_H +#define EMSCRIPTEN_FD_WAIT_H + +// Blocking socket data ops on emscripten: the underlying JS syscalls are +// strictly synchronous and return -EAGAIN when they would block. The network +// wrappers then wait for readiness via _emscripten_fd_wait and retry. The wait +// itself only succeeds where a stack can be suspended - a proxied pthread +// worker parked on its sync-proxy, or an ASYNCIFY/JSPI stack - and only for a +// blocking fd; otherwise it returns false and the EAGAIN surfaces unchanged. + +#include +#include +#include "emscripten_internal.h" +#include "syscall.h" + +// `attempt` is a strictly synchronous __socketcall_cp expression returning +// -EAGAIN when it would block; `dontwait` (MSG_DONTWAIT) suppresses the wait. +// Yields the raw syscall result; callers apply __syscall_ret. +#define __emscripten_sock_retry(fd, dontwait, attempt) ({ \ + long __r; \ + for (;;) { \ + __r = (attempt); \ + if (__r != -EAGAIN || (dontwait) || !_emscripten_fd_wait(fd, POLLIN)) \ + break; \ + } \ + __r; \ +}) + +#endif diff --git a/system/lib/libc/musl/src/network/accept.c b/system/lib/libc/musl/src/network/accept.c index a92406fa7315c..c61638fed8cd5 100644 --- a/system/lib/libc/musl/src/network/accept.c +++ b/system/lib/libc/musl/src/network/accept.c @@ -1,7 +1,15 @@ #include #include "syscall.h" +#ifdef __EMSCRIPTEN__ +#include "emscripten_fd_wait.h" +#endif int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict len) { +#ifdef __EMSCRIPTEN__ + return __syscall_ret(__emscripten_sock_retry(fd, 0, + __socketcall_cp(accept, fd, addr, len, 0, 0, 0))); +#else return socketcall_cp(accept, fd, addr, len, 0, 0, 0); +#endif } diff --git a/system/lib/libc/musl/src/network/accept4.c b/system/lib/libc/musl/src/network/accept4.c index 765a38edc37d8..e68f4eecb6ff6 100644 --- a/system/lib/libc/musl/src/network/accept4.c +++ b/system/lib/libc/musl/src/network/accept4.c @@ -3,11 +3,19 @@ #include #include #include "syscall.h" +#ifdef __EMSCRIPTEN__ +#include "emscripten_fd_wait.h" +#endif int accept4(int fd, struct sockaddr *restrict addr, socklen_t *restrict len, int flg) { if (!flg) return accept(fd, addr, len); +#ifdef __EMSCRIPTEN__ + int ret = __syscall_ret(__emscripten_sock_retry(fd, 0, + __socketcall_cp(accept4, fd, addr, len, flg, 0, 0))); +#else int ret = socketcall_cp(accept4, fd, addr, len, flg, 0, 0); +#endif if (ret>=0 || (errno != ENOSYS && errno != EINVAL)) return ret; if (flg & ~(SOCK_CLOEXEC|SOCK_NONBLOCK)) { errno = EINVAL; diff --git a/system/lib/libc/musl/src/network/recvfrom.c b/system/lib/libc/musl/src/network/recvfrom.c index 61911663e0868..83a1d2795e384 100644 --- a/system/lib/libc/musl/src/network/recvfrom.c +++ b/system/lib/libc/musl/src/network/recvfrom.c @@ -1,7 +1,15 @@ #include #include "syscall.h" +#ifdef __EMSCRIPTEN__ +#include "emscripten_fd_wait.h" +#endif ssize_t recvfrom(int fd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict alen) { +#ifdef __EMSCRIPTEN__ + return __syscall_ret(__emscripten_sock_retry(fd, flags & MSG_DONTWAIT, + __socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen))); +#else return socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen); +#endif } diff --git a/system/lib/libc/musl/src/network/recvmsg.c b/system/lib/libc/musl/src/network/recvmsg.c index a973763a85a2e..34f5f783d5c91 100644 --- a/system/lib/libc/musl/src/network/recvmsg.c +++ b/system/lib/libc/musl/src/network/recvmsg.c @@ -4,6 +4,9 @@ #include #include #include "syscall.h" +#ifdef __EMSCRIPTEN__ +#include "emscripten_fd_wait.h" +#endif hidden void __convert_scm_timestamps(struct msghdr *, socklen_t); @@ -59,7 +62,12 @@ ssize_t recvmsg(int fd, struct msghdr *msg, int flags) msg = &h; } #endif +#ifdef __EMSCRIPTEN__ + r = __syscall_ret(__emscripten_sock_retry(fd, flags & MSG_DONTWAIT, + __socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0))); +#else r = socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0); +#endif if (r >= 0) __convert_scm_timestamps(msg, orig_controllen); #if LONG_MAX > INT_MAX && !defined(__EMSCRIPTEN__) if (orig) *orig = h; diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 8d098b12d2213..46c20baa537ce 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -1732,6 +1732,10 @@ int _munmap_js( // Stubs (at least for now) +// Socket readiness wait used by libc's blocking socket wrappers; WASMFS has no +// socket support, so there is never anything to wait for. +int _emscripten_fd_wait(int fd, int events) { return 0; } + int __syscall_accept4(int sockfd, struct sockaddr* addr, socklen_t* len, diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index b1aad64974b3a..84fbe19f480cd 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 270568, - "a.out.nodebug.wasm": 588318, - "total": 858886, + "a.out.js": 270648, + "a.out.nodebug.wasm": 588458, + "total": 859106, "sent": [ "IMG_Init", "IMG_Load", @@ -287,6 +287,7 @@ "_dlsym_catchup_js", "_dlsym_js", "_emscripten_dlopen_js", + "_emscripten_fd_wait", "_emscripten_fs_load_embedded_files", "_emscripten_get_last_devicemotion_event", "_emscripten_get_last_deviceorientation_event", @@ -1818,6 +1819,7 @@ "env._dlopen_js", "env._dlsym_js", "env._emscripten_dlopen_js", + "env._emscripten_fd_wait", "env._emscripten_get_last_devicemotion_event", "env._emscripten_get_last_deviceorientation_event", "env._emscripten_get_last_mouse_event", diff --git a/test/sockets/test_tcp_accept_nonblock.c b/test/sockets/test_tcp_accept_nonblock.c new file mode 100644 index 0000000000000..2d0438c2bdab9 --- /dev/null +++ b/test/sockets/test_tcp_accept_nonblock.c @@ -0,0 +1,98 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * accept4(SOCK_NONBLOCK) must yield a non-blocking accepted socket even off a + * *blocking* listener: the flag is applied on top of the flags inherited from + * the listener, not dropped. A poll()-driven main loop (single-threaded, zero + * timeout) waits for the incoming connection, accept4()s it with SOCK_NONBLOCK, + * then checks F_GETFL reports O_NONBLOCK and that a data-less recv() would-block + * with EAGAIN rather than hanging. Plain POSIX, so it also runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +static int listen_fd = -1; +static int client_fd = -1; +static int peer_fd = -1; + +static void finish(void) { + if (client_fd >= 0) close(client_fd); + if (peer_fd >= 0) close(peer_fd); + if (listen_fd >= 0) close(listen_fd); + printf("done\n"); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); +#endif +} + +static void main_loop(void) { + struct pollfd pfd = { .fd = listen_fd, .events = POLLIN }; + if (poll(&pfd, 1, 0) <= 0 || !(pfd.revents & POLLIN)) { + return; // no connection queued yet + } + + // The listener is blocking (never marked O_NONBLOCK), so inheritance alone + // would give a blocking socket; SOCK_NONBLOCK must override that. + peer_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK); + assert(peer_fd >= 0); + + int fl = fcntl(peer_fd, F_GETFL); + assert(fl >= 0 && (fl & O_NONBLOCK) && "accept4 SOCK_NONBLOCK not honored"); + + // A non-blocking recv with no data pending returns EAGAIN immediately instead + // of blocking, confirming the fd is really non-blocking. + char buf[4]; + ssize_t n = recv(peer_fd, buf, sizeof(buf), 0); + assert(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)); + + finish(); +} + +int main(void) { + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + assert(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); + socklen_t l = sizeof(addr); + assert(getsockname(listen_fd, (struct sockaddr*)&addr, &l) == 0); + assert(listen(listen_fd, 4) == 0); + // Deliberately leave listen_fd blocking to prove SOCK_NONBLOCK is applied on + // top of the inherited (blocking) listener flags. + + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + fcntl(client_fd, F_SETFL, O_NONBLOCK); + int r = connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)); + assert(r == 0 || errno == EINPROGRESS); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (peer_fd < 0) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_blocking.c b/test/sockets/test_tcp_blocking.c new file mode 100644 index 0000000000000..6432eaa8d9251 --- /dev/null +++ b/test/sockets/test_tcp_blocking.c @@ -0,0 +1,104 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Blocking TCP loopback exercising the _emscripten_fd_wait primitive: a + * *blocking* accept() and a *blocking* recv() that each have to suspend. The + * client connects on a delay (from a separate thread under -pthread, or a timer + * under JSPI), so the server's accept and recv both would-block first and can + * only complete by being woken through the inode readiness wait-queue (the + * SOCKFS.emit bridge). Under PROXY_TO_PTHREAD every blocking call parks its + * proxied worker on the sync-proxy; under JSPI the calling stack suspends. In + * both cases the main-thread event loop drives node's sockets and delivers the + * wakes. send()/write() never block (node buffers), so only the read side waits. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN_PTHREADS__ +#include +#endif + +static struct sockaddr_in server_addr; +static int client_fd; + +static void client_connect(void* arg) { + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); +#ifndef __EMSCRIPTEN_PTHREADS__ + // Runs on the event-loop thread while main() is suspended, so it must not + // block itself: connect asynchronously, node buffers the send until open. + fcntl(client_fd, F_SETFL, O_NONBLOCK); + int r = connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); + assert(r == 0 || errno == EINPROGRESS); +#else + assert(connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0); +#endif + assert(send(client_fd, "ping", 4, 0) == 4); // buffered, never blocks +} + +#ifdef __EMSCRIPTEN_PTHREADS__ +static void* client_thread(void* arg) { + usleep(100000); // let the server block in accept() first + client_connect(NULL); + char buf[4]; + assert(recv(client_fd, buf, sizeof(buf), 0) == 4 && memcmp(buf, "pong", 4) == 0); + close(client_fd); + return NULL; +} +#endif + +int main(void) { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); + assert(bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0); + socklen_t l = sizeof(server_addr); + assert(getsockname(listen_fd, (struct sockaddr*)&server_addr, &l) == 0); + assert(listen(listen_fd, 4) == 0); + +#ifdef __EMSCRIPTEN_PTHREADS__ + // Under PROXY_TO_PTHREAD main() runs on a worker that parks in accept(), so + // its event loop can't fire a timer - the wake is a cross-thread notify. + pthread_t t; + assert(pthread_create(&t, NULL, client_thread, NULL) == 0); +#else + emscripten_async_call(client_connect, NULL, 100); +#endif + + // Blocking accept(): no connection is pending yet (the client waits first), + // so it suspends on the listener's readiness queue until the client connects. + struct sockaddr_in ca; + socklen_t cl = sizeof(ca); + int peer_fd = accept(listen_fd, (struct sockaddr*)&ca, &cl); + assert(peer_fd >= 0); + + // Blocking recv(): suspends until the client's "ping" arrives. + char buf[4]; + assert(recv(peer_fd, buf, sizeof(buf), 0) == 4 && memcmp(buf, "ping", 4) == 0); + assert(send(peer_fd, "pong", 4, 0) == 4); + +#ifdef __EMSCRIPTEN_PTHREADS__ + assert(pthread_join(t, NULL) == 0); +#else + close(client_fd); +#endif + close(peer_fd); + close(listen_fd); + printf("done\n"); + return 0; +} diff --git a/test/test_sockets_node.py b/test/test_sockets_node.py index 97750c862641c..16b89409f3fb3 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -218,6 +218,27 @@ def test_noderawsockets_epoll_socket_blocking_jspi(self): self.do_runf('sockets/test_epoll_socket_blocking.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + def test_noderawsockets_tcp_blocking(self): + # Blocking accept() + recv() via the _emscripten_fd_wait primitive: the + # client connects from another thread after a delay so both would-block + # first and can only complete by being woken, with main() proxied to a + # worker so each blocking call can park it. + self.do_runf('sockets/test_tcp_blocking.c', 'done\n', + cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) + + @requires_jspi_node + def test_noderawsockets_tcp_blocking_jspi(self): + # Same, but the blocking accept()/recv() suspend the wasm stack under JSPI. + self.do_runf('sockets/test_tcp_blocking.c', 'done\n', + cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + + def test_noderawsockets_tcp_accept_nonblock(self): + # accept4(SOCK_NONBLOCK) off a blocking listener yields a non-blocking fd + # (the flag is applied on top of the inherited listener flags). Single + # threaded, poll()-driven, so no fd_wait blocking is involved. + self.do_runf('sockets/test_tcp_accept_nonblock.c', 'done\n', + cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + def test_noderawsockets_epoll_rdhup(self): # A blocking epoll_wait reports EPOLLRDHUP when the TCP peer half-closes its # write side (FIN), distinct from a full EPOLLHUP, and only when requested.