458 lines
15 KiB
C
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;
|
|
}
|