Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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
Expand Down
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
44 changes: 44 additions & 0 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down
3 changes: 3 additions & 0 deletions system/lib/libc/emscripten_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions system/lib/libc/musl/src/internal/emscripten_fd_wait.h
Original file line number Diff line number Diff line change
@@ -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 <fcntl.h>
#include <errno.h>
#include <poll.h>
#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
8 changes: 8 additions & 0 deletions system/lib/libc/musl/src/network/accept.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#include <sys/socket.h>
#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
}
8 changes: 8 additions & 0 deletions system/lib/libc/musl/src/network/accept4.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
#include <errno.h>
#include <fcntl.h>
#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;
Expand Down
8 changes: 8 additions & 0 deletions system/lib/libc/musl/src/network/recvfrom.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#include <sys/socket.h>
#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
}
8 changes: 8 additions & 0 deletions system/lib/libc/musl/src/network/recvmsg.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#include <sys/time.h>
#include <string.h>
#include "syscall.h"
#ifdef __EMSCRIPTEN_PTHREADS__
#include "emscripten_fd_wait.h"
#endif

hidden void __convert_scm_timestamps(struct msghdr *, socklen_t);

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 270568,
"a.out.js": 270649,
"a.out.nodebug.wasm": 588316,
"total": 858884,
"total": 858965,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down Expand Up @@ -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",
Expand Down
98 changes: 98 additions & 0 deletions test/sockets/test_tcp_accept_nonblock.c
Original file line number Diff line number Diff line change
@@ -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 <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#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;
}
73 changes: 73 additions & 0 deletions test/sockets/test_tcp_blocking.c
Original file line number Diff line number Diff line change
@@ -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 <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

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;
}
Loading
Loading