feat(fcntl): fcntl/open flag handling
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user