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
16 changes: 16 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ See docs/process.md for more on how version tagging works.
FS-backend handler signature changed from `poll(stream, timeout)` to
`poll(stream)` returning the current readiness mask; out-of-tree custom FS
backends with a `poll` handler must update. (#27226)
- Added support for `epoll` (`epoll_create1`/`epoll_ctl`/`epoll_wait`/
`epoll_pwait`) on the legacy (non-WASMFS) JS filesystem, including
level- and edge-triggered modes, `EPOLLONESHOT`, `EPOLLEXCLUSIVE`,
`EPOLLRDHUP`, nesting, and blocking waits under `PROXY_TO_PTHREAD`,
`ASYNCIFY`, and `JSPI`. Also added `emscripten_epoll_set_callback`
(in the new `<emscripten/epoll.h>`, experimental), a non-blocking variant
that signals an epoll set's readiness to a callback (which collects the
events itself via a zero-timeout `epoll_wait`) with no `ASYNCIFY`/`JSPI`.
(#27207)
- Blocking `accept`, `recv`, `recvfrom` and `recvmsg` on sockets are now
supported under `-pthread` with `PROXY_TO_PTHREAD`: a blocking call whose
socket would-block suspends the proxied worker on the inode readiness queue
and retries when woken, instead of returning `EAGAIN`. (`send`/`write` never
block, as the Node.js backend buffers.) This covers the socket calls only,
not a blocking `read()`/`write()` on a socket fd; single-threaded
`ASYNCIFY`/`JSPI` builds should use `epoll` for readiness instead. (#27277)
- compiler-rt and libunwind were updated to LLVM 22.1.8. (#27245, #27246)
- `-fcoverage-mapping` is currently broken due to a mismatch between the version
of LLVM used and the imported version of compiler-rt. We hope to fix this
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
27 changes: 27 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,27 @@
#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. 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);
}

#endif
12 changes: 12 additions & 0 deletions system/lib/libc/musl/src/network/accept.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
#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__
for (;;) {
long r = __socketcall_cp(accept, fd, addr, len, 0, 0, 0);
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, 0)
|| _emscripten_fd_wait(fd, POLLIN))
return __syscall_ret(r);
}
#else
return socketcall_cp(accept, fd, addr, len, 0, 0, 0);
#endif
}
15 changes: 15 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,26 @@
#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;
for (;;) {
long r = __socketcall_cp(accept4, fd, addr, len, flg, 0, 0);
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, 0)
|| _emscripten_fd_wait(fd, POLLIN)) {
ret = __syscall_ret(r);
break;
}
}
#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
12 changes: 12 additions & 0 deletions system/lib/libc/musl/src/network/recvfrom.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
#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__
for (;;) {
long r = __socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen);
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, flags & MSG_DONTWAIT)
|| _emscripten_fd_wait(fd, POLLIN))
return __syscall_ret(r);
}
#else
return socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen);
#endif
}
14 changes: 14 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,18 @@ ssize_t recvmsg(int fd, struct msghdr *msg, int flags)
msg = &h;
}
#endif
#ifdef __EMSCRIPTEN_PTHREADS__
for (;;) {
long rr = __socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0);
if (rr != -EAGAIN || !__emscripten_sock_can_wait(fd, flags & MSG_DONTWAIT)
|| _emscripten_fd_wait(fd, POLLIN)) {
r = __syscall_ret(rr);
break;
}
}
#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": 270584,
"a.out.js": 270665,
"a.out.nodebug.wasm": 588747,
"total": 859331,
"total": 859412,
"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;
}
Loading
Loading