diff --git a/include/poll.h b/include/poll.h new file mode 100644 index 0000000..151724c --- /dev/null +++ b/include/poll.h @@ -0,0 +1,99 @@ +#ifndef VLIBC_POLL_H +#define VLIBC_POLL_H + +/* + * vlibc — . + * + * Poll multiple file descriptors for readiness. Every declaration here is a + * thin pass-through to the kernel: poll() blocks per its millisecond + * timeout, ppoll() takes a struct timespec and an optional signal mask + * directly, and both return the count of ready descriptors (0 on timeout, + * -1 with errno set on error). + * + * Level 1 (onlyposix): poll, nfds_t, struct pollfd, the POLL* event bits. + * Level 2 (muslmimic): ppoll (Linux-specific; POSIX has only poll). + * + * struct pollfd and the POLL* values mirror the kernel ABI (x86_64 + * asm-generic/poll.h) exactly: the wrappers pass them to SYS_ppoll + * unmodified. POLLERR, POLLHUP and POLLNVAL are report-only — they can + * appear in revents even when not requested in events. + * + * The signal mask type ppoll takes is declared under the shared guard + * below; is its canonical POSIX home and will own the full sig* + * API. + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Requestable event bits (poll()'s events field). */ +#define POLLIN 0x001 /* readable */ +#define POLLPRI 0x002 /* urgent readable (out-of-band) */ +#define POLLOUT 0x004 /* writable */ + +/* Report-only event bits (poll()'s revents field). */ +#define POLLERR 0x008 /* error condition */ +#define POLLHUP 0x010 /* hung up */ +#define POLLNVAL 0x020 /* invalid fd */ + +/* The number of struct pollfd entries poll()/ppoll() watch. */ +typedef unsigned long nfds_t; + +/* + * One descriptor watched by poll()/ppoll(). fd is the descriptor (negative + * to ignore), events the requested bits (POLLIN/POLLOUT), revents the bits + * the kernel reports (the requested bits plus any POLLERR/POLLHUP/POLLNVAL). + */ +struct pollfd +{ + int fd; + short events; + short revents; +}; + +/* + * Wait for readiness on the first nfds entries of fds, blocking up to + * timeout milliseconds (timeout < 0 blocks indefinitely, 0 never blocks). + * Return the number of entries with a nonzero revents, 0 on timeout, or -1 + * with errno set on error. + */ +int +poll(struct pollfd *fds, nfds_t nfds, int timeout); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): Linux-specific. */ + +#ifndef VLIBC_SIGSET_T_DEFINED +#define VLIBC_SIGSET_T_DEFINED +/* + * Signal mask type: a single 64-bit word — the x86_64 Linux sigset_t (see + * setjmp.h). is the canonical POSIX home for sigset_t and builds + * the sig* API on this same layout; the typedef is repeated in + * under this guard so the two headers stay consistent. + */ +typedef unsigned long sigset_t; +#endif + +/* + * Like poll(), but the timeout is a struct timespec and, when sigmask is + * not NULL, the given signal mask is atomically installed for the duration + * of the wait (the previous mask is restored before returning). + * Linux-specific. + */ +int +ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_POLL_H */ diff --git a/include/sys/select.h b/include/sys/select.h new file mode 100644 index 0000000..08c3347 --- /dev/null +++ b/include/sys/select.h @@ -0,0 +1,120 @@ +#ifndef VLIBC_SYS_SELECT_H +#define VLIBC_SYS_SELECT_H + +/* + * vlibc — . + * + * Synchronous I/O multiplexing: wait for readiness on file-descriptor sets + * with select() or pselect(). Both are thin wrappers over the SYS_pselect6 + * kernel ABI, (nfds, readfds, writefds, exceptfds, struct timespec *, + * { sigset_t *, size_t } *): pselect passes its timespec and optional signal + * mask straight through, select converts its struct timeval timeout to a + * timespec and passes a NULL sigset pair. + * + * Level 1 (onlyposix): select, pselect, FD_* macros, fd_set, struct + * timeval. + * + * fd_set is the kernel's bitmap layout on x86_64: an array of unsigned long + * words, with descriptor d held in bit d % 64 of word d / 64. FD_SETSIZE + * caps the tracked descriptors at 1024 (16 words), so select()/pselect() + * must be called with nfds <= FD_SETSIZE and FD_SET() only ever receives + * descriptors below FD_SETSIZE. select() and pselect() report the number of + * ready descriptors, 0 on timeout, or -1 with errno set on error. + * + * The signal mask type is declared here under the shared guard; + * is its canonical POSIX home and will own the full sig* API. + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Maximum descriptors select()/pselect() track (fd_set's bit capacity). */ +#define FD_SETSIZE 1024 + +/* + * A set of file descriptors, one bit per descriptor, stored in the kernel + * bitmap layout: an array of unsigned long words, bit d in word + * d / (8 * sizeof(unsigned long)). Descriptors 0..FD_SETSIZE-1 fit; no + * operation may touch a descriptor at or above FD_SETSIZE. + */ +typedef struct +{ + unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))]; +} fd_set; + +/* The word holding bit d of an fd_set, and that bit's mask within the word. */ +#define VLIBC_FDS_WORD(d) ((d) / (8 * sizeof(unsigned long))) +#define VLIBC_FDS_MASK(d) (1UL << ((d) % (8 * sizeof(unsigned long)))) + +/* Clear every descriptor bit of set. */ +#define FD_ZERO(set) \ + do \ + { \ + size_t fd_zero_i; \ + for (fd_zero_i = 0; fd_zero_i < sizeof(fd_set) / sizeof(unsigned long); fd_zero_i++) \ + { \ + (set)->fds_bits[fd_zero_i] = 0UL; \ + } \ + } while (0) + +/* Add descriptor d to set. */ +#define FD_SET(d, set) ((set)->fds_bits[VLIBC_FDS_WORD(d)] |= VLIBC_FDS_MASK(d)) + +/* Remove descriptor d from set. */ +#define FD_CLR(d, set) ((set)->fds_bits[VLIBC_FDS_WORD(d)] &= ~VLIBC_FDS_MASK(d)) + +/* Nonzero when descriptor d is a member of set. */ +#define FD_ISSET(d, set) ((set)->fds_bits[VLIBC_FDS_WORD(d)] & VLIBC_FDS_MASK(d)) + +#ifndef VLIBC_SIGSET_T_DEFINED +#define VLIBC_SIGSET_T_DEFINED +/* + * Signal mask type: a single 64-bit word — the x86_64 Linux sigset_t (see + * setjmp.h). pselect() and ppoll() only ever forward a pointer to the + * kernel, which reads the word directly. is the canonical POSIX + * home for sigset_t and builds the sig* API on this same layout; the + * typedef is repeated in under this guard so the two headers stay + * consistent. + */ +typedef unsigned long sigset_t; +#endif + +/* Elapsed time in seconds and microseconds (see select()'s timeout). */ +struct timeval +{ + time_t tv_sec; + suseconds_t tv_usec; +}; + +/* + * Wait for readiness on the descriptors marked in readfds, writefds and + * exceptfds (each may be NULL), up to nfds descriptors (the highest + * descriptor in any set plus one). timeout is an upper bound on the wait; a + * NULL timeout blocks indefinitely, { 0, 0 } never blocks. On return each + * non-NULL set holds only its ready descriptors. Return the number of ready + * descriptors across the sets, 0 on timeout, or -1 with errno set. + */ +int +select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); + +/* + * Like select(), but the timeout is a struct timespec and, when sigmask is + * not NULL, the given signal mask is atomically installed for the duration + * of the wait (the previous mask is restored before returning). + */ +int +pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, + const struct timespec *timeout, const sigset_t *sigmask); + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_SELECT_H */ diff --git a/src/select/poll.c b/src/select/poll.c new file mode 100644 index 0000000..ed4dd52 --- /dev/null +++ b/src/select/poll.c @@ -0,0 +1,32 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * poll: plain POSIX poll over SYS_ppoll with a NULL sigmask (Linux has no + * separate poll syscall for >1024 fds; ppoll is the generic form). The + * millisecond timeout is converted to the struct timespec the kernel + * expects; a negative timeout means "block indefinitely" and stays NULL. + * + * The (nfds_t, int) parameter pair is the fixed POSIX signature, so the + * easily-swappable-parameters warning does not apply. + */ +int +poll(struct pollfd *fds, nfds_t nfds, // NOLINT(bugprone-easily-swappable-parameters) + int timeout) +{ + struct timespec ts; + struct timespec *tsp = NULL; + + if (timeout >= 0) + { + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (long)(timeout % 1000) * 1000000L; + tsp = &ts; + } + return syscall_ret(__syscall5(SYS_ppoll, (long)fds, (long)nfds, (long)tsp, 0L, 0L)); +} diff --git a/src/select/ppoll.c b/src/select/ppoll.c new file mode 100644 index 0000000..dd0370b --- /dev/null +++ b/src/select/ppoll.c @@ -0,0 +1,24 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * ppoll: Linux-specific poll over SYS_ppoll with a struct timespec timeout + * and an optional signal mask (the kernel takes the mask and its size as + * separate trailing arguments; the size is only announced — as the 8-byte + * kernel sigset size — when a mask is actually given). + */ +int +ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask) +{ + return syscall_ret(__syscall5(SYS_ppoll, (long)fds, (long)nfds, (long)timeout, (long)sigmask, + sigmask != NULL ? (long)sizeof(sigset_t) : 0L)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/select/pselect.c b/src/select/pselect.c new file mode 100644 index 0000000..f4e31e4 --- /dev/null +++ b/src/select/pselect.c @@ -0,0 +1,27 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * pselect: plain POSIX pselect over SYS_pselect6. Its 6th kernel argument is + * a pointer to a { sigset_t *, size_t } pair (the kernel reads two words); + * NULL means "leave the signal mask alone", so the pair is only supplied — + * and only the 8-byte kernel sigset size announced — when sigmask is given. + * The timeout passes through as a struct timespec (NULL = indefinite). + */ +int +pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, + const struct timespec *timeout, const sigset_t *sigmask) +{ + long data[2]; + + data[0] = (long)sigmask; + data[1] = (long)sizeof(sigset_t); + return syscall_ret(__syscall6(SYS_pselect6, (long)nfds, (long)readfds, (long)writefds, + (long)exceptfds, (long)timeout, + sigmask != NULL ? (long)data : 0L)); +} diff --git a/src/select/select.c b/src/select/select.c new file mode 100644 index 0000000..6ae5f30 --- /dev/null +++ b/src/select/select.c @@ -0,0 +1,38 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * select: plain POSIX select over SYS_pselect6. The kernel wants a struct + * timespec, so the struct timeval timeout is converted here (NULL passes + * through as "block indefinitely") and the sigset pair stays NULL: select + * never changes the signal mask. select() and pselect() share this syscall + * on Linux; the 6th argument of pselect6 is a { sigset_t *, size_t } pair + * pointer that is NULL when the mask is left alone. + */ +int +select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) +{ + struct timespec ts; + struct timespec *tsp = NULL; + + if (timeout != NULL) + { + if (timeout->tv_sec < 0 || timeout->tv_usec < 0 || timeout->tv_usec >= 1000000L) + { + errno = EINVAL; + return -1; + } + ts.tv_sec = timeout->tv_sec; + ts.tv_nsec = (long)timeout->tv_usec * 1000; + tsp = &ts; + } + return syscall_ret(__syscall6(SYS_pselect6, (long)nfds, (long)readfds, (long)writefds, + (long)exceptfds, (long)tsp, 0L)); +} diff --git a/tests/test_poll.c b/tests/test_poll.c new file mode 100644 index 0000000..0ef8ca0 --- /dev/null +++ b/tests/test_poll.c @@ -0,0 +1,331 @@ +/* + * vlibc — select/pselect/poll/ppoll test (todo 25). + * + * Exercises the fd_set macros and the multiplexing wrappers end to end: + * + * 1. FD_ZERO/FD_SET/FD_CLR/FD_ISSET round-trip, including bit 63/64 (the + * word boundary) and descriptor FD_SETSIZE-1, and fd_set has exactly + * FD_SETSIZE/8 bytes (the kernel bitmap size). + * 2. A pipe holding one byte: poll() returns 1 with POLLIN set, + * select()/pselect() return > 0 with the pipe bit set. + * 3. After the byte is read the pipe is empty: select()/pselect() with a + * 0 timeout return 0 and clear the set, poll() with timeout 0 returns + * 0, and no wait ever hangs. + * 4. Failure scenarios (only the return value, never errno): poll with an + * invalid fds pointer, poll(NULL, 1, 0), poll with an absurd nfds, and + * poll with a big nfds plus a bad pointer all return -1. + * + * Level-2 gated section: ppoll happy paths (data present / 0 timeout). + * + * The negative paths make the LIBRARY write errno (syscall_ret), which + * under a host-linked binary targets glibc's private dtv slot at %fs:0+8. + * In the default mode each such call is bracketed with a save/restore of + * that slot (task 13 technique) — only vlibc/raw-syscall code runs between + * the write and the restore. The test itself NEVER reads errno. The -f mode + * runs the failure scenarios and exits via raw SYS_exit_group (house + * pattern, tests/test_unistd_file.c). + * + * All diagnostics go through raw SYS_write (no stdio): under -Iinclude the + * vlibc public headers shadow GCC's internal ones, so a host header would + * not compile. + */ + +#include + +#include "../include/poll.h" +#include "../include/sys/select.h" +#include "../include/unistd.h" + +#include "../src/internal/syscall.h" + +static int failures; + +/* Write a NUL-terminated string to fd via the raw syscall layer. The + * optimize attribute keeps GCC from lowering the length loop into a + * strlen call, which would leave a vlibc-owned symbol undefined in this + * host-linked standalone binary (house idiom, see src/string). */ +static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +/* Write v in decimal to fd. */ +static void +say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters) +{ + char buf[24]; + int i = (int)sizeof(buf); + + buf[--i] = '\0'; + do + { + buf[--i] = (char)('0' + (v % 10)); + v /= 10; + } while (v != 0); + __syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i)); +} + +static void +check(int cond, const char *what) +{ + if (cond) + { + say(1, "PASS: "); + say(1, what); + say(1, "\n"); + } + else + { + say(2, "FAIL: "); + say(2, what); + say(2, "\n"); + failures++; + } +} + +/* + * Host-TCB slot-1 bracket: the library's errno write on a negative path + * lands at %fs:0+8, glibc's dtv pointer. Save and restore it around each + * such call; only vlibc/raw-syscall code runs in between (task 13 + * technique). + */ +static unsigned long +tcb_slot1(void) +{ + return *(unsigned long *)((char *)__builtin_thread_pointer() + 8); +} + +static void +tcb_slot1_set(unsigned long value) +{ + *(unsigned long *)((char *)__builtin_thread_pointer() + 8) = value; +} + +/* A deliberately invalid fds pointer: address 1 is never mapped. */ +static struct pollfd * +bad_fds(void) +{ + return (struct pollfd *)(unsigned long)1; // NOLINT(performance-no-int-to-ptr) +} + +/* 1. fd_set macro round-trip. */ +static void +fdset_scenarios(void) +{ + fd_set s; + + FD_ZERO(&s); + check(FD_ISSET(0, &s) == 0 && FD_ISSET(63, &s) == 0 && FD_ISSET(1023, &s) == 0, + "FD_ZERO leaves every bit clear"); + FD_SET(7, &s); + FD_SET(1023, &s); + check(FD_ISSET(7, &s) != 0, "FD_ISSET sees FD_SET(7)"); + check(FD_ISSET(1023, &s) != 0, "FD_ISSET sees FD_SET(1023) (last bit)"); + check(FD_ISSET(8, &s) == 0 && FD_ISSET(1022, &s) == 0, "neighboring bits stay clear"); + FD_CLR(7, &s); + check(FD_ISSET(7, &s) == 0 && FD_ISSET(1023, &s) != 0, "FD_CLR(7) clears only bit 7"); + + FD_ZERO(&s); + FD_SET(63, &s); + FD_SET(64, &s); + check(FD_ISSET(63, &s) != 0 && FD_ISSET(64, &s) != 0, + "FD_SET/FD_ISSET across the 64-bit word boundary"); + check(FD_ISSET(62, &s) == 0 && FD_ISSET(65, &s) == 0, "word-boundary neighbors stay clear"); + check(sizeof(fd_set) == (size_t)(FD_SETSIZE / 8), "fd_set is FD_SETSIZE/8 bytes"); +} + +/* 2+3. poll/select/pselect on a pipe, with and without data. */ +static void +poll_select_scenarios(void) +{ + struct pollfd pfd; + struct timeval tv; + struct timespec ts; + fd_set r; + int pr; + char c; + int fds[2]; + + check(pipe(fds) == 0, "pipe returns 0"); + check(write(fds[1], "x", 1) == 1, "write of one byte to the pipe returns 1"); + pr = fds[0]; + + pfd.fd = pr; + pfd.events = POLLIN; + check(poll(&pfd, 1, 1000) == 1, "poll on a pipe with data returns 1"); + check((pfd.revents & POLLIN) != 0, "revents reports POLLIN"); + + FD_ZERO(&r); + FD_SET(pr, &r); + tv.tv_sec = 0; + tv.tv_usec = 0; + check(select(pr + 1, &r, NULL, NULL, &tv) > 0, "select finds the pipe readable"); + check(FD_ISSET(pr, &r) != 0, "select leaves the pipe fd set in readfds"); + + FD_ZERO(&r); + FD_SET(pr, &r); + ts.tv_sec = 0; + ts.tv_nsec = 0; + check(pselect(pr + 1, &r, NULL, NULL, &ts, NULL) > 0, + "pselect with a NULL sigset finds the pipe readable"); + check(FD_ISSET(pr, &r) != 0, "pselect leaves the pipe fd set in readfds"); + + check(read(pr, &c, 1) == 1 && c == 'x', "the piped byte reads back"); + + FD_ZERO(&r); + FD_SET(pr, &r); + tv.tv_sec = 0; + tv.tv_usec = 0; + check(select(pr + 1, &r, NULL, NULL, &tv) == 0, + "select on the now-empty pipe times out with 0"); + check(FD_ISSET(pr, &r) == 0, "empty-pipe select clears the readfds bit"); + + pfd.fd = pr; + pfd.events = POLLIN; + check(poll(&pfd, 1, 0) == 0, "poll with timeout 0 on the empty pipe returns 0"); + check((pfd.revents & POLLIN) == 0, "no POLLIN on the empty pipe"); + + ts.tv_sec = 0; + ts.tv_nsec = 0; + check(pselect(pr + 1, NULL, NULL, NULL, &ts, NULL) == 0, + "pselect with a 0 timeout and all-NULL sets returns 0"); + check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe ends returns 0"); +} + +#if VLIBC_LEVEL_GE(2) + +/* Level-2 gate proof: ppoll happy paths. */ +static void +ppoll_scenarios(void) +{ + struct pollfd pfd; + struct timespec ts; + char c; + int fds[2]; + + check(pipe(fds) == 0, "pipe returns 0 (ppoll)"); + check(write(fds[1], "y", 1) == 1, "write of one byte returns 1 (ppoll)"); + pfd.fd = fds[0]; + pfd.events = POLLIN; + ts.tv_sec = 1; + ts.tv_nsec = 0; + check(ppoll(&pfd, 1, &ts, NULL) == 1, "ppoll on a pipe with data returns 1"); + check((pfd.revents & POLLIN) != 0, "ppoll revents reports POLLIN"); + ts.tv_sec = 0; + ts.tv_nsec = 0; + check(ppoll(&pfd, 1, &ts, NULL) == 1, "ppoll with a 0 timeout on unread data still returns 1"); + check(read(fds[0], &c, 1) == 1 && c == 'y', "the ppoll byte reads back"); + check(ppoll(&pfd, 1, &ts, NULL) == 0, "ppoll with a 0 timeout on the empty pipe returns 0"); + check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe ends returns 0 (ppoll)"); +} + +#endif /* VLIBC_LEVEL_GE(2) */ + +/* 4. Negative paths: -1 assertions only, errno bracketed (see above). */ +static void +negative_scenarios(void) +{ + unsigned long saved; + struct pollfd one; + + one.fd = 0; + one.events = POLLIN; + saved = tcb_slot1(); + check(poll(bad_fds(), 1, 0) == -1, "poll with an invalid fds pointer returns -1"); + tcb_slot1_set(saved); + saved = tcb_slot1(); + check(poll(NULL, 1, 0) == -1, "poll(NULL, 1, 0) returns -1"); + tcb_slot1_set(saved); + saved = tcb_slot1(); + check(poll(&one, (nfds_t)1000000000, 0) == -1, "poll with an absurd nfds returns -1"); + tcb_slot1_set(saved); + saved = tcb_slot1(); + check(poll(bad_fds(), (nfds_t)1000000000, 0) == -1, + "poll with a big nfds and a bad pointer returns -1"); + tcb_slot1_set(saved); +} + +/* + * Failure scenarios (-f): every assertion is on the return value only, and + * the process exits through raw SYS_exit_group because the library writes + * errno on these paths (host-TCB hazard). + */ +static int +failure_scenarios(void) +{ + struct pollfd one; + int rc = 0; + + one.fd = 0; + one.events = POLLIN; + if (poll(bad_fds(), 1, 0) != -1) + { + say(2, "FAIL: poll with an invalid fds pointer did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: poll with an invalid fds pointer -> -1\n"); + } + if (poll(NULL, 1, 0) != -1) + { + say(2, "FAIL: poll(NULL, 1, 0) did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: poll(NULL, 1, 0) -> -1\n"); + } + if (poll(&one, (nfds_t)1000000000, 0) != -1) + { + say(2, "FAIL: poll with an absurd nfds did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: poll with an absurd nfds -> -1\n"); + } + return rc; +} + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + int rc; + + /* + * The failure scenarios write errno inside the library; under the + * host libc that slot is glibc's private TLS state, so leave via + * the raw syscall without running host cleanup. + */ + rc = failure_scenarios(); + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + fdset_scenarios(); + poll_select_scenarios(); +#if VLIBC_LEVEL_GE(2) + ppoll_scenarios(); +#endif + negative_scenarios(); + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all poll/select tests passed\n"); + return 0; +}