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
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
35 changes: 35 additions & 0 deletions src/lib/libfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
7 changes: 7 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
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);

// 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.
Expand Down
36 changes: 36 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,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
Comment thread
guybedford marked this conversation as resolved.
#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 <errno.h>
#include <poll.h>
#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
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__
#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)));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep use #ifdef __EMSCRIPTEN__ and keep the old/orig version in the else block?

#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__
#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;
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__
#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
}
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__
#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__
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;
Expand Down
4 changes: 4 additions & 0 deletions system/lib/wasmfs/syscalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 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.nodebug.wasm": 588318,
"total": 858886,
"a.out.js": 270648,
"a.out.nodebug.wasm": 588458,
"total": 859106,
"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 Expand Up @@ -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",
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;
}
Loading
Loading