diff --git a/include/fcntl.h b/include/fcntl.h new file mode 100644 index 0000000..bc5be16 --- /dev/null +++ b/include/fcntl.h @@ -0,0 +1,206 @@ +#ifndef VLIBC_FCNTL_H +#define VLIBC_FCNTL_H + +/* + * vlibc — . + * + * File control, record locks, and the open-flag constants (POSIX.1-2008 plus + * the Linux x86_64 additions). Every O_*, F_*, AT_* and POSIX_FADV_* value + * below is a Linux kernel UAPI fact for x86_64 (asm-generic/fcntl.h), transcribed in + * hex with the kernel's octal form in a comment — not an invented number. + * O_TMPFILE and O_SYNC are defined as flag combinations exactly as the + * kernel defines them. + * + * Note on the *at constants: on x86_64 AT_EACCESS == AT_REMOVEDIR == 0x200; + * the kernel disambiguates by syscall (faccessat vs unlinkat), so both names + * carry the same value here. + * + * open/openat are deliberately NOT declared here: POSIX places them in this + * header, but this project's single declaration site is (todo 19; + * see its header comment). creat() lives here — its canonical POSIX home. + * + * Level 2 (muslmimic/XSI): lockf plus F_LOCK/F_TLOCK/F_ULOCK/F_TEST. POSIX + * puts lockf in ; that file is todo 19's and out of scope here, + * and musl declares lockf in as well — the constants accompany the + * declaration so the header stays self-contained. A later integration pass + * may re-export the same declaration from (identical, harmless). + * + * None of these declarations carry an intent attribute: every function + * performs I/O with side effects and reports failures through errno (or, + * for posix_fadvise/posix_fallocate, an error-number return), so const/pure + * would be unsound — the same rationale unistd.h documents for its I/O + * family. + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- open(2) flag bits ---- */ + +/* Mask for the O_RDONLY/O_WRONLY/O_RDWR access mode. */ +#define O_ACCMODE 0x3 + +#define O_RDONLY 0x0 /* 00000000 */ +#define O_WRONLY 0x1 /* 00000001 */ +#define O_RDWR 0x2 /* 00000002 */ + +#define O_CREAT 0x40 /* 00000100 */ +#define O_EXCL 0x80 /* 00000200 */ +#define O_NOCTTY 0x100 /* 00000400 */ +#define O_TRUNC 0x200 /* 00001000 */ +#define O_APPEND 0x400 /* 00002000 */ +#define O_NONBLOCK 0x800 /* 00004000 */ +#define O_DSYNC 0x1000 /* 00010000 */ +#define O_ASYNC 0x2000 /* 00020000 */ +#define O_DIRECT 0x4000 /* 00040000 */ +#define O_LARGEFILE 0x8000 /* 00100000 */ + +#define O_DIRECTORY 0x10000 /* 00200000 */ +#define O_NOFOLLOW 0x20000 /* 00400000 */ +#define O_NOATIME 0x40000 /* 01000000 */ +#define O_CLOEXEC 0x80000 /* 02000000 */ + +/* + * __O_SYNC/__O_TMPFILE are Linux-kernel names in the implementation-reserved + * namespace (this libc IS "the implementation", and the kernel UAPI mandates + * the exact spellings) — the NOLINT below is the house waiver for that. + */ +#define __O_SYNC 0x100000 /* NOLINT(bugprone-reserved-identifier) 04000000 */ +#define O_SYNC (__O_SYNC | O_DSYNC) +#define O_PATH 0x200000 /* 010000000 */ +#define __O_TMPFILE 0x400000 /* NOLINT(bugprone-reserved-identifier) 020000000 */ +#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY) + +/* ---- fcntl(2) commands ---- */ + +#define F_DUPFD 0 +#define F_GETFD 1 +#define F_SETFD 2 +#define F_GETFL 3 +#define F_SETFL 4 +#define F_GETLK 5 +#define F_SETLK 6 +#define F_SETLKW 7 +#define F_SETOWN 8 +#define F_GETOWN 9 +#define F_SETSIG 10 +#define F_GETSIG 11 + +/* Linux-specific owner-identity commands (constants only; struct f_owner_ex + * is out of POSIX scope and not provided). */ +#define F_SETOWN_EX 15 +#define F_GETOWN_EX 16 +#define F_GETOWNER_UIDS 17 + +/* Open-file-description (OFD) locks, Linux 3.15+. */ +#define F_OFD_GETLK 36 +#define F_OFD_SETLK 37 +#define F_OFD_SETLKW 38 + +#define F_DUPFD_CLOEXEC 1030 /* F_LINUX_SPECIFIC_BASE (1024) + 6 */ + +/* Close the descriptor on exec — the F_SETFD/F_GETFD flag. */ +#define FD_CLOEXEC 1 + +/* Record-lock types (struct flock l_type). */ +#define F_RDLCK 0 +#define F_WRLCK 1 +#define F_UNLCK 2 + +/* ---- *at(2) base-directory and behavior flags ---- */ + +#define AT_FDCWD (-100) +#define AT_SYMLINK_NOFOLLOW 0x100 +#define AT_REMOVEDIR 0x200 +#define AT_SYMLINK_FOLLOW 0x400 +#define AT_EACCESS 0x200 + +/* ---- posix_fadvise(2) advice values ---- */ + +#define POSIX_FADV_NORMAL 0 +#define POSIX_FADV_RANDOM 1 +#define POSIX_FADV_SEQUENTIAL 2 +#define POSIX_FADV_WILLNEED 3 +#define POSIX_FADV_DONTNEED 4 +#define POSIX_FADV_NOREUSE 5 + +/* + * Record-lock descriptor, ABI-identical to the x86_64 kernel layout + * (asm-generic/fcntl.h: short, short, long, long, int — 32 bytes with + * natural padding). On x86_64 the LFS and non-LFS layouts are one struct, + * so there is no separate flock64 here. + */ +struct flock +{ + short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */ + short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END (from ) */ + off_t l_start; /* relative offset of the locked region */ + off_t l_len; /* region length; 0 means through EOF */ + pid_t l_pid; /* PID of the process holding the lock (F_GETLK) */ +}; + +/* + * Perform one of the F_* control operations on fildes. Only the commands + * that take a third argument read it from the varargs; all others pass 0, + * which the kernel ignores. Return the command-specific result, or -1 with + * errno set. + */ +int +fcntl(int fildes, int cmd, ...); + +/* + * Equivalent to open(path, O_WRONLY | O_CREAT | O_TRUNC, mode): create + * path for writing, truncating any existing file, with mode masked by the + * process umask. Return a file descriptor, or -1 with errno set. + */ +int +creat(const char *path, mode_t mode); + +/* + * Announce an expected access pattern for the range [offset, offset+len) of + * fd (len 0 means through EOF). Unlike the rest of the family this returns + * an error NUMBER directly — 0 on success, else the positive errno value + * (e.g. EBADF, ESPIPE) — and errno is untouched (POSIX). + */ +int +posix_fadvise(int fd, off_t offset, off_t len, int advice); + +/* + * Ensure storage is allocated for the range [offset, offset+len) of fd, + * growing the file as needed. Returns an error number directly (0 on + * success) and leaves errno untouched, like posix_fadvise (POSIX). + */ +int +posix_fallocate(int fd, off_t offset, off_t len); + +#if VLIBC_LEVEL_GE(2) + +/* lockf() commands (POSIX XSI; the kernel has no lockf syscall — these are + * userspace cmd values). */ +#define F_ULOCK 0 /* unlock a previously locked region */ +#define F_LOCK 1 /* lock a region, blocking until available */ +#define F_TLOCK 2 /* try to lock; -1 with EACCES/EAGAIN if held */ +#define F_TEST 3 /* test a region for another process's lock */ + +/* + * Apply or remove an exclusive POSIX record lock on [current offset, + * current offset + len) of fd (len 0 means through EOF). XSI. Return 0, or + * -1 with errno set. + */ +int +lockf(int fd, int cmd, off_t len); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_FCNTL_H */ diff --git a/src/fcntl/creat.c b/src/fcntl/creat.c new file mode 100644 index 0000000..4791e2b --- /dev/null +++ b/src/fcntl/creat.c @@ -0,0 +1,21 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * creat ≡ open(path, O_WRONLY | O_CREAT | O_TRUNC, mode) (POSIX). Done as + * a direct SYS_openat rather than a call to open(): the flag set is fixed, + * the mode is always supplied, and this keeps src/fcntl self-contained + * with no inter-object dependency on src/unistd. The kernel applies the + * process umask to mode. + */ +int +creat(const char *path, mode_t mode) +{ + return syscall_ret( + __syscall4(SYS_openat, AT_FDCWD, (long)path, O_WRONLY | O_CREAT | O_TRUNC, mode)); +} diff --git a/src/fcntl/fcntl.c b/src/fcntl/fcntl.c new file mode 100644 index 0000000..67696e3 --- /dev/null +++ b/src/fcntl/fcntl.c @@ -0,0 +1,55 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * fcntl over SYS_fcntl (72). The third argument is read from the varargs + * only for the commands POSIX defines one for (F_DUPFD, F_DUPFD_CLOEXEC, + * F_SETFD, F_SETFL, the lock commands, and the owner/signal commands); + * every other command passes 0, which the kernel ignores. Pointers (the + * struct flock * of F_GETLK/F_SETLK/F_SETLKW/F_OFD_*) ride the varargs + * slot as a long — on x86_64 long and void * share one GPR slot, so the + * read is ABI-exact. + * + * F_DUPFD_CLOEXEC passes straight through: the x86_64 kernel has supported + * it as a single native operation since 2.6.24, so no F_DUPFD + F_SETFD + * fallback is needed. F_SETFL needs no O_LARGEFILE massaging on x86_64 + * (that is a 32-bit compat concern only). + */ +int +fcntl(int fildes, int cmd, ...) +{ + long arg = 0; + va_list ap; + + switch (cmd) + { + case F_DUPFD: + case F_DUPFD_CLOEXEC: + case F_SETFD: + case F_SETFL: + case F_GETLK: + case F_SETLK: + case F_SETLKW: + case F_SETOWN: + case F_SETSIG: + case F_GETOWN_EX: + case F_SETOWN_EX: + case F_OFD_GETLK: + case F_OFD_SETLK: + case F_OFD_SETLKW: + va_start(ap, cmd); + arg = va_arg(ap, long); + va_end(ap); + break; + default: + break; + } + return syscall_ret(__syscall3(SYS_fcntl, fildes, cmd, arg)); +} diff --git a/src/fcntl/lockf.c b/src/fcntl/lockf.c new file mode 100644 index 0000000..25e8bee --- /dev/null +++ b/src/fcntl/lockf.c @@ -0,0 +1,70 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "../internal/syscall.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * lockf (XSI) over the fcntl record-lock commands: the kernel has no lockf + * syscall, so every cmd maps to a struct flock operation on the range + * [current offset, current offset + len) — the POSIX definition. len 0 + * means through EOF, which the kernel's l_len 0 convention already + * expresses. + * + * The region start uses l_whence = SEEK_CUR with l_start = 0: the kernel + * resolves the current file offset at syscall time, atomically with the + * lock operation, so no lseek round-trip (and no TOCTOU window) is needed. + * + * F_TEST probes with F_GETLK using a read lock (the probe type that + * conflicts with any exclusive lock): if the kernel reports no lock, or + * the reported holder is this process itself (a different descriptor of + * ours may hold it — POSIX says our own process never conflicts with + * itself), the region is lockable. The pid is read via raw SYS_getpid; + * the process wrappers are todo 20's and not a dependency of this file. + */ +int +lockf(int fd, int cmd, off_t len) +{ + struct flock lk; + + lk.l_type = F_WRLCK; + lk.l_whence = SEEK_CUR; + lk.l_start = 0; + lk.l_len = len; + + switch (cmd) + { + case F_TEST: + lk.l_type = F_RDLCK; + if (fcntl(fd, F_GETLK, &lk) == -1) + { + return -1; + } + if (lk.l_type == F_UNLCK || lk.l_pid == (pid_t)__syscall0(SYS_getpid)) + { + return 0; + } + errno = EACCES; + return -1; + case F_ULOCK: + lk.l_type = F_UNLCK; + return fcntl(fd, F_SETLK, &lk); + case F_LOCK: + return fcntl(fd, F_SETLKW, &lk); + case F_TLOCK: + return fcntl(fd, F_SETLK, &lk); + default: + errno = EINVAL; + return -1; + } +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/fcntl/posix_fadvise.c b/src/fcntl/posix_fadvise.c new file mode 100644 index 0000000..38d8259 --- /dev/null +++ b/src/fcntl/posix_fadvise.c @@ -0,0 +1,24 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * posix_fadvise over SYS_fadvise64 (221). POSIX return convention, unlike + * the rest of the family: an error NUMBER directly — 0 on success, else + * the positive errno value (EBADF, ESPIPE, EINVAL, ...) — and errno is + * left untouched. The raw syscall result is therefore mapped without the + * syscall_ret() translation (which would write errno): a negative return + * is negated, 0 stays 0. On x86_64 the syscall takes the offset as one + * 64-bit value (fd, offset, len, advice) — no lo/hi split. + */ +int +posix_fadvise(int fd, off_t offset, off_t len, int advice) +{ + long r = __syscall4(SYS_fadvise64, fd, offset, len, advice); + + return r < 0 ? (int)-r : 0; +} diff --git a/src/fcntl/posix_fallocate.c b/src/fcntl/posix_fallocate.c new file mode 100644 index 0000000..c556303 --- /dev/null +++ b/src/fcntl/posix_fallocate.c @@ -0,0 +1,24 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * posix_fallocate over SYS_fallocate (285). The kernel signature is + * (fd, mode, offset, len); mode is 0 — POSIX exposes no FALLOC_FL_* flags, + * so the plain allocate-and-grow operation is all there is (the kernel + * rejects any other mode with EOPNOTSUPP/EINVAL, which is exactly what + * POSIX wants). Same error-number return convention as posix_fadvise: + * 0 on success, the positive errno value on failure, errno untouched — + * so the raw result is negated directly, no syscall_ret(). + */ +int +posix_fallocate(int fd, off_t offset, off_t len) +{ + long r = __syscall4(SYS_fallocate, fd, 0, offset, len); + + return r < 0 ? (int)-r : 0; +} diff --git a/tests/test_fcntl.c b/tests/test_fcntl.c new file mode 100644 index 0000000..542c539 --- /dev/null +++ b/tests/test_fcntl.c @@ -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 + +#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; +}