From 85251d545568653f4e5265008b6beb7357eba1f2 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:34:29 +0900 Subject: [PATCH 01/10] select: separate parsers to functions Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 128 +++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 5f9a0be9afb17..da7e8e3882282 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -102,6 +102,71 @@ var SyscallsLibrary = { #endif return ret; }, + + getTimeoutInMillis(timeout) { + // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". + // However, musl passes the two values to the syscall as an array of long values. + // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. + // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. + // So, instead, we use POINTER_SIZE. + var tv_sec = ({{{ makeGetValue('timeout', 0, 'i32') }}}), + tv_usec = ({{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}}); + return (tv_sec + tv_usec / 1000000) * 1000; + }, + + parseSelectFDSet(readfds, writefds, exceptfds) { + var total = 0; + + var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), + srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); + var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), + srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); + var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), + srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); + + var dstReadLow = 0, + dstReadHigh = 0; + var dstWriteLow = 0, + dstWriteHigh = 0; + var dstExceptLow = 0, + dstExceptHigh = 0; + + var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + + return { + getTotal: () => total, + setFlags: (fd, flags) => { + var mask = 1 << (fd % 32); + + if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { + fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { + fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { + fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); + total++; + } + }, + commit: () => { + if (readfds) { + {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; + {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; + } + if (writefds) { + {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; + {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; + } + if (exceptfds) { + {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; + {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; + } + } + }; + }, }, $syscallGetVarargI__internal: true, @@ -552,21 +617,7 @@ var SyscallsLibrary = { assert(nfds <= 64, 'nfds must be less than or equal to 64'); // fd sets have 64 bits // TODO: this could be 1024 based on current musl headers #endif - var total = 0; - - var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), - srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); - var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), - srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); - var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), - srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); - - var dstReadLow = 0, - dstReadHigh = 0; - var dstWriteLow = 0, - dstWriteHigh = 0; - var dstExceptLow = 0, - dstExceptHigh = 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); var allLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0) | (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0) | @@ -577,6 +628,11 @@ var SyscallsLibrary = { var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + var timeoutInMillis = -1; + if (timeout) { + timeoutInMillis = SYSCALLS.getTimeoutInMillis(timeout); + } + for (var fd = 0; fd < nfds; fd++) { var mask = 1 << (fd % 32); if (!(check(fd, allLow, allHigh, mask))) { @@ -588,48 +644,16 @@ var SyscallsLibrary = { var flags = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { - var timeoutInMillis = -1; - if (timeout) { - // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". - // However, musl passes the two values to the syscall as an array of long values. - // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. - // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. - // So, instead, we use POINTER_SIZE. - var tv_sec = (readfds ? {{{ makeGetValue('timeout', 0, 'i32') }}} : 0), - tv_usec = (readfds ? {{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}} : 0); - timeoutInMillis = (tv_sec + tv_usec / 1000000) * 1000; - } - flags = stream.stream_ops.poll(stream, timeoutInMillis); + flags = stream.stream_ops.poll(stream, ((timeoutInMillis < 0) || readfds) ? timeoutInMillis : 0); } - if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { - fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { - fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { - fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); - total++; - } + fdSet.setFlags(fd, flags); } - if (readfds) { - {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; - {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; - } - if (writefds) { - {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; - {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; - } - if (exceptfds) { - {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; - {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; - } - return total; + fdSet.commit(fd, flags); + + return fdSet.getTotal(); }, _msync_js__i53abi: true, _msync_js: (addr, len, prot, flags, fd, offset) => { From d708df80ede1118eacce2b2a2b07fd607c690d02 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 02:05:16 +0900 Subject: [PATCH 02/10] select: Enable timeout on select when PROXY_TO_PTHREAD This commit enables the select syscall to handle timeout with multiple event sources. PROXY_TO_PTHREAD is needed to prevent blocking the main worker. When a thread worker invokes the select syscall with non-zero timeout and no fd is ready, it blocks using Atmoics.wait until it receives a readiness notification. On the main worker, the underlying stream implementation can trigger readiness using Atomics.notify through a callback. A notification also issued automatically once the specified timeout expires. Communication between the thread worker and the main worker occurs via a shared memory region. To prevent a select invocation being unblocked by the callbacks of a previous invocation, all active callbacks are tracked in a list. See the comments in activeSelectCallbacks for details. Usage of the notification callback is optional. If the stream implementation doesn't support it, the "poll" method can still synchronously return the event status. Signed-off-by: Kohei Tokunaga --- src/lib/libpthread.js | 54 ++++++++++++++++++++++++++++++++++ src/lib/libsyscall.js | 68 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 695320d4e5304..5d4e12ae81909 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -88,6 +88,11 @@ var LibraryPThread = { 'exit', #if PTHREADS_DEBUG || ASSERTIONS '$ptrToString', +#endif +#if PROXY_TO_PTHREAD + '$addThreadToActiveSelectCallbacks', + '$removeThreadFromActiveSelectCallbacks', + '$activeSelectCallbacks', #endif ], $PThread: { @@ -587,6 +592,9 @@ var LibraryPThread = { #endif #if ASSERTIONS assert(worker); +#endif +#if PROXY_TO_PTHREAD + removeThreadFromActiveSelectCallbacks(pthread_ptr); #endif PThread.returnWorkerToPool(worker); }, @@ -631,6 +639,49 @@ var LibraryPThread = { $registerTLSInit: (tlsInitFunc) => PThread.tlsInitFunctions.push(tlsInitFunc), #endif +#if PROXY_TO_PTHREAD + // On the main worker, activeSelectCallbacks records the set of callbacks + // that are allowed to update the shared region. Any callback not in this + // set (i.e. when !isActiveSelectCallback) must not update the region. + // + // Each select syscall invocation must call deactivateSelectCallbacks to + // reset this set, ensuring that callbacks from previous invocations don't + // affect the current one. + // + // If a callback executes after the thread worker has already returned (due + // to a timeout, a readiness notification or other exceptional conditions) + // but before the next deactivation, it may still update the shared region. + // However the thread worker will not read that value and just ignore it. + // + // activeSelectCallbacks records multiple callback lists one per thread + // worker so that each worker can manage its own set of active callbacks + // independently. + $activeSelectCallbacks: {}, + $addThreadToActiveSelectCallbacks__deps: ['malloc'], + $addThreadToActiveSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr] = { + buf: _malloc(8), + callbacks: [], + }; + }, + $removeThreadFromActiveSelectCallbacks: (pthread_ptr) => { + delete activeSelectCallbacks[pthread_ptr]; + }, + $getActiveSelectCallbacks: (pthread_ptr) => { + return activeSelectCallbacks[pthread_ptr]; + }, + $deactivateSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr].callbacks = []; + }, + $activateSelectCallback: (pthread_ptr, cb) => { + activeSelectCallbacks[pthread_ptr].callbacks.push(cb); + }, + $isActiveSelectCallback: (pthread_ptr, cb) => { + return (activeSelectCallbacks[pthread_ptr] != null) && + (activeSelectCallbacks[pthread_ptr].callbacks.indexOf(cb) != -1); + }, +#endif + $spawnThread: (threadParams) => { #if ASSERTIONS assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! spawnThread() can only ever be called from main application thread!'); @@ -658,6 +709,9 @@ var LibraryPThread = { arg: threadParams.arg, pthread_ptr: threadParams.pthread_ptr, }; +#if PROXY_TO_PTHREAD + addThreadToActiveSelectCallbacks(threadParams.pthread_ptr); +#endif #if OFFSCREENCANVAS_SUPPORT // Note that we do not need to quote these names because they are only used // in this file, and not from the external worker.js. diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index da7e8e3882282..2b380252dd437 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -607,7 +607,35 @@ var SyscallsLibrary = { FS.chdir(stream.path); return 0; }, + __syscall__newselect__deps: ['$newselectInner','malloc','free'], + __syscall__newselect__proxy: 'none', __syscall__newselect: (nfds, readfds, writefds, exceptfds, timeout) => { +#if PROXY_TO_PTHREAD + var waitPtr = _malloc(8); + var result = newselectInner(nfds, readfds, writefds, exceptfds, timeout, waitPtr); + if ((result != 0) || ((timeout) && (SYSCALLS.getTimeoutInMillis(timeout) == 0))) { + _free(waitPtr); + return result; + } + var fdRegion = {{{ makeGetValue('waitPtr', 0, '*') }}}; + Atomics.wait(HEAP32 , fdRegion >> 2, -1); + var fd = Atomics.load(HEAP32 , fdRegion >> 2); + var flags = Atomics.load(HEAP32 , fdRegion >> 2 + 1); + _free(waitPtr); + if (fd < 0) return 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); + fdSet.setFlags(fd, flags); + fdSet.commit(); + return fdSet.getTotal(); +#else + return newselectInner(nfds, readfds, writefds, exceptfds, timeout, -1); +#endif + }, +#if PROXY_TO_PTHREAD + $newselectInner__deps: ['$PThread', '$deactivateSelectCallbacks', '$getActiveSelectCallbacks', '$activateSelectCallback', '$isActiveSelectCallback'], +#endif + $newselectInner__proxy: 'sync', + $newselectInner: (nfds, readfds, writefds, exceptfds, timeout, waitPtr) => { // readfds are supported, // writefds checks socket open status // exceptfds are supported, although on web, such exceptional conditions never arise in web sockets @@ -633,6 +661,34 @@ var SyscallsLibrary = { timeoutInMillis = SYSCALLS.getTimeoutInMillis(timeout); } +#if PROXY_TO_PTHREAD + const pthread_ptr = PThread.currentProxiedOperationCallerThread; + deactivateSelectCallbacks(pthread_ptr); // deactivate all old callbacks + var makeNotifyCallback = (fd) => null; + if (timeoutInMillis != 0) { + var info = getActiveSelectCallbacks(pthread_ptr); + {{{ makeSetValue('waitPtr', 0, 'info.buf', '*') }}}; + Atomics.store(HEAP32, info.buf >> 2, -1); // Initialize the shared region + makeNotifyCallback = (fd) => { + var cb = (flags) => { + if (!isActiveSelectCallback(pthread_ptr, cb)) { + return; // This callback is no longer active. + } + deactivateSelectCallbacks(pthread_ptr); // Only the first event is notified. + Atomics.store(HEAP32, info.buf >> 2 + 1, flags); + Atomics.store(HEAP32, info.buf >> 2, fd); + Atomics.notify(HEAP32, info.buf >> 2); + } + activateSelectCallback(pthread_ptr, cb); + return cb; + } + if (timeoutInMillis > 0) { + var cb = makeNotifyCallback(-2); + setTimeout(() => cb(0), timeoutInMillis); + } + } +#endif + for (var fd = 0; fd < nfds; fd++) { var mask = 1 << (fd % 32); if (!(check(fd, allLow, allHigh, mask))) { @@ -644,14 +700,26 @@ var SyscallsLibrary = { var flags = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { +#if PROXY_TO_PTHREAD + flags = stream.stream_ops.poll(stream, timeoutInMillis, makeNotifyCallback(fd)); +#else flags = stream.stream_ops.poll(stream, ((timeoutInMillis < 0) || readfds) ? timeoutInMillis : 0); +#endif } fdSet.setFlags(fd, flags); } +#if PROXY_TO_PTHREAD + if ((fdSet.getTotal() > 0) || (timeoutInMillis == 0) ) { + fdSet.commit(fd, flags); + // No wait will happen in the caller. Deactivate all callbacks. + deactivateSelectCallbacks(pthread_ptr); + } +#else fdSet.commit(fd, flags); +#endif return fdSet.getTotal(); }, From 6d12ce98ee69825c3191406ad6a1794e97578e09 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 02:05:44 +0900 Subject: [PATCH 03/10] select: Allow registering cleanup method for the callback This commit adds the registerCleanupFunc method for the callback passed to the stream implementation. It allows the stream implementation to optionally register a cleanup function that will be invoked when select is no longer interested in the stream events. This enables the stream implementation to perform additional cleanups such as discarding the reference to the event callback. Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 2b380252dd437..2df831a2d5835 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -665,6 +665,7 @@ var SyscallsLibrary = { const pthread_ptr = PThread.currentProxiedOperationCallerThread; deactivateSelectCallbacks(pthread_ptr); // deactivate all old callbacks var makeNotifyCallback = (fd) => null; + var cleanupFuncs = []; if (timeoutInMillis != 0) { var info = getActiveSelectCallbacks(pthread_ptr); {{{ makeSetValue('waitPtr', 0, 'info.buf', '*') }}}; @@ -675,10 +676,14 @@ var SyscallsLibrary = { return; // This callback is no longer active. } deactivateSelectCallbacks(pthread_ptr); // Only the first event is notified. + cleanupFuncs.forEach(cb => cb()); Atomics.store(HEAP32, info.buf >> 2 + 1, flags); Atomics.store(HEAP32, info.buf >> 2, fd); Atomics.notify(HEAP32, info.buf >> 2); } + cb.registerCleanupFunc = (f) => { + if (f != null) cleanupFuncs.push(f); + } activateSelectCallback(pthread_ptr, cb); return cb; } @@ -716,6 +721,7 @@ var SyscallsLibrary = { fdSet.commit(fd, flags); // No wait will happen in the caller. Deactivate all callbacks. deactivateSelectCallbacks(pthread_ptr); + cleanupFuncs.forEach(f => f()); } #else fdSet.commit(fd, flags); From 4482be9c6f3c7e13f075698cc9f745aed9d60398 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:36:00 +0900 Subject: [PATCH 04/10] select: update code comment about timeout Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 2df831a2d5835..1a491130df20c 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -640,7 +640,8 @@ var SyscallsLibrary = { // writefds checks socket open status // exceptfds are supported, although on web, such exceptional conditions never arise in web sockets // and so the exceptfds list will always return empty. - // timeout is supported, although on SOCKFS and PIPEFS these are ignored and always treated as 0 - fully async + // timeout is supported, although on SOCKFS these are ignored and always treated as 0 - fully async + // and PIPEFS supports timeout only when PROXY_TO_PTHREAD is enabled. #if ASSERTIONS assert(nfds <= 64, 'nfds must be less than or equal to 64'); // fd sets have 64 bits // TODO: this could be 1024 based on current musl headers #endif From fd2999e7dc892ecbf27ae45030972330b777df19 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:36:40 +0900 Subject: [PATCH 05/10] pipefs: Enable pipes to notify readiness The poll method of PIPEFS receives a notification callback from the caller. PIPEFS notifies the caller when the fd becomes readable using that callback. Signed-off-by: Kohei Tokunaga --- src/lib/libpipefs.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/libpipefs.js b/src/lib/libpipefs.js index d9a3fed62180e..25ad2cea03cfd 100644 --- a/src/lib/libpipefs.js +++ b/src/lib/libpipefs.js @@ -21,6 +21,19 @@ addToLibrary({ // able to read from the read end after write end is closed. refcnt : 2, timestamp: new Date(), + readableHandlers: [], + registerReadableHanlders: (notifyCallback) => { + if (notifyCallback == null) return; + notifyCallback.registerCleanupFunc(() => { + const i = pipe.readableHandlers.indexOf(notifyCallback); + if (i !== -1) pipe.readableHandlers.splice(i, 1); + }); + pipe.readableHandlers.push(notifyCallback); + }, + notifyReadableHanders: () => { + pipe.readableHandlers.forEach(cb => cb({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}})); + pipe.readableHandlers = []; + } }; pipe.buckets.push({ @@ -80,7 +93,7 @@ addToLibrary({ blocks: 0, }; }, - poll(stream) { + poll(stream, timeout, notifyCallback) { var pipe = stream.node.pipe; if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_WRONLY }}}) { @@ -92,6 +105,7 @@ addToLibrary({ } } + pipe.registerReadableHanlders(notifyCallback); return 0; }, dup(stream) { @@ -204,6 +218,7 @@ addToLibrary({ if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; + pipe.notifyReadableHanders(); return dataLen; } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); @@ -235,6 +250,7 @@ addToLibrary({ newBucket.buffer.set(data); } + pipe.notifyReadableHanders(); return dataLen; }, close(stream) { From a6dccfb522097771e17a7aa339d6092b6a4d6249 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:37:23 +0900 Subject: [PATCH 06/10] test: Add tests for timeout on select with PROXY_TO_PTHREAD Signed-off-by: Kohei Tokunaga --- test/core/test_pthread_select_timeout.c | 129 ++++++++++++++++++++++++ test/test_core.py | 3 + 2 files changed, 132 insertions(+) create mode 100644 test/core/test_pthread_select_timeout.c diff --git a/test/core/test_pthread_select_timeout.c b/test/core/test_pthread_select_timeout.c new file mode 100644 index 0000000000000..2f8cd4fa81a1d --- /dev/null +++ b/test/core/test_pthread_select_timeout.c @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Check if timeout works without fds +void test_timeout_without_fds() +{ + struct timeval tv, begin, end; + + tv.tv_sec = 1; + tv.tv_usec = 0; + gettimeofday(&begin, NULL); + assert(select(0, NULL, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); +} + +// Check if timeout works with fds without events +void test_timeout_with_fds_without_events() +{ + struct timeval tv, begin, end; + fd_set readfds; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + + tv.tv_sec = 1; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + gettimeofday(&begin, NULL); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + close(pipe_a[0]); close(pipe_a[1]); +} + +int pipe_shared[2]; + +void *wakeup_after_2s(void * arg) +{ + const char *t = "test\n"; + + sleep(2); + write(pipe_shared[1], t, strlen(t)); + + return NULL; +} + +// Check if select can unblock on an event +void test_unblock_select() +{ + struct timeval begin, end; + fd_set readfds; + int maxfd; + pthread_t tid; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + assert(pipe(pipe_shared) == 0); + + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + FD_SET(pipe_shared[0], &readfds); + maxfd = (pipe_a[0] > pipe_shared[0] ? pipe_a[0] : pipe_shared[0]); + assert(pthread_create(&tid, NULL, wakeup_after_2s, NULL) == 0); + gettimeofday(&begin, NULL); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 1); + gettimeofday(&end, NULL); + assert(FD_ISSET(pipe_shared[0], &readfds)); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + pthread_join(tid, NULL); + + close(pipe_a[0]); close(pipe_a[1]); + close(pipe_shared[0]); close(pipe_shared[1]); +} + +// Check if select works with ready fds +void test_ready_fds() +{ + struct timeval tv; + fd_set readfds; + int maxfd; + const char *t = "test\n"; + int pipe_c[2]; + int pipe_d[2]; + + assert(pipe(pipe_c) == 0); + assert(pipe(pipe_d) == 0); + + write(pipe_c[1], t, strlen(t)); + write(pipe_d[1], t, strlen(t)); + maxfd = (pipe_c[0] > pipe_d[0] ? pipe_c[0] : pipe_d[0]); + + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, &tv) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + close(pipe_c[0]); close(pipe_c[1]); + close(pipe_d[0]); close(pipe_d[1]); +} + +int main() +{ + test_timeout_without_fds(); + test_timeout_with_fds_without_events(); + test_unblock_select(); + test_ready_fds(); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 316be245cad4b..92bd29a4b31af 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9722,6 +9722,9 @@ def test_wasm_global(self, dynlink): def test_syscall_intercept(self): self.do_core_test('test_syscall_intercept.c') + def test_pthread_select_timeout(self): + self.do_runf('core/test_pthread_select_timeout.c', cflags=['-pthread', '-sPROXY_TO_PTHREAD=1', '-sEXIT_RUNTIME=1', '-Wno-pthreads-mem-growth']) + @also_without_bigint def test_jslib_i64_params(self): # Tests the defineI64Param and receiveI64ParamAsI53 helpers that are From e2615b85ffead477ba72d17e66a73a05f81c0cff Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:37:42 +0900 Subject: [PATCH 07/10] test: Add test for select without PROXY_TO_PTHREAD Signed-off-by: Kohei Tokunaga --- test/core/test_select.c | 25 +++++++++++++++++++++++++ test/test_core.py | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 test/core/test_select.c diff --git a/test/core/test_select.c b/test/core/test_select.c new file mode 100644 index 0000000000000..97412d7049120 --- /dev/null +++ b/test/core/test_select.c @@ -0,0 +1,25 @@ +#include +#include +#include +#include +#include +#include + +int pipe_a[2]; + +int main() +{ + fd_set readfds; + const char *t = "test\n"; + + assert(pipe(pipe_a) == 0); + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + write(pipe_a[1], t, strlen(t)); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, NULL) == 1); + assert(FD_ISSET(pipe_a[0], &readfds)); + + close(pipe_a[0]); close(pipe_a[1]); + + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 92bd29a4b31af..ca58fd62c4731 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9725,6 +9725,9 @@ def test_syscall_intercept(self): def test_pthread_select_timeout(self): self.do_runf('core/test_pthread_select_timeout.c', cflags=['-pthread', '-sPROXY_TO_PTHREAD=1', '-sEXIT_RUNTIME=1', '-Wno-pthreads-mem-growth']) + def test_select(self): + self.do_runf('core/test_select.c') + @also_without_bigint def test_jslib_i64_params(self): # Tests the defineI64Param and receiveI64ParamAsI53 helpers that are From 936b4f8c1c07d330d18e1ad4b1f986305e713e9b Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 16:13:33 +0900 Subject: [PATCH 08/10] test: specify timeout as 0 to select when expecting nonblocking Signed-off-by: Kohei Tokunaga --- test/sockets/test_sockets_echo_client.c | 5 ++++- test/sockets/test_sockets_echo_server.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/sockets/test_sockets_echo_client.c b/test/sockets/test_sockets_echo_client.c index 64df6b969b8cc..1c1afd52d1787 100644 --- a/test/sockets/test_sockets_echo_client.c +++ b/test/sockets/test_sockets_echo_client.c @@ -67,13 +67,16 @@ void main_loop() { fd_set fdr; fd_set fdw; int res; + struct timeval tv; // make sure that server.fd is ready to read / write FD_ZERO(&fdr); FD_ZERO(&fdw); FD_SET(server.fd, &fdr); FD_SET(server.fd, &fdw); - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); finish(EXIT_FAILURE); diff --git a/test/sockets/test_sockets_echo_server.c b/test/sockets/test_sockets_echo_server.c index a77c599e3c310..43377c88327f6 100644 --- a/test/sockets/test_sockets_echo_server.c +++ b/test/sockets/test_sockets_echo_server.c @@ -77,6 +77,7 @@ void main_loop() { int res; fd_set fdr; fd_set fdw; + struct timeval tv; // see if there are any connections to accept or read / write from FD_ZERO(&fdr); @@ -87,7 +88,9 @@ void main_loop() { if (client.fd) FD_SET(client.fd, &fdr); if (client.fd) FD_SET(client.fd, &fdw); #endif - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); exit(EXIT_SUCCESS); From e4e13f08870e8b7afb962f1985d65df218b71d8e Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 16:51:40 +0900 Subject: [PATCH 09/10] select: remove the word "eval." from the comment This is needed to avoid the following test failure. > AssertionError: Expected to NOT find 'eval. Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 1a491130df20c..26409c2f55518 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -107,7 +107,7 @@ var SyscallsLibrary = { // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". // However, musl passes the two values to the syscall as an array of long values. // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. - // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. + // This means using "C_STRUCTS.timeval\.tv_usec" leads to a wrong offset. // So, instead, we use POINTER_SIZE. var tv_sec = ({{{ makeGetValue('timeout', 0, 'i32') }}}), tv_usec = ({{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}}); From 3938766d089437f153270ab31a777dd0a7f4bcc4 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Thu, 9 Oct 2025 01:33:13 +0900 Subject: [PATCH 10/10] pthread: Enable the noleakcheck mark also for PROXY_TO_PTHREAD In addition to the existing OFFSCREENCANVAS_SUPPORT configuration, PROXY_TO_PTHREAD now also performs memory allocation internally for use by activeSelectCallbacks. Signed-off-by: Kohei Tokunaga --- src/lib/libpthread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 5d4e12ae81909..7170eeb402ff2 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -756,7 +756,7 @@ var LibraryPThread = { $pthreadCreateProxied__deps: ['__pthread_create_js'], $pthreadCreateProxied: (pthread_ptr, attr, startRoutine, arg) => ___pthread_create_js(pthread_ptr, attr, startRoutine, arg), -#if OFFSCREENCANVAS_SUPPORT +#if OFFSCREENCANVAS_SUPPORT || PROXY_TO_PTHREAD // ASan wraps the emscripten_builtin_pthread_create call in // __lsan::ScopedInterceptorDisabler. Unfortunately, that only disables it on // the thread that made the call. __pthread_create_js gets proxied to the