diff --git a/include/unistd.h b/include/unistd.h new file mode 100644 index 0000000..deecc51 --- /dev/null +++ b/include/unistd.h @@ -0,0 +1,214 @@ +#ifndef VLIBC_UNISTD_H +#define VLIBC_UNISTD_H + +/* + * vlibc — . + * + * File descriptors, file I/O, and the access/whence symbolic constants + * (POSIX.1-2008). Every function here is an unbuffered pass-through to the + * kernel: failures are reported as -1 (or the fd/offset on success) with + * errno set by the syscall layer. + * + * Level 1 (onlyposix): read, write, pread, pwrite, open, openat, close, + * lseek, dup, dup2, pipe, fsync, fdatasync, ftruncate, + * sync, access, faccessat. + * Level 2 (muslmimic): dup3, pipe2 (Linux extensions), truncate (XSI), + * lseek64 (glibc LFS alias of lseek on x86_64). + * + * The open-flag constants (O_RDONLY, O_CREAT, O_CLOEXEC, ...) belong to + * and are deliberately not defined here; the oflag arguments below + * are plain int and take their values from that header. The optional mode + * argument of open/openat is a mode_t supplied only when oflag contains + * O_CREAT or O_TMPFILE. + * + * None of these declarations carry an intent attribute: every function + * performs I/O with side effects and reports failures through errno, so + * const/pure would be unsound. + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Level 1 (POSIX base). */ + +/* Access-check modes for access() and faccessat(). */ +#define F_OK 0 /* existence only */ +#define X_OK 1 /* execute (search for a directory) */ +#define W_OK 2 /* write */ +#define R_OK 4 /* read */ + +/* whence values for lseek() and lseek64() (also defined by ). */ +#define SEEK_SET 0 /* from the beginning of the file */ +#define SEEK_CUR 1 /* from the current position */ +#define SEEK_END 2 /* from the end of the file */ + +/* + * Read up to nbyte bytes from fildes into buf and return the number of + * bytes read, 0 at end of file, or -1 with errno set on error. Unbuffered. + */ +ssize_t +read(int fildes, void *buf, size_t nbyte); + +/* + * Write up to nbyte bytes from buf to fildes and return the number of + * bytes written, or -1 with errno set on error. Unbuffered. + */ +ssize_t +write(int fildes, const void *buf, size_t nbyte); + +/* + * Read nbyte bytes from fildes starting at offset, without changing the + * file position; return the number of bytes read, or -1 with errno set. + */ +ssize_t +pread(int fildes, void *buf, size_t nbyte, off_t offset); + +/* + * Write nbyte bytes from buf to fildes starting at offset, without + * changing the file position; return the number of bytes written, or -1 + * with errno set. + */ +ssize_t +pwrite(int fildes, const void *buf, size_t nbyte, off_t offset); + +/* + * Open path with the oflag access mode (from ) and return a file + * descriptor, or -1 with errno set. A mode argument is required — and read + * from the varargs — only when oflag contains O_CREAT or O_TMPFILE. + */ +int +open(const char *path, int oflag, ...); + +/* + * Like open(), but path is relative to the directory named by fd (use + * AT_FDCWD from for the current working directory). The mode + * varargs rule is the same as open(). + */ +int +openat(int fd, const char *path, int oflag, ...); + +/* + * Close the file descriptor fildes; return 0, or -1 with errno set. + */ +int +close(int fildes); + +/* + * Reposition the file offset of fildes to offset per whence (SEEK_SET, + * SEEK_CUR, SEEK_END) and return the resulting offset, or (off_t)-1 with + * errno set. The full 64-bit offset is returned; errno is untouched on + * success. + */ +off_t +lseek(int fildes, off_t offset, int whence); + +/* + * Duplicate fildes to the lowest-numbered free descriptor; return it, or + * -1 with errno set. The copy shares the file description (position, + * flags, locks) with the original. + */ +int +dup(int fildes); + +/* + * Duplicate fildes onto fildes2, closing fildes2 first if it was open; + * return fildes2, or -1 with errno set. dup2(f, f) returns f without + * doing anything (POSIX). + */ +int +dup2(int fildes, int fildes2); + +/* + * Create a pipe: fildes[0] becomes the read end, fildes[1] the write end. + * Return 0, or -1 with errno set. No descriptor flags are set (unlike + * pipe2, this is plain POSIX). + */ +int +pipe(int fildes[2]); + +/* + * Flush all buffered modifications of fildes and its metadata to stable + * storage; return 0, or -1 with errno set. + */ +int +fsync(int fildes); + +/* + * Like fsync(), but may skip the metadata work needed only to preserve + * file contents; return 0, or -1 with errno set. + */ +int +fdatasync(int fildes); + +/* + * Truncate fildes to length bytes; return 0, or -1 with errno set. + */ +int +ftruncate(int fildes, off_t length); + +/* + * Flush all filesystem caches to stable storage. Returns nothing. + */ +void +sync(void); + +/* + * Check accessibility of path under amode (R_OK, W_OK, X_OK, F_OK); return + * 0, or -1 with errno set. Uses the real IDs of the calling process. + */ +int +access(const char *path, int amode); + +/* + * Like access(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory) and flag may hold + * AT_EACCESS; return 0, or -1 with errno set. + */ +int +faccessat(int fd, const char *path, int amode, int flag); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): Linux extensions + XSI. */ + +/* + * Like dup2(), but with descriptor flags (O_CLOEXEC from ) applied + * atomically; return fildes2, or -1 with errno set. Linux-specific. + */ +int +dup3(int fildes, int fildes2, int flags); + +/* + * Like pipe(), but with descriptor flags (e.g. O_CLOEXEC) applied + * atomically; return 0, or -1 with errno set. Linux-specific. + */ +int +pipe2(int fildes[2], int flags); + +/* + * Truncate the file named by path to length bytes; return 0, or -1 with + * errno set. XSI. + */ +int +truncate(const char *path, off_t length); + +/* + * glibc LFS alias of lseek(): on x86_64 the LFS and non-LFS off_t are + * identical (both 64-bit), so this simply calls lseek(). Provided for + * source compatibility only. + */ +off_t +lseek64(int fildes, off_t offset, int whence); +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_UNISTD_H */ diff --git a/src/unistd/access.c b/src/unistd/access.c new file mode 100644 index 0000000..e96cba0 --- /dev/null +++ b/src/unistd/access.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "unistd_impl.h" + +/* + * access/faccessat over SYS_faccessat (the *at form is the modern kernel + * ABI; SYS_faccessat2 is deliberately not used — the classic syscall + * covers the POSIX amode set). access() passes AT_FDCWD and no flags. + */ +int +access(const char *path, int amode) +{ + return syscall_ret(__syscall4(SYS_faccessat, VLIBC_UNISTD_AT_FDCWD, (long)path, amode, 0)); +} + +int +faccessat(int fd, const char *path, int amode, int flag) +{ + return syscall_ret(__syscall4(SYS_faccessat, fd, (long)path, amode, flag)); +} diff --git a/src/unistd/close.c b/src/unistd/close.c new file mode 100644 index 0000000..8110446 --- /dev/null +++ b/src/unistd/close.c @@ -0,0 +1,17 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * close: unbuffered pass-through. There is nothing to flush — vlibc I/O is + * unbuffered at this layer, and the stdio layer owns its own buffers. + */ +int +close(int fildes) +{ + return syscall_ret(__syscall1(SYS_close, fildes)); +} diff --git a/src/unistd/dup.c b/src/unistd/dup.c new file mode 100644 index 0000000..69a36b8 --- /dev/null +++ b/src/unistd/dup.c @@ -0,0 +1,38 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * dup/dup2: descriptor duplication. The copies share the file description + * (position, status flags, locks) with the original. On x86_64 SYS_dup2 is + * a two-argument syscall. + */ +int +dup(int fildes) +{ + return syscall_ret(__syscall1(SYS_dup, fildes)); +} + +int +dup2(int fildes, int fildes2) +{ + return syscall_ret(__syscall2(SYS_dup2, fildes, fildes2)); +} + +#if VLIBC_LEVEL_GE(2) + +/* + * dup3: dup2 with descriptor flags (O_CLOEXEC) applied atomically. + * Linux-specific; the kernel rejects fildes == fildes2 with EINVAL. + */ +int +dup3(int fildes, int fildes2, int flags) +{ + return syscall_ret(__syscall3(SYS_dup3, fildes, fildes2, flags)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/fsync.c b/src/unistd/fsync.c new file mode 100644 index 0000000..13c2819 --- /dev/null +++ b/src/unistd/fsync.c @@ -0,0 +1,23 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * fsync/fdatasync: flush a descriptor's dirty data (and for fsync, the + * metadata) to stable storage. + */ +int +fsync(int fildes) +{ + return syscall_ret(__syscall1(SYS_fsync, fildes)); +} + +int +fdatasync(int fildes) +{ + return syscall_ret(__syscall1(SYS_fdatasync, fildes)); +} diff --git a/src/unistd/lseek.c b/src/unistd/lseek.c new file mode 100644 index 0000000..1614c85 --- /dev/null +++ b/src/unistd/lseek.c @@ -0,0 +1,44 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * lseek returns a full-width off_t: on x86_64 the offset is a 64-bit long + * that can legitimately exceed INT_MAX, so the value must not be routed + * through syscall_ret() (which narrows to int). The error translation is + * inlined here with the same semantics: errno set from -r and (off_t)-1 on + * error, errno untouched on success. + */ +off_t +lseek(int fildes, off_t offset, int whence) +{ + long r = __syscall3(SYS_lseek, fildes, offset, whence); + + if (r < 0 && r > -4096) + { + errno = (int)-r; + return (off_t)-1; + } + return (off_t)r; +} + +#if VLIBC_LEVEL_GE(2) + +/* + * lseek64: glibc LFS alias. On x86_64 the LFS and non-LFS off_t are + * identical (both 64-bit) and SYS_lseek is the single ABI, so this is a + * plain delegation. Provided for source compatibility only. + */ +off_t +lseek64(int fildes, off_t offset, int whence) +{ + return lseek(fildes, offset, whence); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/open.c b/src/unistd/open.c new file mode 100644 index 0000000..69d49d1 --- /dev/null +++ b/src/unistd/open.c @@ -0,0 +1,51 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" +#include "unistd_impl.h" + +/* + * open/openat over SYS_openat (the *at syscall family is the modern kernel + * ABI; SYS_open exists only for compatibility and is not used). The + * optional fourth mode argument is read from the varargs only when oflag + * contains O_CREAT or O_TMPFILE — otherwise the caller supplied no mode + * and 0 is passed, which the kernel ignores. mode_t promotes to unsigned + * int in the varargs list. + */ +int +open(const char *path, int oflag, ...) +{ + unsigned mode = 0; + + if ((oflag & VLIBC_UNISTD_O_CREAT) != 0 || (oflag & VLIBC_UNISTD_O_TMPFILE) != 0) + { + va_list ap; + + va_start(ap, oflag); + mode = va_arg(ap, unsigned int); + va_end(ap); + } + return syscall_ret( + __syscall4(SYS_openat, VLIBC_UNISTD_AT_FDCWD, (long)path, oflag, (long)mode)); +} + +int +openat(int fd, const char *path, int oflag, ...) +{ + unsigned mode = 0; + + if ((oflag & VLIBC_UNISTD_O_CREAT) != 0 || (oflag & VLIBC_UNISTD_O_TMPFILE) != 0) + { + va_list ap; + + va_start(ap, oflag); + mode = va_arg(ap, unsigned int); + va_end(ap); + } + return syscall_ret(__syscall4(SYS_openat, fd, (long)path, oflag, (long)mode)); +} diff --git a/src/unistd/pipe.c b/src/unistd/pipe.c new file mode 100644 index 0000000..4d3dac6 --- /dev/null +++ b/src/unistd/pipe.c @@ -0,0 +1,31 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * pipe: plain POSIX pipe via SYS_pipe. Deliberately does NOT set + * O_CLOEXEC — pipe2 is the flag-taking variant (level 2). + */ +int +pipe(int fildes[2]) +{ + return syscall_ret(__syscall1(SYS_pipe, (long)fildes)); +} + +#if VLIBC_LEVEL_GE(2) + +/* + * pipe2: pipe with descriptor flags (O_CLOEXEC) applied atomically. + * Linux-specific. + */ +int +pipe2(int fildes[2], int flags) +{ + return syscall_ret(__syscall2(SYS_pipe2, (long)fildes, flags)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/pread.c b/src/unistd/pread.c new file mode 100644 index 0000000..8803ef0 --- /dev/null +++ b/src/unistd/pread.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * pread/pwrite: positional I/O over SYS_pread64/SYS_pwrite64. On x86_64 + * the offset is a single 64-bit register argument (off_t == long), so + * there is no lo/hi split. Positional I/O never touches the file position + * — the kernel handles that. + */ +ssize_t +pread(int fildes, void *buf, size_t nbyte, off_t offset) +{ + return syscall_ret(__syscall4(SYS_pread64, fildes, (long)buf, (long)nbyte, offset)); +} + +ssize_t +pwrite(int fildes, const void *buf, size_t nbyte, off_t offset) +{ + return syscall_ret(__syscall4(SYS_pwrite64, fildes, (long)buf, (long)nbyte, offset)); +} diff --git a/src/unistd/rw.c b/src/unistd/rw.c new file mode 100644 index 0000000..db0bd00 --- /dev/null +++ b/src/unistd/rw.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * read/write: unbuffered pass-through to the kernel. The raw result goes + * through syscall_ret(), which returns the byte count on success and -1 + * with errno set on error; errno is untouched on success. No buffering, no + * argument inspection. + */ +ssize_t +read(int fildes, void *buf, size_t nbyte) +{ + return syscall_ret(__syscall3(SYS_read, fildes, (long)buf, (long)nbyte)); +} + +ssize_t +write(int fildes, const void *buf, size_t nbyte) +{ + return syscall_ret(__syscall3(SYS_write, fildes, (long)buf, (long)nbyte)); +} diff --git a/src/unistd/sync.c b/src/unistd/sync.c new file mode 100644 index 0000000..c58055c --- /dev/null +++ b/src/unistd/sync.c @@ -0,0 +1,18 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * sync: flush all filesystem caches to stable storage. The kernel's SYS_sync + * always returns 0 and the POSIX interface is void, so the raw result is + * deliberately not inspected (there is no failure to report). + */ +void +sync(void) +{ + __syscall0(SYS_sync); +} diff --git a/src/unistd/truncate.c b/src/unistd/truncate.c new file mode 100644 index 0000000..a62a52b --- /dev/null +++ b/src/unistd/truncate.c @@ -0,0 +1,30 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * ftruncate: truncate an open descriptor; POSIX base form. + */ +int +ftruncate(int fildes, off_t length) +{ + return syscall_ret(__syscall2(SYS_ftruncate, fildes, length)); +} + +#if VLIBC_LEVEL_GE(2) + +/* + * truncate: truncate by path name; XSI, so it is gated at level 2 + * (ftruncate is the POSIX base form). + */ +int +truncate(const char *path, off_t length) +{ + return syscall_ret(__syscall2(SYS_truncate, (long)path, length)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/unistd_impl.h b/src/unistd/unistd_impl.h new file mode 100644 index 0000000..b78aeea --- /dev/null +++ b/src/unistd/unistd_impl.h @@ -0,0 +1,33 @@ +#ifndef VLIBC_UNISTD_INTERNAL_H +#define VLIBC_UNISTD_INTERNAL_H + +/* + * vlibc — internal constants for the unistd wrappers (temporary). + * + * include/fcntl.h is owned by todo 21; until it lands, the open/openat and + * access wrappers need the O_* flag bits and AT_FDCWD locally. These names + * carry the VLIBC_UNISTD_ prefix so they cannot collide with the real + * constants todo 21 will publish. The values are kernel UAPI facts + * (asm-generic/fcntl.h), transcribed, not invented: + * + * O_RDONLY 0x0, O_WRONLY 0x1, O_RDWR 0x2, O_CREAT 0x40, O_EXCL 0x80, + * O_TRUNC 0x200, O_APPEND 0x400, O_CLOEXEC 0x80000, + * O_TMPFILE 0x410000 (__O_TMPFILE 0x400000 | O_DIRECTORY 0x10000), + * AT_FDCWD -100. + */ + +/* fcntl open-flag bits (kernel UAPI; see the header comment above). */ +#define VLIBC_UNISTD_O_RDONLY 0x0 +#define VLIBC_UNISTD_O_WRONLY 0x1 +#define VLIBC_UNISTD_O_RDWR 0x2 +#define VLIBC_UNISTD_O_CREAT 0x40 +#define VLIBC_UNISTD_O_EXCL 0x80 +#define VLIBC_UNISTD_O_TRUNC 0x200 +#define VLIBC_UNISTD_O_APPEND 0x400 +#define VLIBC_UNISTD_O_CLOEXEC 0x80000 +#define VLIBC_UNISTD_O_TMPFILE 0x410000 + +/* *at syscall base directory (kernel UAPI). */ +#define VLIBC_UNISTD_AT_FDCWD (-100) + +#endif /* VLIBC_UNISTD_INTERNAL_H */ diff --git a/tests/test_unistd_file.c b/tests/test_unistd_file.c new file mode 100644 index 0000000..d12d7b5 --- /dev/null +++ b/tests/test_unistd_file.c @@ -0,0 +1,445 @@ +/* + * vlibc — unistd file I/O test (todo 19). + * + * Exercises the raw syscall wrappers end to end: + * + * 1. open(O_RDWR|O_CREAT|O_EXCL, 0600) + 4 KiB write + seek/read + * round-trip, then unlink via raw SYS_unlinkat. + * 2. pread/pwrite positional I/O: pread(fd, buf, 4, 0) reads the first + * four bytes after the file position advanced to the end, and the + * position is untouched by positional calls. + * 3. lseek SEEK_SET/CUR/END offsets, including lseek(SEEK_END) after + * ftruncate to 100. + * 4. pipe + write + read transports bytes. + * 5. dup2 duplicates a descriptor (a write through the dup is visible + * through the original); level-2 dup3 rejects same-fd with -1 and + * duplicates with O_CLOEXEC. + * 6. fsync/fdatasync on the temp file return 0. + * 7. sync() completes. + * 8. access/faccessat on the existing file return 0; on a nonexistent + * path return -1. + * 9. open flag validation: O_RDONLY on the existing file, O_CREAT|O_EXCL + * on the existing file → -1, O_TRUNC truncates. + * + * Level-2 gated section: dup3, pipe2, truncate, lseek64. + * + * The negative paths make the LIBRARY write errno (syscall_ret), which + * under a host-linked binary targets glibc's private dtv slot at %fs:0+8. + * In the default mode each such call is bracketed with a save/restore of + * that slot (task 13 technique) — only vlibc/raw-syscall code runs between + * the write and the restore, so host state is intact when host code runs + * again. The test itself NEVER reads errno; every negative is asserted on + * the return value. The -f mode runs the failure scenarios and exits via + * raw SYS_exit_group (house pattern, tests/test_malloc.c). + * + * 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/unistd.h" + +#include "../src/internal/syscall.h" + +/* Kernel-UAPI open flags, local to this test (include/fcntl.h is todo 21). */ +#define T19_O_RDONLY 0x0 +#define T19_O_WRONLY 0x1 +#define T19_O_RDWR 0x2 +#define T19_O_CREAT 0x40 +#define T19_O_EXCL 0x80 +#define T19_O_TRUNC 0x200 +#define T19_O_CLOEXEC 0x80000 +#define T19_AT_FDCWD (-100) + +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++; + } +} + +/* + * 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). + */ +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; +} + +/* 1+2+3: file round-trip, positional I/O, seeks, on one temp file. */ +static int +file_scenarios(const char *path) +{ + static unsigned char pattern[4096]; + unsigned char rbuf[4096]; + unsigned char small[4]; + unsigned i; + int ok = 1; + int fd; + + for (i = 0; i < sizeof(pattern); i++) + { + pattern[i] = (unsigned char)(i * 7 + 1); + } + fd = open(path, T19_O_RDWR | T19_O_CREAT | T19_O_EXCL, 0600); + check(fd >= 0, "open O_RDWR|O_CREAT|O_EXCL 0600 returns a descriptor"); + if (fd < 0) + { + return -1; + } + check(write(fd, pattern, sizeof(pattern)) == (ssize_t)sizeof(pattern), + "write of 4096 bytes returns 4096"); + check(lseek(fd, 0, SEEK_SET) == 0, "lseek SEEK_SET 0 returns 0"); + check(read(fd, rbuf, sizeof(rbuf)) == (ssize_t)sizeof(rbuf), "read of 4096 bytes returns 4096"); + for (i = 0; i < sizeof(pattern); i++) + { + if (rbuf[i] != pattern[i]) + { + ok = 0; + } + } + check(ok, "read-back matches the written 4096 bytes"); + + /* The file position is now at EOF (4096). */ + check(pread(fd, small, 4, 0) == 4, "pread(fd, buf, 4, 0) returns 4"); + check(small[0] == pattern[0] && small[1] == pattern[1] && small[2] == pattern[2] && + small[3] == pattern[3], + "pread reads the first four bytes"); + check(lseek(fd, 0, SEEK_CUR) == 4096, "pread leaves the position at 4096"); + + small[0] = 'W'; + small[1] = 'X'; + small[2] = 'Y'; + small[3] = 'Z'; + check(pwrite(fd, small, 4, 100) == 4, "pwrite(fd, buf, 4, 100) returns 4"); + small[0] = 0; + small[1] = 0; + small[2] = 0; + small[3] = 0; + check(pread(fd, small, 4, 100) == 4, "pread(fd, buf, 4, 100) returns 4"); + check(small[0] == 'W' && small[1] == 'X' && small[2] == 'Y' && small[3] == 'Z', + "pwrite/pread round-trip at offset 100"); + check(lseek(fd, 0, SEEK_CUR) == 4096, "position still 4096 after positional I/O"); + + /* Scenario 3: ftruncate + seek offsets. */ + check(ftruncate(fd, 100) == 0, "ftruncate(fd, 100) returns 0"); + check(lseek(fd, 0, SEEK_END) == 100, "lseek SEEK_END after ftruncate returns 100"); + check(lseek(fd, 10, SEEK_SET) == 10, "lseek SEEK_SET 10 returns 10"); + check(lseek(fd, 5, SEEK_CUR) == 15, "lseek SEEK_CUR +5 from 10 returns 15"); + check(lseek(fd, -7, SEEK_CUR) == 8, "lseek SEEK_CUR -7 from 15 returns 8"); + return fd; +} + +/* 4. pipe + write + read transports bytes. */ +static void +pipe_scenario(void) +{ + int fds[2]; + char buf[8]; + long n = 0; + + check(pipe(fds) == 0, "pipe returns 0"); + check(write(fds[1], "hello", 5) == 5, "write to the pipe write end returns 5"); + check(read(fds[0], buf, sizeof(buf)) == 5, "read from the pipe read end returns 5"); + while (n < 5 && buf[n] == "hello"[n]) + { + n++; + } + check(n == 5, "pipe transports the five bytes intact"); + check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe ends returns 0"); +} + +/* 5. dup2 duplicates a descriptor sharing the file description. */ +static void +dup_scenarios(int fd) +{ + unsigned char b[1]; + int d; + + if (fd < 0) + { + return; + } + d = dup2(fd, 200); + check(d == 200, "dup2(fd, 200) returns 200"); + if (d != 200) + { + return; + } + check(lseek(200, 0, SEEK_SET) == 0, "the dup shares the file position"); + check(write(200, "Z", 1) == 1, "write through the dup returns 1"); + check(pread(fd, b, 1, 0) == 1 && b[0] == 'Z', "write through dup visible on original"); + check(dup2(200, 200) == 200, "dup2(x, x) returns x (no-op)"); + check(close(200) == 0, "close(200) returns 0"); +} + +/* 6+7. fsync/fdatasync/sync on the temp file. */ +static void +sync_scenarios(int fd) +{ + if (fd < 0) + { + return; + } + check(fsync(fd) == 0, "fsync on the temp file returns 0"); + check(fdatasync(fd) == 0, "fdatasync on the temp file returns 0"); + sync(); + check(1, "sync() completes"); +} + +/* 8. access/faccessat happy + negative (bracketed). */ +static void +access_scenarios(const char *path) +{ + unsigned long saved; + + check(access(path, F_OK) == 0, "access on the existing file with F_OK returns 0"); + check(access(path, R_OK) == 0, "access on the existing file with R_OK returns 0"); + check(access(path, W_OK) == 0, "access on the existing file with W_OK returns 0"); + check(faccessat(T19_AT_FDCWD, path, F_OK, 0) == 0, + "faccessat(AT_FDCWD, path, F_OK, 0) returns 0"); + saved = tcb_slot1(); + check(access("/nonexistent-vlibc-t19", F_OK) == -1, "access on a nonexistent path returns -1"); + tcb_slot1_set(saved); + saved = tcb_slot1(); + check(faccessat(T19_AT_FDCWD, "/nonexistent-vlibc-t19", F_OK, 0) == -1, + "faccessat on a nonexistent path returns -1"); + tcb_slot1_set(saved); +} + +/* 9. open flag validation (O_TRUNC runs last: it resets the size). */ +static void +open_flag_scenarios(const char *path) +{ + unsigned long saved; + int fd; + + fd = open(path, T19_O_RDONLY); + check(fd >= 0, "open with O_RDONLY on the existing file succeeds"); + if (fd >= 0) + { + check(close(fd) == 0, "close of the O_RDONLY descriptor returns 0"); + } + saved = tcb_slot1(); + check(open(path, T19_O_CREAT | T19_O_EXCL, 0600) == -1, + "open O_CREAT|O_EXCL on the existing file returns -1"); + tcb_slot1_set(saved); + fd = open(path, T19_O_WRONLY | T19_O_TRUNC); + check(fd >= 0, "open with O_WRONLY|O_TRUNC succeeds"); + if (fd >= 0) + { + check(lseek(fd, 0, SEEK_END) == 0, "O_TRUNC leaves the size at 0"); + check(close(fd) == 0, "close of the O_TRUNC descriptor returns 0"); + } +} + +#if VLIBC_LEVEL_GE(2) + +/* Level-2 gate proof: dup3, pipe2, truncate, lseek64. */ +static void +level2_scenarios(int fd) +{ + const char *path2 = "/tmp/vlibc-t19-l2"; + unsigned long saved; + char buf[8]; + int fds[2]; + long n = 0; + int d; + int fd2; + + if (fd < 0) + { + return; + } + saved = tcb_slot1(); + check(dup3(fd, fd, 0) == -1, "dup3(fd, fd, 0) returns -1 (same-fd)"); + tcb_slot1_set(saved); + d = dup3(fd, 201, T19_O_CLOEXEC); + check(d == 201, "dup3(fd, 201, O_CLOEXEC) returns 201"); + if (d == 201) + { + check(close(201) == 0, "close of the dup3 descriptor returns 0"); + } + check(pipe2(fds, T19_O_CLOEXEC) == 0, "pipe2 with O_CLOEXEC returns 0"); + check(write(fds[1], "hi", 2) == 2, "write to the pipe2 write end returns 2"); + check(read(fds[0], buf, sizeof(buf)) == 2, "read from the pipe2 read end returns 2"); + while (n < 2 && buf[n] == "hi"[n]) + { + n++; + } + check(n == 2, "pipe2 transports the two bytes intact"); + check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe2 ends returns 0"); + + fd2 = open(path2, T19_O_RDWR | T19_O_CREAT | T19_O_EXCL, 0600); + check(fd2 >= 0, "open creates the second temp file for truncate"); + if (fd2 >= 0) + { + check(write(fd2, "abcdef", 6) == 6, "write six bytes to the second file"); + check(truncate(path2, 42) == 0, "truncate(path, 42) returns 0"); + check(lseek64(fd2, 0, SEEK_END) == 42, "lseek64 SEEK_END after truncate returns 42"); + check(lseek64(fd2, 0, SEEK_SET) == 0, "lseek64 SEEK_SET 0 returns 0"); + check(close(fd2) == 0, "close of the second temp file returns 0"); + } + check(__syscall3(SYS_unlinkat, T19_AT_FDCWD, (long)path2, 0) == 0, + "unlink of the second temp file returns 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 the library writes + * errno on these paths (host-TCB hazard). + */ +static int +failure_scenarios(void) +{ + const char *path = "/tmp/vlibc-t19-f"; + int rc = 0; + int fd; + + if (open("/nonexistent/vlibc/t19", T19_O_RDONLY) != -1) + { + say(2, "FAIL: open on a nonexistent path did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: open on a nonexistent path -> -1\n"); + } + fd = open(path, T19_O_RDWR | T19_O_CREAT | T19_O_TRUNC, 0600); + if (fd < 0) + { + say(2, "FAIL: -f setup open failed\n"); + return 1; + } + if (close(fd) != 0) + { + say(2, "FAIL: -f setup close failed\n"); + rc = 1; + } + if (write(fd, "x", 1) != -1) + { + say(2, "FAIL: write on a closed fd did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: write on a closed fd -> -1\n"); + } + if (lseek(fd, 0, SEEK_SET) != -1) + { + say(2, "FAIL: lseek on a closed fd did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: lseek on a closed fd -> -1\n"); + } + __syscall3(SYS_unlinkat, T19_AT_FDCWD, (long)path, 0); + return rc; +} + +int +main(int argc, char **argv) +{ + const char *path = "/tmp/vlibc-t19-XXXX"; + 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 = file_scenarios(path); + pipe_scenario(); + dup_scenarios(fd); + sync_scenarios(fd); + access_scenarios(path); + open_flag_scenarios(path); +#if VLIBC_LEVEL_GE(2) + level2_scenarios(fd); +#endif + if (fd >= 0) + { + check(close(fd) == 0, "close of the main temp file returns 0"); + } + check(__syscall3(SYS_unlinkat, T19_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 unistd file I/O tests passed\n"); + return 0; +}