/* * 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 #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; }