feat(select): poll/select and fd_set
This commit is contained in:
@@ -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 <stddef.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user