diff --git a/ChangeLog.md b/ChangeLog.md index c593b7a9e7869..fed1e966e9a72 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -30,6 +30,12 @@ See docs/process.md for more on how version tagging works. level- and edge-triggered modes, `EPOLLONESHOT`, `EPOLLEXCLUSIVE`, `EPOLLRDHUP`, nesting, and blocking waits under `PROXY_TO_PTHREAD`, `ASYNCIFY`, and `JSPI`. (#27207) +- Blocking `accept`, `accept4`, `recv`, `recvfrom` and `recvmsg` on sockets + are now supported under `-pthread` on secondary threads (including `main()` + under `PROXY_TO_PTHREAD`): instead of returning `EAGAIN`, a would-block call + on a blocking socket parks the calling thread until ready. On the main + browser thread, which cannot block, `EAGAIN` still surfaces. `accept4` also + now honors `SOCK_NONBLOCK` on the accepted socket. (#27342) - The deprecated `LEGALIZE_JS_FFI` setting was completely removed and moved to legacy settings. This behaviour of lowering away i64 values at the Wasm bounary is still used when `WASM_BIGINT` is disabled. However 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..cbcf3b72c794d 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'], @@ -716,6 +723,43 @@ var SyscallsLibrary = { __syscall_poll_nonblocking: (fds, nfds) => { return doPollSync(fds, nfds); }, + // The single wait primitive behind blocking socket data ops. The data + // syscalls themselves are strictly synchronous (single attempt, -EAGAIN when + // they would block); libc's blocking wrappers (compiled only into the -mt + // libc) call this on EAGAIN with a blocking fd and then retry. It blocks only + // on a proxied pthread worker: the sync-proxy completes - ending the worker's + // futex wait - when the returned promise resolves. In every other context + // (including the event-loop thread, which cannot block) it fails with + // -EAGAIN. Resolves 0 once `fd` reports one of `events` (POLL* flags; + // error/hangup/close always wake). Single-threaded builds use epoll instead. +#if !PTHREADS + // Without pthreads the body is just `return -EAGAIN`, which cannot throw; + // skip the syscall try/catch wrapper so closure doesn't flag it as dead. + _emscripten_fd_wait__nothrow: true, +#endif + _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) { + // Must resolve through a Promise: the caller's sync-proxy awaits a + // thenable (PROXY_SYNC_ASYNC), even when already ready. + return new Promise((resolve) => { + if (pollOne(fd, events)) return resolve(0); + var stream = FS.getStream(fd); + if (!stream) return resolve(0); // closed: let the retry surface EBADF + var reg = stream.node.addListener(() => { + if (pollOne(fd, events)) { + reg.listeners.delete(reg.entry); + resolve(0); + } + }); + }); + } +#endif + return -{{{ cDefs.EAGAIN }}}; + }, // epoll: the entry points live here (like every other syscall); the heavy // lifting is in libepoll.js, which they call after resolving the epoll stream. __syscall_epoll_create1__deps: ['$epollNewInstance'], diff --git a/system/lib/libc/emscripten_internal.h b/system/lib/libc/emscripten_internal.h index 55ca0fa09dc2a..a4660ec7d0ed0 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); +// Blocking wait for fd readiness; see _emscripten_fd_wait in libsyscall.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..5ee0a0c7b627c --- /dev/null +++ b/system/lib/libc/musl/src/internal/emscripten_fd_wait.h @@ -0,0 +1,42 @@ +#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. For a +// blocking fd the network wrappers wait for readiness via the single blocking +// primitive _emscripten_fd_wait and retry. This is a pthreads-only facility +// (the retry loops compile only into the -mt libc): _emscripten_fd_wait blocks +// by parking a proxied worker on its sync-proxy. Where no stack can wait (the +// event-loop thread itself), the wait fails and the EAGAIN surfaces unchanged. +// Single-threaded JSPI/ASYNCIFY builds use epoll for readiness instead. + +#include +#include +#include +#include "syscall.h" + +int _emscripten_fd_wait(int fd, int events); + +static inline int __emscripten_sock_can_wait(int fd, int dontwait) +{ + if (dontwait) return 0; + int fl = __syscall(SYS_fcntl64, fd, F_GETFL); + return fl >= 0 && !(fl & O_NONBLOCK); +} + +// The blocking-socket retry convention: `attempt` is a strictly synchronous +// __socketcall_cp expression returning -EAGAIN when it would block. On EAGAIN +// with a blocking fd (and no MSG_DONTWAIT), wait for readiness and retry. If +// the wait itself fails (no thread to park on), the EAGAIN surfaces unchanged. +// Yields the raw syscall result; callers apply __syscall_ret. +#define __emscripten_sock_retry_cp(fd, dontwait, attempt) ({ \ + long __r; \ + for (;;) { \ + __r = (attempt); \ + if (__r != -EAGAIN || !__emscripten_sock_can_wait(fd, 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..addcccb743b27 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_PTHREADS__ +#include "emscripten_fd_wait.h" +#endif int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict len) { +#ifdef __EMSCRIPTEN_PTHREADS__ + return __syscall_ret(__emscripten_sock_retry_cp(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..85cd2e7aac7c4 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_PTHREADS__ +#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_PTHREADS__ + int ret = __syscall_ret(__emscripten_sock_retry_cp(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..c12c6485a40aa 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_PTHREADS__ +#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_PTHREADS__ + return __syscall_ret(__emscripten_sock_retry_cp(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..c89ad8bce0ef9 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_PTHREADS__ +#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_PTHREADS__ + r = __syscall_ret(__emscripten_sock_retry_cp(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/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 5b126e30017d7..05ca646ee89ff 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.js": 270649, "a.out.nodebug.wasm": 588325, - "total": 858893, + "total": 858974, "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", 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..dbd4216f3878e --- /dev/null +++ b/test/sockets/test_tcp_blocking.c @@ -0,0 +1,73 @@ +/* + * 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 ping/pong exercising the _emscripten_fd_wait primitive: + * a *blocking* accept() and a *blocking* recv() that each have to suspend. The + * client connects from a separate thread after a delay, 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; 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 + +static struct sockaddr_in server_addr; + +static void* client_thread(void* arg) { + usleep(100000); // let the server block in accept() first + int fd = socket(AF_INET, SOCK_STREAM, 0); + assert(fd >= 0); + assert(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0); + assert(send(fd, "ping", 4, 0) == 4); // buffered, never blocks + char buf[4]; + assert(recv(fd, buf, sizeof(buf), 0) == 4 && memcmp(buf, "pong", 4) == 0); + close(fd); + return NULL; +} + +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); + + pthread_t t; + assert(pthread_create(&t, NULL, client_thread, NULL) == 0); + + // Blocking accept(): no connection is pending yet (the client sleeps first), + // so it suspends the proxied worker 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); + + assert(pthread_join(t, NULL) == 0); + 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..07871fdfaaaad 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -218,6 +218,22 @@ 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. This is a pthreads-only + # facility (the retry loops compile only into the -mt libc), so it runs + # under PROXY_TO_PTHREAD, where each blocking call parks its proxied worker. + self.do_runf('sockets/test_tcp_blocking.c', 'done\n', + cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-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.