Files

458 lines
15 KiB
C

/*
* vlibc — fcntl test (todo 21).
*
* Exercises the fcntl.h surface end to end:
*
* 1. fcntl F_GETFL/F_SETFL on a temp file: the access mode matches
* O_RDWR and F_SETFL can add O_NONBLOCK.
* 2. F_DUPFD with arg 10 returns a descriptor >= 10 that shares the file
* description (a write through the dup is visible as a shared offset
* through the original); F_DUPFD_CLOEXEC returns >= 20 with
* FD_CLOEXEC already set.
* 3. F_GETFD starts at 0; F_SETFD FD_CLOEXEC then F_GETFD == FD_CLOEXEC.
* 4. posix_fallocate preallocates (size >= 4096 via lseek SEEK_END) and
* returns an error NUMBER directly (EINVAL for len 0 — no errno
* involved, so this is safe in the default mode).
* 5. posix_fadvise returns 0 on the happy paths.
* 6. open with O_CLOEXEC leaves FD_CLOEXEC set.
* 7. creat creates, truncates a pre-existing file, and honors the mode
* (write + reopen + read round trip; mode itself needs stat, todo 22).
*
* Level-2 gated section: lockf — F_TLOCK/F_TEST/F_LOCK/F_ULOCK over the
* fcntl record locks, including the second-descriptor conflict and the
* same-process F_TEST short-circuit.
*
* The negative paths that make the LIBRARY write errno (fcntl with a bad
* fd, lockf F_TLOCK conflicts, lockf bad cmd) are bracketed with a
* save/restore of host-TCB slot 1 (task 13 technique) in the default mode;
* the test itself NEVER reads errno. The -f mode runs the failure
* scenarios and exits via raw SYS_exit_group (house pattern).
*
* 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. Not part of the library proper; compiled manually for this
* todo (the tests/ + make check wiring is owned by a later todo).
*/
#include <stddef.h>
#include "../include/fcntl.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++;
}
}
#if VLIBC_LEVEL_GE(2)
/*
* 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). Only the level-2 lockf scenarios have such negative paths in
* the default mode, so the helpers are gated with them.
*/
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;
}
#endif /* VLIBC_LEVEL_GE(2) */
/* 1+2+3: F_GETFL/F_SETFL, F_DUPFD(_CLOEXEC), F_GETFD/F_SETFD. */
static void
cmd_scenarios(int fd)
{
char b[1];
int dupfd;
int cloexec;
int fl;
if (fd < 0)
{
return;
}
fl = fcntl(fd, F_GETFL);
check(fl >= 0, "F_GETFL on a valid descriptor returns flags");
check((fl & O_ACCMODE) == O_RDWR, "F_GETFL access mode matches the O_RDWR open");
check(fcntl(fd, F_SETFL, fl | O_NONBLOCK) == 0, "F_SETFL adding O_NONBLOCK returns 0");
check((fcntl(fd, F_GETFL) & O_NONBLOCK) != 0, "F_GETFL now reports O_NONBLOCK");
check(fcntl(fd, F_GETFD) == 0, "F_GETFD starts at 0 on a plain open");
check(fcntl(fd, F_SETFD, FD_CLOEXEC) == 0, "F_SETFD FD_CLOEXEC returns 0");
check(fcntl(fd, F_GETFD) == FD_CLOEXEC, "F_GETFD reports FD_CLOEXEC after F_SETFD");
dupfd = fcntl(fd, F_DUPFD, 10);
check(dupfd >= 10, "F_DUPFD with arg 10 returns a descriptor >= 10");
if (dupfd < 10)
{
return;
}
check(lseek(fd, 0, SEEK_SET) == 0, "lseek to 0 before the shared-offset probe");
check(__syscall3(SYS_write, dupfd, (long)"Q", 1) == 1, "write through the dup writes 1 byte");
check(lseek(fd, 0, SEEK_CUR) == 1, "the dup shares the file offset with the original");
check(lseek(fd, 0, SEEK_SET) == 0, "lseek back to 0 for the shared-offset read-back");
check(__syscall3(SYS_read, fd, (long)b, 1) == 1 && b[0] == 'Q',
"the byte written through the dup reads back through the original");
check(close(dupfd) == 0, "close of the F_DUPFD descriptor returns 0");
cloexec = fcntl(fd, F_DUPFD_CLOEXEC, 20);
check(cloexec >= 20, "F_DUPFD_CLOEXEC with arg 20 returns a descriptor >= 20");
if (cloexec >= 20)
{
check(fcntl(cloexec, F_GETFD) == FD_CLOEXEC, "F_DUPFD_CLOEXEC sets FD_CLOEXEC");
check(close(cloexec) == 0, "close of the F_DUPFD_CLOEXEC descriptor returns 0");
}
}
/* 4+5. posix_fallocate/posix_fadvise error-number and sizing behavior. */
static void
fallocate_scenarios(int fd)
{
if (fd < 0)
{
return;
}
/* len 0 → kernel EINVAL (22), returned DIRECTLY by the POSIX
* convention — no errno write, so no host-TCB bracket is needed. */
check(posix_fallocate(fd, 0, 0) == 22, "posix_fallocate with len 0 returns EINVAL directly");
check(posix_fallocate(fd, 0, 4096) == 0, "posix_fallocate(fd, 0, 4096) returns 0");
check(lseek(fd, 0, SEEK_END) >= 4096, "lseek SEEK_END >= 4096 after posix_fallocate");
check(lseek(fd, 0, SEEK_SET) == 0, "lseek back to 0 after the size check");
check(posix_fadvise(fd, 0, 0, POSIX_FADV_NORMAL) == 0,
"posix_fadvise POSIX_FADV_NORMAL returns 0");
check(posix_fadvise(fd, 0, 4096, POSIX_FADV_WILLNEED) == 0,
"posix_fadvise POSIX_FADV_WILLNEED returns 0");
}
/* 6. O_CLOEXEC on open leaves FD_CLOEXEC visible. */
static void
cloexec_open_scenario(const char *path)
{
int fd;
fd = open(path, O_RDWR | O_CLOEXEC);
check(fd >= 0, "open with O_CLOEXEC succeeds");
if (fd >= 0)
{
check((fcntl(fd, F_GETFD) & FD_CLOEXEC) != 0, "O_CLOEXEC open has FD_CLOEXEC set");
check(close(fd) == 0, "close of the O_CLOEXEC descriptor returns 0");
}
}
/* 7. creat: create, truncate on re-create, write/reopen/read round trip. */
static void
creat_scenario(const char *path)
{
char b[5];
long n;
int fd;
fd = creat(path, 0600);
check(fd >= 0, "creat creates the file and returns a descriptor");
if (fd < 0)
{
return;
}
check(__syscall3(SYS_write, fd, (long)"hello", 5) == 5, "write 5 bytes through the creat fd");
check(close(fd) == 0, "close of the creat fd returns 0");
/* Re-create: O_TRUNC must reset the contents. */
fd = creat(path, 0600);
check(fd >= 0, "creat on the existing file re-opens it");
if (fd < 0)
{
return;
}
check(__syscall3(SYS_write, fd, (long)"yo", 2) == 2, "write 2 bytes through the re-creat fd");
check(close(fd) == 0, "close of the re-creat fd returns 0");
fd = open(path, O_RDONLY);
check(fd >= 0, "reopen the creat file read-only");
if (fd < 0)
{
return;
}
check(lseek(fd, 0, SEEK_END) == 2, "creat truncated the previous contents (size 2)");
check(lseek(fd, 0, SEEK_SET) == 0, "lseek back to 0 for the read-back");
n = __syscall3(SYS_read, fd, (long)b, 5);
check(n == 2 && b[0] == 'y' && b[1] == 'o', "the re-created file holds the new bytes");
check(close(fd) == 0, "close of the read-only descriptor returns 0");
check(__syscall3(SYS_unlinkat, AT_FDCWD, (long)path, 0) == 0,
"unlink of the creat file returns 0");
}
#if VLIBC_LEVEL_GE(2)
/*
* Level-2 gate: lockf over the fcntl record locks.
*
* Conflict semantics note: on modern Linux (since the 2013 POSIX-lock
* rework, kernel ~3.15) fcntl record locks are OWNED BY THE PROCESS
* (fl_owner = the files_struct), not by the open file description — so two
* descriptors of the same process never conflict, exactly as POSIX says.
* The cross-process conflict below is therefore exercised with a child
* process via raw SYS_pipe/SYS_fork/SYS_wait4 (the process wrappers are
* todo 20's, not available here). Verified empirically: F_SETLK through a
* second descriptor of the same process succeeds on this kernel (and under
* glibc), so a same-process two-fd conflict test would be wrong.
*/
static void
lockf_scenarios(const char *path, int fd)
{
unsigned long saved;
int sig[2];
int go[2];
char b = 0;
long st = 0;
long child;
int fd2;
if (fd < 0)
{
return;
}
check(lseek(fd, 0, SEEK_SET) == 0, "lseek the main fd to 0 for lockf");
/* Same process, same fd: overlapping requests merge, never conflict. */
check(lockf(fd, F_TLOCK, 0) == 0, "lockf F_TLOCK from offset 0 succeeds");
check(lockf(fd, F_TLOCK, 0) == 0, "a second F_TLOCK by the same process merges");
check(lockf(fd, F_TEST, 0) == 0, "F_TEST by the holding process itself returns 0");
check(lockf(fd, F_ULOCK, 0) == 0, "F_ULOCK releases the region");
check(lockf(fd, F_LOCK, 16) == 0, "F_LOCK of 16 bytes acquires without contention");
check(lockf(fd, F_TEST, 16) == 0, "F_TEST of the region held by this process returns 0");
check(lockf(fd, F_ULOCK, 16) == 0, "F_ULOCK of the 16-byte region returns 0");
saved = tcb_slot1();
check(lockf(fd, 999, 0) == -1, "lockf with an invalid cmd returns -1");
tcb_slot1_set(saved);
/*
* Cross-process conflict. Two pipes (not one): a single pipe would let
* the child's own read steal the handshake byte it just wrote — both
* processes park on the same read end and the kernel wakes one of
* them, so the byte is not guaranteed to reach the parent. Each side
* closes the end it must never touch.
*/
check(__syscall1(SYS_pipe, (long)sig) == 0, "pipe for the lockf signal returns 0");
check(__syscall1(SYS_pipe, (long)go) == 0, "pipe for the lockf go returns 0");
child = __syscall0(SYS_fork);
if (child == 0)
{
/*
* Child: hold the lock, tell the parent, wait for the go. Exits
* through the raw syscall; if the lock cannot be taken, "F" is
* sent instead so the parent never blocks forever.
*/
__syscall1(SYS_close, sig[0]);
__syscall1(SYS_close, go[1]);
fd2 = open(path, O_RDWR);
if (fd2 < 0 || lockf(fd2, F_LOCK, 0) != 0)
{
__syscall3(SYS_write, sig[1], (long)"F", 1);
__syscall1(SYS_exit_group, 1);
return; /* not reached */
}
__syscall3(SYS_write, sig[1], (long)"L", 1);
__syscall3(SYS_read, go[0], (long)&b, 1);
__syscall1(SYS_exit_group, 0);
return; /* not reached */
}
check(child > 0, "fork returned a child pid");
if (child < 0)
{
return;
}
__syscall1(SYS_close, sig[1]);
__syscall1(SYS_close, go[0]);
__syscall3(SYS_read, sig[0], (long)&b, 1);
check(b == 'L', "the child acquired the lock");
if (b == 'L')
{
saved = tcb_slot1();
check(lockf(fd, F_TLOCK, 0) == -1, "F_TLOCK conflicts with the child's lock -> -1");
tcb_slot1_set(saved);
saved = tcb_slot1();
check(lockf(fd, F_TEST, 0) == -1, "F_TEST finds the child's lock -> -1");
tcb_slot1_set(saved);
}
__syscall3(SYS_write, go[1], (long)"G", 1);
__syscall4(SYS_wait4, child, (long)&st, 0, 0);
}
#endif /* VLIBC_LEVEL_GE(2) */
/*
* Failure scenarios (-f): every assertion is on the return value only, and
* the process exits through raw SYS_exit_group because fcntl/creat/lockf
* write errno on these paths (host-TCB hazard). EBADF is 9 and comes back
* DIRECTLY from posix_fadvise/posix_fallocate — no errno involved there.
*/
static int
failure_scenarios(void)
{
int rc = 0;
if (fcntl(-1, F_GETFL) != -1)
{
say(2, "FAIL: fcntl(-1, F_GETFL) did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: fcntl(-1, F_GETFL) -> -1\n");
}
if (fcntl(-1, F_SETFL, O_NONBLOCK) != -1)
{
say(2, "FAIL: fcntl(-1, F_SETFL, O_NONBLOCK) did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: fcntl(-1, F_SETFL, O_NONBLOCK) -> -1\n");
}
if (creat("/nonexistent/vlibc/t21", 0600) != -1)
{
say(2, "FAIL: creat on a nonexistent directory did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: creat on a nonexistent directory -> -1\n");
}
if (posix_fallocate(-1, 0, 4096) != 9)
{
say(2, "FAIL: posix_fallocate(-1, 0, 4096) did not return EBADF (9)\n");
rc = 1;
}
else
{
say(1, "PASS: posix_fallocate(-1, 0, 4096) -> EBADF (9) directly\n");
}
if (posix_fadvise(-1, 0, 0, POSIX_FADV_NORMAL) != 9)
{
say(2, "FAIL: posix_fadvise(-1, 0, 0, POSIX_FADV_NORMAL) did not return EBADF (9)\n");
rc = 1;
}
else
{
say(1, "PASS: posix_fadvise(-1, 0, 0, POSIX_FADV_NORMAL) -> EBADF (9) directly\n");
}
#if VLIBC_LEVEL_GE(2)
if (lockf(-1, F_TLOCK, 0) != -1)
{
say(2, "FAIL: lockf(-1, F_TLOCK, 0) did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: lockf(-1, F_TLOCK, 0) -> -1\n");
}
#endif
return rc;
}
int
main(int argc, char **argv)
{
const char *path = "/tmp/vlibc-t21";
const char *cpath = "/tmp/vlibc-t21-creat";
int rc;
int fd;
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
/*
* 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 */
}
fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
check(fd >= 0, "open O_RDWR|O_CREAT|O_TRUNC 0600 returns a descriptor");
cmd_scenarios(fd);
fallocate_scenarios(fd);
cloexec_open_scenario(path);
creat_scenario(cpath);
#if VLIBC_LEVEL_GE(2)
lockf_scenarios(path, fd);
#endif
if (fd >= 0)
{
check(close(fd) == 0, "close of the main temp file returns 0");
}
check(__syscall3(SYS_unlinkat, AT_FDCWD, (long)path, 0) == 0,
"unlink of the temp file returns 0");
if (failures > 0)
{
say(2, "FAILED (");
say_dec(2, (unsigned long)failures);
say(2, " check(s))\n");
return 1;
}
say(1, "all fcntl tests passed\n");
return 0;
}