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