From 4fa8a263ce5e3fa70825ef2189da5bbccd235af4 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 11 Jun 2026 13:30:14 -0700 Subject: [PATCH 1/3] Route zero-timeout poll() through a non-suspending import --- src/lib/libsigs.js | 1 + src/lib/libsyscall.js | 64 +++++++++++++++++--------- system/include/emscripten/syscalls.h | 1 + system/lib/libc/musl/src/select/poll.c | 9 ++++ test/poll_nonblocking.c | 36 +++++++++++++++ test/test_other.py | 15 ++++++ 6 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 test/poll_nonblocking.c diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 5fbfec805bd06..60dce773943be 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -263,6 +263,7 @@ sigs = { __syscall_openat__sig: 'iipip', __syscall_pipe2__sig: 'ipi', __syscall_poll__sig: 'ipii', + __syscall_poll_nonblocking__sig: 'ipi', __syscall_readlinkat__sig: 'iippp', __syscall_recvfrom__sig: 'iippipp', __syscall_recvmsg__sig: 'iipiiii', diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index a0105e2a2871f..dd709c8a9ca5e 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -563,6 +563,7 @@ var SyscallsLibrary = { }, __syscall_poll__proxy: 'sync', __syscall_poll__async: 'auto', + __syscall_poll__deps: ['__syscall_poll_nonblocking'], __syscall_poll: (fds, nfds, timeout) => { #if PTHREADS || ASYNCIFY #if PTHREADS @@ -622,9 +623,46 @@ var SyscallsLibrary = { }, timeout); cleanupFuncs.push(() => clearTimeout(t)); } + var count = 0; + for (var i = 0; i < nfds; i++) { + var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; + var fd = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.fd, 'i32') }}}; + var events = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}; + var flags = {{{ cDefs.POLLNVAL }}}; + var stream = FS.getStream(fd); + if (stream) { + if (stream.stream_ops.poll) { + flags = timeout + ? stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)) + : stream.stream_ops.poll(stream, -1); + } else { + flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + } + } + flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; + if (flags) count++; + {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}}; + } + if (count || !timeout) { + asyncPollComplete(count); + } + return promise; } #endif + var count = ___syscall_poll_nonblocking(fds, nfds); +#if ASSERTIONS + if (!count && timeout != 0) warnOnce('non-zero poll() timeout not supported: ' + timeout) +#endif + return count; + }, + // libc routes zero-timeout poll() calls here: the same synchronous + // readiness derivation as __syscall_poll, but as a plain import that never + // suspends, so probes stay callable from any context (under JSPI, + // __syscall_poll is a suspending import and traps when called from a stack + // that wasn't entered through a promising export). + __syscall_poll_nonblocking__proxy: 'sync', + __syscall_poll_nonblocking: (fds, nfds) => { var count = 0; for (var i = 0; i < nfds; i++) { var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; @@ -633,34 +671,14 @@ var SyscallsLibrary = { var flags = {{{ cDefs.POLLNVAL }}}; var stream = FS.getStream(fd); if (stream) { - if (stream.stream_ops.poll) { -#if PTHREADS || ASYNCIFY - if (isAsyncContext && timeout) { - flags = stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)); - } else -#endif - flags = stream.stream_ops.poll(stream, -1); - } else { - flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; - } + flags = stream.stream_ops.poll + ? stream.stream_ops.poll(stream, -1) + : {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; } flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; if (flags) count++; {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}}; } - -#if PTHREADS || ASYNCIFY - if (isAsyncContext) { - if (count || !timeout) { - asyncPollComplete(count); - } - return promise; - } -#endif - -#if ASSERTIONS - if (!count && timeout != 0) warnOnce('non-zero poll() timeout not supported: ' + timeout) -#endif return count; }, __syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'], diff --git a/system/include/emscripten/syscalls.h b/system/include/emscripten/syscalls.h index a01c138c05b5d..336d6a8d5ddd3 100644 --- a/system/include/emscripten/syscalls.h +++ b/system/include/emscripten/syscalls.h @@ -51,6 +51,7 @@ int __syscall_mlockall(int flags); int __syscall_munlockall(void); int __syscall_mremap(intptr_t old_addr, size_t old_size, size_t new_size, int flags, intptr_t new_addr); int __syscall_poll(intptr_t fds, int nfds, int timeout); +int __syscall_poll_nonblocking(intptr_t fds, int nfds); int __syscall_getcwd(intptr_t buf, size_t size); intptr_t __syscall_mmap2(intptr_t addr, size_t len, int prot, int flags, int fd, off_t offset); int __syscall_truncate64(intptr_t path, off_t length); diff --git a/system/lib/libc/musl/src/select/poll.c b/system/lib/libc/musl/src/select/poll.c index 7883dfab478a2..10359d60fff50 100644 --- a/system/lib/libc/musl/src/select/poll.c +++ b/system/lib/libc/musl/src/select/poll.c @@ -5,6 +5,15 @@ int poll(struct pollfd *fds, nfds_t n, int timeout) { +#ifdef __EMSCRIPTEN__ + // A zero timeout is an instantaneous probe: route it through a plain + // import that never suspends. Under JSPI, __syscall_poll 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). + if (timeout == 0) + return __syscall_ret(__syscall_poll_nonblocking((intptr_t)fds, n)); +#endif #ifdef SYS_poll return syscall_cp(SYS_poll, fds, n, timeout); #else diff --git a/test/poll_nonblocking.c b/test/poll_nonblocking.c new file mode 100644 index 0000000000000..e7b5e2ce75ace --- /dev/null +++ b/test/poll_nonblocking.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include +#include +#include + +// A zero-timeout poll() is a non-suspending probe, so it must work from any +// context. The property under test with -sJSPI: it stays callable from an +// event-loop callback, where a suspending call would trap (Suspending +// imports require a stack entered via a promising export). + +static void probe(const char* where) { + struct pollfd pfd; + pfd.fd = STDOUT_FILENO; + pfd.events = POLLOUT; + pfd.revents = 0; + int n = poll(&pfd, 1, 0); + assert(n == 1); + assert(pfd.revents & POLLOUT); + printf("poll probe ok from %s\n", where); +} + +static void on_timeout(void* user_data) { + probe("callback"); + // A true shutdown even where the runtime is kept alive (e.g. worker + // threads under PROXY_TO_PTHREAD). + emscripten_force_exit(0); +} + +int main(void) { + probe("main"); + emscripten_set_timeout(on_timeout, 0, NULL); + emscripten_exit_with_live_runtime(); + return 99; +} diff --git a/test/test_other.py b/test/test_other.py index e8c3106d26957..c372bb1caeb59 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13168,6 +13168,21 @@ def test_emscripten_set_timeout(self): def test_emscripten_set_timeout_loop(self): self.do_runf('emscripten_set_timeout_loop.c', args=['-pthread', '-sPROXY_TO_PTHREAD']) + @parameterized({ + '': ([],), + 'asyncify': (['-sASYNCIFY'],), + 'jspi': (['-sJSPI'],), + 'pthreads': (['-pthread', '-sPROXY_TO_PTHREAD'],), + }) + def test_poll_nonblocking(self, cflags): + if '-sJSPI' in cflags: + self.require_jspi() + if '-pthread' in cflags: + self.require_pthreads() + self.do_runf('poll_nonblocking.c', + 'poll probe ok from main\npoll probe ok from callback\n', + cflags=cflags) + # Verify that we are able to successfully compile a script when the Windows 7 # and Python workaround env. vars are enabled. # See https://bugs.python.org/issue34780 From 103fa0d5b3bd854ff7605e6c1269030b198c260f Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 11 Jun 2026 15:21:09 -0700 Subject: [PATCH 2/3] pr feedback --- src/lib/libsyscall.js | 63 +++++++++---------- system/lib/libc/musl/src/select/poll.c | 3 +- system/lib/standalone/standalone.c | 4 ++ system/lib/wasmfs/syscalls.cpp | 6 ++ .../test_codesize_hello_dylink_all.json | 8 ++- test/test_other.py | 10 +-- ..._nonblocking.c => test_poll_nonblocking.c} | 0 7 files changed, 53 insertions(+), 41 deletions(-) rename test/{poll_nonblocking.c => test_poll_nonblocking.c} (100%) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index dd709c8a9ca5e..8a6d8fc504bc6 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -563,7 +563,7 @@ var SyscallsLibrary = { }, __syscall_poll__proxy: 'sync', __syscall_poll__async: 'auto', - __syscall_poll__deps: ['__syscall_poll_nonblocking'], + __syscall_poll__deps: ['$doPoll'], __syscall_poll: (fds, nfds, timeout) => { #if PTHREADS || ASYNCIFY #if PTHREADS @@ -623,26 +623,9 @@ var SyscallsLibrary = { }, timeout); cleanupFuncs.push(() => clearTimeout(t)); } - var count = 0; - for (var i = 0; i < nfds; i++) { - var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; - var fd = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.fd, 'i32') }}}; - var events = {{{ makeGetValue('pollfd', C_STRUCTS.pollfd.events, 'i16') }}}; - var flags = {{{ cDefs.POLLNVAL }}}; - var stream = FS.getStream(fd); - if (stream) { - if (stream.stream_ops.poll) { - flags = timeout - ? stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)) - : stream.stream_ops.poll(stream, -1); - } else { - flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; - } - } - flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; - if (flags) count++; - {{{ makeSetValue('pollfd', C_STRUCTS.pollfd.revents, 'flags', 'i16') }}}; - } + // A zero timeout never registers notifications: the derivation alone + // answers, matching the non-blocking probe. + var count = doPoll(fds, nfds, timeout || undefined, makeNotifyCallback); if (count || !timeout) { asyncPollComplete(count); } @@ -650,19 +633,21 @@ var SyscallsLibrary = { } #endif - var count = ___syscall_poll_nonblocking(fds, nfds); + var count = doPoll(fds, nfds, undefined, undefined); #if ASSERTIONS if (!count && timeout != 0) warnOnce('non-zero poll() timeout not supported: ' + timeout) #endif return count; }, - // libc routes zero-timeout poll() calls here: the same synchronous - // readiness derivation as __syscall_poll, but as a plain import that never - // suspends, so probes stay callable from any context (under JSPI, - // __syscall_poll is a suspending import and traps when called from a stack - // that wasn't entered through a promising export). - __syscall_poll_nonblocking__proxy: 'sync', - __syscall_poll_nonblocking: (fds, nfds) => { + // The shared readiness derivation: one pass over the pollfds, writing + // revents and returning the ready count. With `timeout` defined, a + // readiness notification is also registered on each stream by the same + // `stream_ops.poll` call that derives it, so there is no window between + // registration and derivation; `timeout === undefined` implies no + // notification (the plain probe). + $doPoll__internal: true, + $doPoll__deps: ['$FS'], + $doPoll: (fds, nfds, timeout, makeNotifyCallback) => { var count = 0; for (var i = 0; i < nfds; i++) { var pollfd = fds + {{{ C_STRUCTS.pollfd.__size__ }}} * i; @@ -671,9 +656,13 @@ var SyscallsLibrary = { var flags = {{{ cDefs.POLLNVAL }}}; var stream = FS.getStream(fd); if (stream) { - flags = stream.stream_ops.poll - ? stream.stream_ops.poll(stream, -1) - : {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + if (stream.stream_ops.poll) { + flags = timeout !== undefined + ? stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)) + : stream.stream_ops.poll(stream, -1); + } else { + flags = {{{ cDefs.POLLIN | cDefs.POLLOUT }}}; + } } flags &= events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}}; if (flags) count++; @@ -681,6 +670,16 @@ var SyscallsLibrary = { } return count; }, + // libc routes zero-timeout poll() calls here: the same synchronous + // readiness derivation as __syscall_poll, but as a plain import that never + // suspends, so probes stay callable from any context (under JSPI, + // __syscall_poll is a suspending import and traps when called from a stack + // that wasn't entered through a promising export). + __syscall_poll_nonblocking__proxy: 'sync', + __syscall_poll_nonblocking__deps: ['$doPoll'], + __syscall_poll_nonblocking: (fds, nfds) => { + return doPoll(fds, nfds, undefined, undefined); + }, __syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'], __syscall_getcwd: (buf, size) => { if (size === 0) return -{{{ cDefs.EINVAL }}}; diff --git a/system/lib/libc/musl/src/select/poll.c b/system/lib/libc/musl/src/select/poll.c index 10359d60fff50..6ffe6b2cee2ea 100644 --- a/system/lib/libc/musl/src/select/poll.c +++ b/system/lib/libc/musl/src/select/poll.c @@ -11,8 +11,9 @@ int poll(struct pollfd *fds, nfds_t n, int timeout) // 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). - if (timeout == 0) + if (timeout == 0) { return __syscall_ret(__syscall_poll_nonblocking((intptr_t)fds, n)); + } #endif #ifdef SYS_poll return syscall_cp(SYS_poll, fds, n, timeout); diff --git a/system/lib/standalone/standalone.c b/system/lib/standalone/standalone.c index 755049544e0b7..c5b0b40729d9a 100644 --- a/system/lib/standalone/standalone.c +++ b/system/lib/standalone/standalone.c @@ -70,6 +70,10 @@ weak int __syscall_poll(intptr_t fds, int nfds, int timeout) { return -ENOSYS; } +weak int __syscall_poll_nonblocking(intptr_t fds, int nfds) { + 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 diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 2a86a5813b76c..95babec3cec1f 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -1436,6 +1436,12 @@ int __syscall_poll(intptr_t fds_, int nfds, int timeout) { return nonzero; } +// libc routes zero-timeout poll() calls here (see musl's poll.c). WasmFS's +// __syscall_poll never blocks, so the zero-timeout probe is the same call. +int __syscall_poll_nonblocking(intptr_t fds, int nfds) { + return __syscall_poll(fds, nfds, 0); +} + int __syscall_fallocate(int fd, int mode, off_t offset, off_t len) { assert(mode == 0); // TODO, but other modes were never supported in the old FS diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 6e7733e0e4911..584a099573a66 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268089, - "a.out.nodebug.wasm": 587586, - "total": 855675, + "a.out.js": 268274, + "a.out.nodebug.wasm": 587640, + "total": 855914, "sent": [ "IMG_Init", "IMG_Load", @@ -248,6 +248,7 @@ "__syscall_openat", "__syscall_pipe2", "__syscall_poll", + "__syscall_poll_nonblocking", "__syscall_readlinkat", "__syscall_recvfrom", "__syscall_recvmsg", @@ -1766,6 +1767,7 @@ "env.__syscall_openat", "env.__syscall_pipe2", "env.__syscall_poll", + "env.__syscall_poll_nonblocking", "env.__syscall_readlinkat", "env.__syscall_recvfrom", "env.__syscall_recvmsg", diff --git a/test/test_other.py b/test/test_other.py index c372bb1caeb59..05596186dc7f3 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13174,14 +13174,14 @@ def test_emscripten_set_timeout_loop(self): 'jspi': (['-sJSPI'],), 'pthreads': (['-pthread', '-sPROXY_TO_PTHREAD'],), }) - def test_poll_nonblocking(self, cflags): - if '-sJSPI' in cflags: + def test_poll_nonblocking(self, args): + if '-sJSPI' in args: self.require_jspi() - if '-pthread' in cflags: + if '-pthread' in args: self.require_pthreads() - self.do_runf('poll_nonblocking.c', + self.do_runf('test_poll_nonblocking.c', 'poll probe ok from main\npoll probe ok from callback\n', - cflags=cflags) + cflags=args) # Verify that we are able to successfully compile a script when the Windows 7 # and Python workaround env. vars are enabled. diff --git a/test/poll_nonblocking.c b/test/test_poll_nonblocking.c similarity index 100% rename from test/poll_nonblocking.c rename to test/test_poll_nonblocking.c From 96b774fe6cac28ad992c3378ca64492ffbb7d603 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 11 Jun 2026 16:34:32 -0700 Subject: [PATCH 3/3] remove undefined variant --- src/lib/libsyscall.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 8a6d8fc504bc6..e9197aec48bae 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -625,7 +625,7 @@ var SyscallsLibrary = { } // A zero timeout never registers notifications: the derivation alone // answers, matching the non-blocking probe. - var count = doPoll(fds, nfds, timeout || undefined, makeNotifyCallback); + var count = doPoll(fds, nfds, timeout, makeNotifyCallback); if (count || !timeout) { asyncPollComplete(count); } @@ -633,18 +633,18 @@ var SyscallsLibrary = { } #endif - var count = doPoll(fds, nfds, undefined, undefined); + var count = doPoll(fds, nfds, 0, undefined); #if ASSERTIONS if (!count && timeout != 0) warnOnce('non-zero poll() timeout not supported: ' + timeout) #endif return count; }, // The shared readiness derivation: one pass over the pollfds, writing - // revents and returning the ready count. With `timeout` defined, a + // revents and returning the ready count. With a nonzero `timeout`, a // readiness notification is also registered on each stream by the same // `stream_ops.poll` call that derives it, so there is no window between - // registration and derivation; `timeout === undefined` implies no - // notification (the plain probe). + // registration and derivation; a zero `timeout` means the caller will not + // wait, so no notification is registered (the plain probe). $doPoll__internal: true, $doPoll__deps: ['$FS'], $doPoll: (fds, nfds, timeout, makeNotifyCallback) => { @@ -657,7 +657,7 @@ var SyscallsLibrary = { var stream = FS.getStream(fd); if (stream) { if (stream.stream_ops.poll) { - flags = timeout !== undefined + flags = timeout ? stream.stream_ops.poll(stream, timeout, makeNotifyCallback(stream, pollfd)) : stream.stream_ops.poll(stream, -1); } else { @@ -678,7 +678,7 @@ var SyscallsLibrary = { __syscall_poll_nonblocking__proxy: 'sync', __syscall_poll_nonblocking__deps: ['$doPoll'], __syscall_poll_nonblocking: (fds, nfds) => { - return doPoll(fds, nfds, undefined, undefined); + return doPoll(fds, nfds, 0, undefined); }, __syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'], __syscall_getcwd: (buf, size) => {