Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
34 changes: 34 additions & 0 deletions src/lib/libfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,40 @@ 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: a proxied caller's sync-proxy awaits a thenable.
return new Promise((resolve) => {
var stream = FS.getStream(fd);
if (!stream) return resolve(1); // closed: let the retry surface EBADF
if (stream.flags & {{{ cDefs.O_NONBLOCK }}}) return resolve(0);
Comment thread
guybedford marked this conversation as resolved.
Outdated
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
4 changes: 3 additions & 1 deletion system/lib/libc/musl/src/network/accept.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <sys/socket.h>
#include "syscall.h"
#include "emscripten_fd_wait.h"

int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict len)
{
return socketcall_cp(accept, fd, addr, len, 0, 0, 0);
return __syscall_ret(__emscripten_sock_retry(fd, 0,
__socketcall_cp(accept, fd, addr, len, 0, 0, 0)));
Comment thread
guybedford marked this conversation as resolved.
}
4 changes: 3 additions & 1 deletion system/lib/libc/musl/src/network/accept4.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
#include <errno.h>
#include <fcntl.h>
#include "syscall.h"
#include "emscripten_fd_wait.h"

int accept4(int fd, struct sockaddr *restrict addr, socklen_t *restrict len, int flg)
{
if (!flg) return accept(fd, addr, len);
int ret = socketcall_cp(accept4, fd, addr, len, flg, 0, 0);
int ret = __syscall_ret(__emscripten_sock_retry(fd, 0,
__socketcall_cp(accept4, fd, addr, len, flg, 0, 0)));
if (ret>=0 || (errno != ENOSYS && errno != EINVAL)) return ret;
if (flg & ~(SOCK_CLOEXEC|SOCK_NONBLOCK)) {
errno = EINVAL;
Expand Down
4 changes: 3 additions & 1 deletion system/lib/libc/musl/src/network/recvfrom.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <sys/socket.h>
#include "syscall.h"
#include "emscripten_fd_wait.h"

ssize_t recvfrom(int fd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict alen)
{
return socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen);
return __syscall_ret(__emscripten_sock_retry(fd, flags & MSG_DONTWAIT,
__socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen)));
}
4 changes: 3 additions & 1 deletion system/lib/libc/musl/src/network/recvmsg.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <sys/time.h>
#include <string.h>
#include "syscall.h"
#include "emscripten_fd_wait.h"

hidden void __convert_scm_timestamps(struct msghdr *, socklen_t);

Expand Down Expand Up @@ -59,7 +60,8 @@ ssize_t recvmsg(int fd, struct msghdr *msg, int flags)
msg = &h;
}
#endif
r = socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0);
r = __syscall_ret(__emscripten_sock_retry(fd, flags & MSG_DONTWAIT,
__socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0)));
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