Skip to content
Merged
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
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ See docs/process.md for more on how version tagging works.
----------------------
- The `JSPI` setting is no longer considered experimental, and the compiler
diagnostic warning has been removed. (#27559)
- 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`. (#27207)

6.0.7 - 08/17/26
----------------
Expand Down
350 changes: 350 additions & 0 deletions src/lib/libepoll.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ sigs = {
__syscall_epoll_create1__sig: 'ii',
__syscall_epoll_ctl__sig: 'iiiip',
__syscall_epoll_pwait__sig: 'iipiipp',
__syscall_epoll_pwait_nonblocking__sig: 'iipi',
__syscall_faccessat__sig: 'iipii',
__syscall_fadvise64__sig: 'iijji',
__syscall_fallocate__sig: 'iiijj',
Expand Down
49 changes: 39 additions & 10 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,7 @@ var SyscallsLibrary = {
if (!stream) return {{{ cDefs.POLLNVAL }}};
// Streams without a poll handler (regular files, incl. NODERAWFS/NODEFS
// which leave stream_ops unset) are treated as always readable+writable.
var flags = stream.stream_ops?.poll
? stream.stream_ops.poll(stream)
: {{{ cDefs.POLLIN | cDefs.POLLOUT }}};
var flags = stream.stream_ops?.poll?.(stream) ?? {{{ cDefs.POLLIN | cDefs.POLLOUT }}};
return flags & (events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}});
},
__syscall_poll__proxy: 'sync',
Expand Down Expand Up @@ -718,13 +716,44 @@ var SyscallsLibrary = {
__syscall_poll_nonblocking: (fds, nfds) => {
return doPollSync(fds, nfds);
},
// epoll is not yet implemented in the legacy (non-WASMFS) JS syscall layer.
__syscall_epoll_create1__nothrow: true,
__syscall_epoll_create1: (flags) => -{{{ cDefs.ENOSYS }}},
__syscall_epoll_ctl__nothrow: true,
__syscall_epoll_ctl: (epfd, op, fd, ev) => -{{{ cDefs.ENOSYS }}},
__syscall_epoll_pwait__nothrow: true,
__syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => -{{{ cDefs.ENOSYS }}},
// 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'],
__syscall_epoll_create1__proxy: 'sync',
__syscall_epoll_create1: (flags) => {
// EPOLL_CLOEXEC is accepted but a no-op (there is no exec).
if (flags & ~{{{ cDefs.EPOLL_CLOEXEC }}}) return -{{{ cDefs.EINVAL }}};
return epollNewInstance().fd;
},
__syscall_epoll_ctl__deps: ['$FS', '$epollCtl'],
__syscall_epoll_ctl__proxy: 'sync',
__syscall_epoll_ctl: (epfd, op, fd, ev) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
return epollCtl(ep.shared, op, fd, ev);
},
__syscall_epoll_pwait__proxy: 'sync',
__syscall_epoll_pwait__async: 'auto',
__syscall_epoll_pwait__deps: ['$FS', '$epollPwait'],
__syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return epollPwait(ep.shared, ev, maxevents, timeout);
},
// libc routes zero-timeout epoll_wait()/epoll_pwait() calls here: a plain
// import that never suspends, so probes stay callable from any context (under
// JSPI, __syscall_epoll_pwait is a suspending import and traps when called
// from a stack that wasn't entered through a promising export). Mirrors
// __syscall_poll_nonblocking.
__syscall_epoll_pwait_nonblocking__proxy: 'sync',
__syscall_epoll_pwait_nonblocking__deps: ['$FS', '$doEpollWait'],
__syscall_epoll_pwait_nonblocking: (epfd, ev, maxevents) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return doEpollWait(ep.shared, ev, maxevents);
},
__syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'],
__syscall_getcwd: (buf, size) => {
if (!size) return -{{{ cDefs.EINVAL }}};
Expand Down
1 change: 1 addition & 0 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function calculateLibraries() {
'libtty.js',
'libpipefs.js', // ok to include it by default since it's only used if the syscall is used
'libsockfs.js', // ok to include it by default since it's only used if the syscall is used
'libepoll.js', // ok to include it by default since it's only used if the syscall is used
);

if (NODERAWSOCKETS) {
Expand Down
24 changes: 24 additions & 0 deletions src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@
]
}
},
{
"file": "sys/epoll.h",
"defines": [
"EPOLLIN",
"EPOLLOUT",
"EPOLLERR",
"EPOLLHUP",
"EPOLLRDNORM",
"EPOLLWRNORM",
"EPOLLET",
"EPOLLONESHOT",
"EPOLLEXCLUSIVE",
"EPOLL_CTL_ADD",
"EPOLL_CTL_DEL",
"EPOLL_CTL_MOD",
"EPOLL_CLOEXEC"
],
"structs": {
"epoll_event": [
"events",
"data"
]
}
},
{
"file": "time.h",
"defines": [
Expand Down
18 changes: 18 additions & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@
"EPERM": 63,
"EPFNOSUPPORT": 139,
"EPIPE": 64,
"EPOLLERR": 8,
"EPOLLET": -2147483648,
"EPOLLEXCLUSIVE": 268435456,
"EPOLLHUP": 16,
"EPOLLIN": 1,
"EPOLLONESHOT": 1073741824,
"EPOLLOUT": 4,
"EPOLLRDNORM": 64,
"EPOLLWRNORM": 256,
"EPOLL_CLOEXEC": 524288,
"EPOLL_CTL_ADD": 1,
"EPOLL_CTL_DEL": 2,
"EPOLL_CTL_MOD": 3,
"EPROTO": 65,
"EPROTONOSUPPORT": 66,
"EPROTOTYPE": 67,
Expand Down Expand Up @@ -1020,6 +1033,11 @@
"stack_ptr": 8,
"user_data": 16
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
18 changes: 18 additions & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@
"EPERM": 63,
"EPFNOSUPPORT": 139,
"EPIPE": 64,
"EPOLLERR": 8,
"EPOLLET": -2147483648,
"EPOLLEXCLUSIVE": 268435456,
"EPOLLHUP": 16,
"EPOLLIN": 1,
"EPOLLONESHOT": 1073741824,
"EPOLLOUT": 4,
"EPOLLRDNORM": 64,
"EPOLLWRNORM": 256,
"EPOLL_CLOEXEC": 524288,
"EPOLL_CTL_ADD": 1,
"EPOLL_CTL_DEL": 2,
"EPOLL_CTL_MOD": 3,
"EPROTO": 65,
"EPROTONOSUPPORT": 66,
"EPROTOTYPE": 67,
Expand Down Expand Up @@ -1020,6 +1033,11 @@
"stack_ptr": 16,
"user_data": 32
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
1 change: 1 addition & 0 deletions system/include/emscripten/syscalls.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ int __syscall_shutdown(int sockfd, int how, int unused1, int unused2, int unused
int __syscall_epoll_create1(int flags);
int __syscall_epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
int __syscall_epoll_pwait(int epfd, struct epoll_event *ev, int maxevents, int timeout, const sigset_t *sigmask, size_t sigsetsize);
int __syscall_epoll_pwait_nonblocking(int epfd, struct epoll_event *ev, int maxevents);

#ifdef __cplusplus
}
Expand Down
10 changes: 10 additions & 0 deletions system/lib/libc/musl/src/linux/epoll.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ int epoll_ctl(int fd, int op, int fd2, struct epoll_event *ev)

int epoll_pwait(int fd, struct epoll_event *ev, int cnt, int to, const sigset_t *sigs)
{
#ifdef __EMSCRIPTEN__
// A zero timeout is an instantaneous probe: route it through a plain
// import that never suspends. Under JSPI, __syscall_epoll_pwait is a
// suspending import and so may only be called from a stack entered through
// a promising export — a requirement a readiness probe must not carry
// (e.g. probes from event-loop callbacks). Mirrors poll() above.
if (to == 0) {
return __syscall_ret(__syscall_epoll_pwait_nonblocking(fd, ev, cnt));
}
#endif
int r = __syscall_cp(SYS_epoll_pwait, fd, ev, cnt, to, sigs, _NSIG/8);
#ifdef SYS_epoll_wait
if (r==-ENOSYS && !sigs) r = __syscall_cp(SYS_epoll_wait, fd, ev, cnt, to);
Expand Down
19 changes: 19 additions & 0 deletions system/lib/standalone/standalone.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ weak int __syscall_poll_nonblocking(struct pollfd *fds, nfds_t nfds) {
return -ENOSYS;
}

weak int __syscall_epoll_create1(int flags) {
return -ENOSYS;
}

weak int __syscall_epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev) {
return -ENOSYS;
}

weak int __syscall_epoll_pwait(int epfd, struct epoll_event *ev, int maxevents,
int timeout, const sigset_t *sigmask,
size_t sigsetsize) {
return -ENOSYS;
}

weak int __syscall_epoll_pwait_nonblocking(int epfd, struct epoll_event *ev,
int maxevents) {
return -ENOSYS;
}

// open(), etc. - we just support the standard streams, with no
// corner case error checking; everything else is not permitted.
// TODO: full file support for WASI, or an option for it
Expand Down
6 changes: 6 additions & 0 deletions system/lib/wasmfs/syscalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1887,4 +1887,10 @@ int __syscall_epoll_pwait(int epfd,
return -ENOSYS;
}

int __syscall_epoll_pwait_nonblocking(int epfd,
struct epoll_event* ev,
int maxevents) {
return -ENOSYS;
}

} // extern "C"
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": 267819,
"a.out.nodebug.wasm": 588299,
"total": 856118,
"a.out.js": 270568,
"a.out.nodebug.wasm": 588359,
"total": 858927,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down Expand Up @@ -231,6 +231,7 @@
"__syscall_epoll_create1",
"__syscall_epoll_ctl",
"__syscall_epoll_pwait",
"__syscall_epoll_pwait_nonblocking",
"__syscall_faccessat",
"__syscall_fadvise64",
"__syscall_fallocate",
Expand Down Expand Up @@ -1762,6 +1763,7 @@
"env.__syscall_epoll_create1",
"env.__syscall_epoll_ctl",
"env.__syscall_epoll_pwait",
"env.__syscall_epoll_pwait_nonblocking",
"env.__syscall_faccessat",
"env.__syscall_fadvise64",
"env.__syscall_fallocate",
Expand Down
104 changes: 104 additions & 0 deletions test/core/test_epoll.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.
*
* Exercises the epoll syscall surface (epoll_create1/epoll_ctl/epoll_wait):
* the interest set, the ADD/MOD/DEL ops with their error returns, readiness
* derivation over a pipe, and that the opaque `data` is echoed back.
*/

#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#include <stdio.h>

int main(void) {
// Unknown creation flags are rejected; EPOLL_CLOEXEC is accepted (a no-op).
assert(epoll_create1(999) == -1 && errno == EINVAL);
int cl = epoll_create1(EPOLL_CLOEXEC);
assert(cl >= 0);
close(cl);

int ep = epoll_create1(0);
assert(ep >= 0);

int p[2];
assert(pipe(p) == 0);

struct epoll_event ev = { .events = EPOLLIN };
ev.data.fd = p[0];
assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0);
assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == -1 && errno == EEXIST);

struct epoll_event out[4];
// Nothing written yet: the read end is not readable.
assert(epoll_wait(ep, out, 4, 0) == 0);

// Make the read end readable.
assert(write(p[1], "x", 1) == 1);
assert(epoll_wait(ep, out, 4, 0) == 1);
assert(out[0].events & EPOLLIN);
assert(out[0].data.fd == p[0]);

// MOD to a condition that is not satisfied (writable on the read end).
ev.events = EPOLLOUT;
assert(epoll_ctl(ep, EPOLL_CTL_MOD, p[0], &ev) == 0);
assert(epoll_wait(ep, out, 4, 0) == 0);

// DEL, and DEL again -> ENOENT.
assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == 0);
assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == -1 && errno == ENOENT);
assert(epoll_wait(ep, out, 4, 0) == 0);

// Bad epoll fd and bad target fd.
assert(epoll_ctl(ep, EPOLL_CTL_ADD, 9999, &ev) == -1 && errno == EBADF);
assert(epoll_ctl(9999, EPOLL_CTL_ADD, p[0], &ev) == -1 && errno == EBADF);

// Bad op.
ev.events = EPOLLIN;
assert(epoll_ctl(ep, 999, p[0], &ev) == -1 && errno == EINVAL);

// An epoll cannot watch itself.
assert(epoll_ctl(ep, EPOLL_CTL_ADD, ep, &ev) == -1 && errno == EINVAL);

// Regular files are not epoll-capable (no readiness derivation -> EPERM).
int rf = open("/tmp/epoll_regular", O_CREAT | O_RDWR, 0600);
assert(rf >= 0);
assert(epoll_ctl(ep, EPOLL_CTL_ADD, rf, &ev) == -1 && errno == EPERM);
close(rf);

// maxevents must be positive.
assert(epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &ev) == 0);
assert(epoll_wait(ep, out, 0, 0) == -1 && errno == EINVAL);
assert(epoll_wait(ep, out, -1, 0) == -1 && errno == EINVAL);

// fd reuse: a registration keys on the open file description, so closing a
// watched fd and reusing its number for a different open must not resurrect
// the registration onto the new fd (which would report wrong readiness).
assert(epoll_ctl(ep, EPOLL_CTL_DEL, p[0], &ev) == 0); // empty the set
int a[2];
assert(pipe(a) == 0);
ev.data.fd = a[0];
assert(epoll_ctl(ep, EPOLL_CTL_ADD, a[0], &ev) == 0);
close(a[0]); // the registration is now stale
int b[2];
assert(pipe(b) == 0);
assert(b[0] == a[0]); // b[0] reused a[0]'s freed number
assert(write(b[1], "y", 1) == 1); // the reused fd is readable
assert(epoll_wait(ep, out, 4, 0) == 0); // stale registration must not fire
// The stale entry is gone, so the reused fd adds fresh (evicted, not EEXIST)
// and then reports normally.
ev.data.fd = b[0];
assert(epoll_ctl(ep, EPOLL_CTL_ADD, b[0], &ev) == 0);
assert(epoll_wait(ep, out, 4, 0) == 1 && out[0].data.fd == b[0]);

close(ep);
close(p[0]);
close(p[1]);
printf("done\n");
return 0;
}
Loading
Loading