From 5baafcc83b4a78f06cbbe949642ed159ebae417c Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Tue, 3 Mar 2026 07:08:05 -0800 Subject: [PATCH] poll, pselect: handle POLLPRI / errorfds gracefully WASI has no out-of-band data, so POLLPRI can never fire. Rather than rejecting any use of POLLPRI outright, adopt a lenient policy: - Define POLLPRI in __header_poll.h for source compatibility. - In poll(): if any fd requests non-POLLPRI events, strip POLLPRI and proceed (it simply never fires). Only return ENOSYS when every fd exclusively requests POLLPRI, since there is nothing useful to poll. - In pselect(): thread errorfds through poll() as POLLPRI entries so the call succeeds when readfds or writefds are also provided. This fixes programs (e.g. vim) that pass errorfds to select/pselect alongside other fd sets. Includes a test exercising pselect with errorfds and poll with POLLPRI. See: https://github.com/WebAssembly/wasi-libc/pull/754#issuecomment-3992790419 Signed-off-by: Christian Stewart --- .../predefined-macros.txt | 1 + expected/wasm32-wasip1/predefined-macros.txt | 1 + expected/wasm32-wasip2/predefined-macros.txt | 1 + expected/wasm32-wasip3/predefined-macros.txt | 1 + .../cloudlibc/src/libc/poll/poll.c | 61 +++++++++++-- .../cloudlibc/src/libc/sys/select/pselect.c | 31 +++++-- .../headers/public/__header_poll.h | 6 ++ test/CMakeLists.txt | 1 + test/src/pselect.c | 85 +++++++++++++++++++ 9 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 test/src/pselect.c diff --git a/expected/wasm32-wasip1-threads/predefined-macros.txt b/expected/wasm32-wasip1-threads/predefined-macros.txt index 5dd34da56..36e4ec9f8 100644 --- a/expected/wasm32-wasip1-threads/predefined-macros.txt +++ b/expected/wasm32-wasip1-threads/predefined-macros.txt @@ -1359,6 +1359,7 @@ #define POLLIN POLLRDNORM #define POLLNVAL 0x4000 #define POLLOUT POLLWRNORM +#define POLLPRI 0x0200 #define POLLRDNORM 0x1 #define POLLWRNORM 0x2 #define POSIX_CLOSE_RESTART 0 diff --git a/expected/wasm32-wasip1/predefined-macros.txt b/expected/wasm32-wasip1/predefined-macros.txt index c20d1ad33..f74f35f7e 100644 --- a/expected/wasm32-wasip1/predefined-macros.txt +++ b/expected/wasm32-wasip1/predefined-macros.txt @@ -1359,6 +1359,7 @@ #define POLLIN POLLRDNORM #define POLLNVAL 0x4000 #define POLLOUT POLLWRNORM +#define POLLPRI 0x0200 #define POLLRDNORM 0x1 #define POLLWRNORM 0x2 #define POSIX_CLOSE_RESTART 0 diff --git a/expected/wasm32-wasip2/predefined-macros.txt b/expected/wasm32-wasip2/predefined-macros.txt index 13d1f8c5c..a023c0f17 100644 --- a/expected/wasm32-wasip2/predefined-macros.txt +++ b/expected/wasm32-wasip2/predefined-macros.txt @@ -1490,6 +1490,7 @@ #define POLLIN POLLRDNORM #define POLLNVAL 0x4000 #define POLLOUT POLLWRNORM +#define POLLPRI 0x0200 #define POLLRDNORM 0x1 #define POLLWRNORM 0x2 #define POSIX_CLOSE_RESTART 0 diff --git a/expected/wasm32-wasip3/predefined-macros.txt b/expected/wasm32-wasip3/predefined-macros.txt index 8e2f62089..f4e0fbe8a 100644 --- a/expected/wasm32-wasip3/predefined-macros.txt +++ b/expected/wasm32-wasip3/predefined-macros.txt @@ -1468,6 +1468,7 @@ #define POLLIN POLLRDNORM #define POLLNVAL 0x4000 #define POLLOUT POLLWRNORM +#define POLLPRI 0x0200 #define POLLRDNORM 0x1 #define POLLWRNORM 0x2 #define POSIX_CLOSE_RESTART 0 diff --git a/libc-bottom-half/cloudlibc/src/libc/poll/poll.c b/libc-bottom-half/cloudlibc/src/libc/poll/poll.c index 796f97c4b..a0a71754e 100644 --- a/libc-bottom-half/cloudlibc/src/libc/poll/poll.c +++ b/libc-bottom-half/cloudlibc/src/libc/poll/poll.c @@ -15,12 +15,35 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { size_t maxevents = 2 * nfds + 1; __wasi_subscription_t subscriptions[maxevents]; size_t nsubscriptions = 0; + + // POLLPRI (exceptional/out-of-band data) is not supported in WASI. + // If all requested events across all fds are POLLPRI, return ENOSYS. + // Otherwise, remove POLLPRI so it never fires (same as no OOB data). + { + bool has_pri_only = false; + bool has_non_pri = false; + for (size_t i = 0; i < nfds; ++i) { + if (fds[i].fd < 0 || fds[i].events == 0) + continue; + if (fds[i].events & ~POLLPRI) + has_non_pri = true; + else + has_pri_only = true; + } + if (has_pri_only && !has_non_pri) { + errno = ENOSYS; + return -1; + } + } + for (size_t i = 0; i < nfds; ++i) { struct pollfd *pollfd = &fds[i]; if (pollfd->fd < 0) continue; + // Strip POLLPRI as it is never reported in WASI. + short events = pollfd->events & ~POLLPRI; bool created_events = false; - if ((pollfd->events & POLLRDNORM) != 0) { + if ((events & POLLRDNORM) != 0) { __wasi_subscription_t *subscription = &subscriptions[nsubscriptions++]; *subscription = (__wasi_subscription_t){ .userdata = (uintptr_t)pollfd, @@ -29,7 +52,7 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { }; created_events = true; } - if ((pollfd->events & POLLWRNORM) != 0) { + if ((events & POLLWRNORM) != 0) { __wasi_subscription_t *subscription = &subscriptions[nsubscriptions++]; *subscription = (__wasi_subscription_t){ .userdata = (uintptr_t)pollfd, @@ -42,8 +65,9 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { // As entries are decomposed into separate read/write subscriptions, // we cannot detect POLLERR, POLLHUP and POLLNVAL if POLLRDNORM and // POLLWRNORM are not specified. Disallow this for now. - // Ignore fd entries that have no events requested. - if (!created_events && pollfd->events != 0) { + // Ignore fd entries that have no events requested (including + // entries that only had POLLPRI which was stripped above). + if (!created_events && events != 0) { errno = ENOSYS; return -1; } @@ -176,6 +200,26 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { fds[i].revents = 0; } + // POLLPRI (exceptional/out-of-band data) is not supported in WASI. + // If all requested events across all fds are exclusively POLLPRI, + // return ENOSYS. Otherwise, strip POLLPRI and proceed. + { + bool has_pri_only = false; + bool has_non_pri = false; + for (size_t i = 0; i < nfds; ++i) { + if (fds[i].fd < 0 || fds[i].events == 0) + continue; + if (fds[i].events & ~POLLPRI) + has_non_pri = true; + else + has_pri_only = true; + } + if (has_pri_only && !has_non_pri) { + errno = ENOSYS; + return -1; + } + } + size_t max_pollables = (2 * nfds) + 1; state_t states[max_pollables]; poll_borrow_pollable_t pollables[max_pollables]; @@ -207,14 +251,17 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { continue; } + // Strip POLLPRI, it is never reported in WASI (no OOB data). + short events = pollfd->events & ~POLLPRI; + // Without a custom registration handle read/write readiness // below, but everything else is unsupported. - if (pollfd->events & ~(POLLRDNORM | POLLWRNORM)) { + if (events & ~(POLLRDNORM | POLLWRNORM)) { errno = EOPNOTSUPP; return -1; } - if (pollfd->events & POLLRDNORM) { + if (events & POLLRDNORM) { if (entry->vtable->get_read_stream) { wasi_read_t read; if (entry->vtable->get_read_stream(entry->data, &read) < 0) @@ -227,7 +274,7 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { } } - if (pollfd->events & POLLWRNORM) { + if (events & POLLWRNORM) { if (entry->vtable->get_write_stream) { wasi_write_t write; if (entry->vtable->get_write_stream(entry->data, &write) < 0) diff --git a/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c b/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c index 05915850c..92777587a 100644 --- a/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c +++ b/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c @@ -20,13 +20,6 @@ int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, return -1; } - // This implementation does not support polling for exceptional - // conditions, such as out-of-band data on TCP sockets. - if (errorfds != NULL && errorfds->__nfds > 0) { - errno = ENOSYS; - return -1; - } - // Replace NULL pointers by the empty set. fd_set empty; FD_ZERO(&empty); @@ -34,8 +27,10 @@ int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, readfds = ∅ if (writefds == NULL) writefds = ∅ + if (errorfds == NULL) + errorfds = ∅ - struct pollfd poll_fds[readfds->__nfds + writefds->__nfds]; + struct pollfd poll_fds[readfds->__nfds + writefds->__nfds + errorfds->__nfds]; size_t poll_nfds = 0; for (size_t i = 0; i < readfds->__nfds; ++i) { @@ -60,6 +55,20 @@ int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, } } + // Thread exceptional conditions through as POLLPRI. WASI has no out-of-band + // data, so POLLPRI will never fire, but poll() will allow the call to proceed + // as long as there are other non-POLLPRI events to wait on. + for (size_t i = 0; i < errorfds->__nfds; ++i) { + int fd = errorfds->__fds[i]; + if (fd < nfds) { + poll_fds[poll_nfds++] = (struct pollfd){ + .fd = fd, + .events = POLLPRI, + .revents = 0 + }; + } + } + int poll_timeout; if (timeout) { uint64_t timeout_u64; @@ -92,6 +101,7 @@ int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, FD_ZERO(readfds); FD_ZERO(writefds); + FD_ZERO(errorfds); for (size_t i = 0; i < poll_nfds; ++i) { struct pollfd* pollfd = poll_fds + i; if ((pollfd->revents & POLLRDNORM) != 0) { @@ -100,7 +110,10 @@ int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, if ((pollfd->revents & POLLWRNORM) != 0) { writefds->__fds[writefds->__nfds++] = pollfd->fd; } + if ((pollfd->revents & POLLPRI) != 0) { + errorfds->__fds[errorfds->__nfds++] = pollfd->fd; + } } - return readfds->__nfds + writefds->__nfds; + return readfds->__nfds + writefds->__nfds + errorfds->__nfds; } diff --git a/libc-bottom-half/headers/public/__header_poll.h b/libc-bottom-half/headers/public/__header_poll.h index 23b36f2f2..f62196a1e 100644 --- a/libc-bottom-half/headers/public/__header_poll.h +++ b/libc-bottom-half/headers/public/__header_poll.h @@ -10,6 +10,12 @@ #define POLLIN POLLRDNORM #define POLLOUT POLLWRNORM +// POLLPRI is defined for source compatibility. WASI has no out-of-band +// data, so POLLPRI is never reported in revents. poll() silently strips +// it when other events are present and returns ENOSYS when it is the +// only requested event. +#define POLLPRI 0x0200 + #define POLLERR 0x1000 #define POLLHUP 0x2000 #define POLLNVAL 0x4000 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1951f21ab..70c225f18 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -290,6 +290,7 @@ add_wasilibc_test(memcmp.c LDFLAGS -Wl,--stack-first -Wl,--initial-memory=327680 add_wasilibc_test(opendir.c FS ARGV /) add_wasilibc_test(open_relative_path.c FS ARGV /) add_wasilibc_test(poll.c FS FAILP3) +add_wasilibc_test(pselect.c FS FAILP3) add_wasilibc_test(preadvwritev.c FS) add_wasilibc_test(preadwrite.c FS) add_wasilibc_test(readlink.c FS) diff --git a/test/src/pselect.c b/test/src/pselect.c new file mode 100644 index 000000000..67885d6b0 --- /dev/null +++ b/test/src/pselect.c @@ -0,0 +1,85 @@ +// Test pselect() with errorfds and poll() POLLPRI handling. +// +// WASI has no out-of-band data, so POLLPRI / exceptfds should never +// report ready. However, pselect() should not fail when errorfds is +// provided alongside other fd sets. poll() should only fail when +// POLLPRI is the exclusive set of events being requested. + +#include "test.h" +#include +#include +#include +#include +#include + +#define TEST(c, ...) ((c) ? 1 : (t_error(#c " failed: " __VA_ARGS__), 0)) + +int main(void) { + char tmp[] = "testsuite-XXXXXX"; + int fd; + + TEST((fd = open(tmp, O_RDWR | O_CREAT | O_EXCL, 0600)) > 2); + TEST(write(fd, "hello", 5) == 5); + TEST(lseek(fd, 0, SEEK_SET) == 0); + + // ---- pselect: readfds + errorfds should succeed ---- + { + fd_set rfds, efds; + FD_ZERO(&rfds); + FD_ZERO(&efds); + FD_SET(fd, &rfds); + FD_SET(fd, &efds); + + // Use blocking wait (no timeout) like the poll test does. + // A zero timeout may race with event delivery on some runtimes. + int r = pselect(fd + 1, &rfds, NULL, &efds, NULL, NULL); + TEST(r >= 0, "pselect with readfds+errorfds returned %d, errno=%d\n", r, + errno); + // errorfds should be empty (POLLPRI never fires in WASI). + TEST(!FD_ISSET(fd, &efds), + "errorfds should be empty (no OOB data in WASI)\n"); + // readfds should have the fd set (file has data). + TEST(FD_ISSET(fd, &rfds), "readfds should have fd set\n"); + } + + // ---- pselect: only errorfds, no readfds/writefds should fail ---- + { + fd_set efds; + FD_ZERO(&efds); + FD_SET(fd, &efds); + + struct timespec tv = {0, 0}; + errno = 0; + int r = pselect(fd + 1, NULL, NULL, &efds, &tv, NULL); + TEST(r == -1 && errno == ENOSYS, + "pselect with only errorfds should fail with ENOSYS, got r=%d " + "errno=%d\n", + r, errno); + } + + // ---- poll: POLLPRI | POLLRDNORM should succeed ---- + { + struct pollfd pfd = { + .fd = fd, .events = POLLPRI | POLLRDNORM, .revents = 0}; + int r = poll(&pfd, 1, -1); + TEST(r >= 0, "poll with POLLPRI|POLLRDNORM returned %d, errno=%d\n", r, + errno); + // POLLPRI should not be set in revents. + TEST(!(pfd.revents & POLLPRI), "POLLPRI should not be reported in WASI\n"); + } + + // ---- poll: exclusively POLLPRI should fail ---- + { + struct pollfd pfd = {.fd = fd, .events = POLLPRI, .revents = 0}; + errno = 0; + int r = poll(&pfd, 1, 0); + TEST(r == -1 && errno == ENOSYS, + "poll with only POLLPRI should fail with ENOSYS, got r=%d errno=%d\n", + r, errno); + } + + close(fd); + TEST(unlink(tmp) != -1); + + return t_status; +}