/* * vlibc — stat family test (todo 22). * * Exercises sys/stat.h end to end: * * 1. stat("/dev/null") reports a character device with a non-zero rdev. * 2. open+write a temp file, fstat shows S_ISREG with the written size; * close then stat(path) agrees (and the inode matches). * 3. umask(0) then mkdir/mkdirat 0755/0700 — stat shows S_ISDIR with the * exact requested mode; the umask is restored afterwards. * 4. A symlink created via raw SYS_symlink: lstat reports the link * itself (S_ISLNK), stat follows it (S_ISREG); fstatat with * AT_SYMLINK_NOFOLLOW does not follow. * 5. chmod(file, 0600) / fchmod / fchmodat update the mode. * 6. chown-family no-ops: fchown(fd, -1, -1) and fchownat(...,-1,-1,0) * succeed without changing ownership; lchown on the symlink succeeds. * 7. utimensat(AT_FDCWD, file, NULL, 0) and futimens(fd, NULL) set the * timestamps to the current time and return 0. * 8. mkfifo/mkfifoat create FIFOs (S_ISFIFO). * 9. Level 2: mknod/mknodat create FIFOs via S_IFIFO (no privilege * needed for a FIFO). * * The -f mode runs only the failure scenarios: stat/lstat/fstatat/fstat on * nonexistent paths/fds, mkdir on an existing directory, mkfifo in a * missing directory, chmod/fchmod/fchmodat/chown/lchown/fchown/fchownat/ * utimensat/futimens negatives, and (L2) mknod without a type bit * (EINVAL). Only return values are asserted — errno is never read. 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, so -f * 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 #include "../src/internal/syscall.h" /* Kernel-UAPI open/at flags, local to this test (include/fcntl.h is todo 21). */ #define T22_AT_FDCWD (-100) #define T22_AT_SYMLINK_NOFOLLOW 0x100 #define T22_O_RDWR 0x2 #define T22_O_CREAT 0x40 #define T22_O_EXCL 0x80 /* Scratch paths in /tmp (test runs from an arbitrary cwd). */ #define T22_FILE "/tmp/vlibc_stat_t22_file" #define T22_DIR "/tmp/vlibc_stat_t22_dir" #define T22_DIR2 "/tmp/vlibc_stat_t22_dir2" #define T22_LINK "/tmp/vlibc_stat_t22_link" #define T22_LINK_TARGET "vlibc_stat_t22_file" #define T22_FIFO "/tmp/vlibc_stat_t22_fifo" #define T22_FIFO2 "/tmp/vlibc_stat_t22_fifo2" #define T22_FIFO3 "/tmp/vlibc_stat_t22_fifo3" #define T22_FIFO4 "/tmp/vlibc_stat_t22_fifo4" #define T22_FDIR "/tmp/vlibc_stat_t22_fdir" #define T22_MISSING "/nonexistent-vlibc-zzz" 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'; if (v == 0) { buf[--i] = '0'; } while (v > 0 && i > 0) { buf[--i] = (char)('0' + v % 10); v /= 10; } say(fd, &buf[i]); } static void check(int ok, const char *msg) { if (ok) { say(1, "PASS: "); } else { say(1, "FAIL: "); failures++; } say(1, msg); say(1, "\n"); } /* * 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. /dev/null is a character device. */ static void dev_null_scenario(void) { struct stat st = {0}; check(sizeof(struct stat) == 144, "sizeof(struct stat) is 144"); check(stat("/dev/null", &st) == 0, "stat(/dev/null) returns 0"); check(S_ISCHR(st.st_mode), "st_mode classifies /dev/null as S_ISCHR"); check(st.st_rdev != 0, "st_rdev of /dev/null is non-zero"); check(fstatat(T22_AT_FDCWD, "/dev/null", &st, 0) == 0, "fstatat(AT_FDCWD, /dev/null) returns 0"); check(S_ISCHR(st.st_mode), "fstatat st_mode classifies /dev/null as S_ISCHR"); check(!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode) && !S_ISFIFO(st.st_mode), "/dev/null is not reg/dir/fifo"); } /* 2 + 5 + 6 + 7: the temp file lifecycle. */ static void file_scenario(void) { static const char payload[] = "hello from vlibc stat\n"; struct stat st = {0}; struct stat st2 = {0}; unsigned long saved; ssize_t wrote; int fd; fd = open(T22_FILE, T22_O_RDWR | T22_O_CREAT | T22_O_EXCL, 0644); check(fd >= 0, "open O_RDWR|O_CREAT|O_EXCL 0644 returns a descriptor"); if (fd < 0) { return; } wrote = write(fd, payload, sizeof(payload) - 1); check(wrote == (ssize_t)(sizeof(payload) - 1), "write of 22 bytes returns 22"); check(fstat(fd, &st) == 0, "fstat(fd) returns 0"); check(S_ISREG(st.st_mode), "fstat st_mode classifies the file as S_ISREG"); check(st.st_size == (off_t)(sizeof(payload) - 1), "fstat st_size equals the written bytes"); check((st.st_mode & 0777) == 0644, "fstat st_mode permission bits are 0644 (umask(0))"); check(st.st_nlink == 1, "fstat st_nlink is 1"); check(st.st_atime > 0 && st.st_mtime > 0 && st.st_ctime > 0, "st_atime/st_mtime/st_ctime macros expose positive times"); check(stat(T22_FILE, &st2) == 0, "stat(path) returns 0"); check(st2.st_size == st.st_size, "stat(path) st_size matches fstat"); check(st2.st_ino == st.st_ino, "stat(path) st_ino matches fstat"); /* 5. chmod family. */ check(chmod(T22_FILE, 0600) == 0, "chmod(file, 0600) returns 0"); saved = tcb_slot1(); check(stat(T22_FILE, &st2) == 0, "stat after chmod returns 0"); tcb_slot1_set(saved); check((st2.st_mode & 0777) == 0600, "mode is 0600 after chmod"); check(fchmod(fd, 0640) == 0, "fchmod(fd, 0640) returns 0"); check(fchmodat(T22_AT_FDCWD, T22_FILE, 0644, 0) == 0, "fchmodat(AT_FDCWD, file, 0644) returns 0"); saved = tcb_slot1(); check(stat(T22_FILE, &st2) == 0, "stat after fchmodat returns 0"); tcb_slot1_set(saved); check((st2.st_mode & 0777) == 0644, "mode is 0644 after fchmodat"); /* 6. chown-family no-ops (-1 = leave unchanged). */ check(fchown(fd, (uid_t)-1, (gid_t)-1) == 0, "fchown(fd, -1, -1) returns 0"); check(fchownat(T22_AT_FDCWD, T22_FILE, (uid_t)-1, (gid_t)-1, 0) == 0, "fchownat(AT_FDCWD, file, -1, -1, 0) returns 0"); /* 7. utimensat/futimens with NULL times (set to current time). */ check(utimensat(T22_AT_FDCWD, T22_FILE, 0, 0) == 0, "utimensat(AT_FDCWD, file, NULL, 0) returns 0"); check(futimens(fd, 0) == 0, "futimens(fd, NULL) returns 0"); check(close(fd) == 0, "close of the temp file returns 0"); } /* 3. mkdir/mkdirat with a cleared umask. */ static void mkdir_scenario(void) { struct stat st = {0}; mode_t old; old = umask(0); check(mkdir(T22_DIR, 0755) == 0, "mkdir(dir, 0755) returns 0"); check(mkdirat(T22_AT_FDCWD, T22_DIR2, 0700) == 0, "mkdirat(AT_FDCWD, dir2, 0700) returns 0"); umask(old); check(stat(T22_DIR, &st) == 0, "stat(dir) returns 0"); check(S_ISDIR(st.st_mode), "st_mode classifies dir as S_ISDIR"); check((st.st_mode & 0777) == 0755, "dir mode is exactly 0755 (umask(0))"); check(stat(T22_DIR2, &st) == 0, "stat(dir2) returns 0"); check(S_ISDIR(st.st_mode) && (st.st_mode & 0777) == 0700, "dir2 mode is exactly 0700"); } /* 4. symlink: lstat reports the link, stat follows. */ static void symlink_scenario(void) { struct stat st = {0}; check(__syscall2(SYS_symlink, (long)T22_LINK_TARGET, (long)T22_LINK) == 0, "raw SYS_symlink creates the link"); check(lstat(T22_LINK, &st) == 0, "lstat(link) returns 0"); check(S_ISLNK(st.st_mode), "lstat st_mode classifies the link as S_ISLNK"); check(!S_ISREG(st.st_mode), "lstat does not follow the link"); check(stat(T22_LINK, &st) == 0, "stat(link) returns 0"); check(S_ISREG(st.st_mode), "stat follows the link to S_ISREG"); check(fstatat(T22_AT_FDCWD, T22_LINK, &st, T22_AT_SYMLINK_NOFOLLOW) == 0, "fstatat(link, AT_SYMLINK_NOFOLLOW) returns 0"); check(S_ISLNK(st.st_mode), "fstatat with AT_SYMLINK_NOFOLLOW reports S_ISLNK"); check(lchown(T22_LINK, (uid_t)-1, (gid_t)-1) == 0, "lchown(link, -1, -1) returns 0"); } /* 8. mkfifo/mkfifoat create FIFOs. */ static void mkfifo_scenario(void) { struct stat st = {0}; check(mkfifo(T22_FIFO, 0644) == 0, "mkfifo(fifo, 0644) returns 0"); check(mkfifoat(T22_AT_FDCWD, T22_FIFO2, 0600) == 0, "mkfifoat(AT_FDCWD, fifo2, 0600) returns 0"); check(stat(T22_FIFO, &st) == 0, "stat(fifo) returns 0"); check(S_ISFIFO(st.st_mode), "st_mode classifies fifo as S_ISFIFO"); check(stat(T22_FIFO2, &st) == 0, "stat(fifo2) returns 0"); check(S_ISFIFO(st.st_mode), "st_mode classifies fifo2 as S_ISFIFO"); } #if VLIBC_LEVEL_GE(2) /* 9. mknod/mknodat create FIFOs (no privilege needed for S_IFIFO). */ static void mknod_scenario(void) { struct stat st = {0}; check(mknod(T22_FIFO3, S_IFIFO | 0640, 0) == 0, "mknod(fifo3, S_IFIFO|0640, 0) returns 0"); check(mknodat(T22_AT_FDCWD, T22_FIFO4, S_IFIFO | 0600, 0) == 0, "mknodat(AT_FDCWD, fifo4, S_IFIFO|0600, 0) returns 0"); check(stat(T22_FIFO3, &st) == 0, "stat(fifo3) returns 0"); check(S_ISFIFO(st.st_mode) && (st.st_mode & 0777) == 0640, "mknod fifo3 is S_ISFIFO with mode 0640"); check(stat(T22_FIFO4, &st) == 0, "stat(fifo4) returns 0"); check(S_ISFIFO(st.st_mode) && (st.st_mode & 0777) == 0600, "mknodat fifo4 is S_ISFIFO with mode 0600"); } #endif /* VLIBC_LEVEL_GE(2) */ /* Clean up every scratch path; failures here are reported, not fatal. */ static void cleanup_scenario(void) { long r; r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_LINK, 0); check(r == 0, "raw unlinkat removes the link"); r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_FILE, 0); check(r == 0, "raw unlinkat removes the temp file"); r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_FIFO, 0); check(r == 0, "raw unlinkat removes fifo"); r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_FIFO2, 0); check(r == 0, "raw unlinkat removes fifo2"); #if VLIBC_LEVEL_GE(2) r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_FIFO3, 0); check(r == 0, "raw unlinkat removes fifo3"); r = __syscall3(SYS_unlinkat, T22_AT_FDCWD, (long)T22_FIFO4, 0); check(r == 0, "raw unlinkat removes fifo4"); #endif /* VLIBC_LEVEL_GE(2) */ r = __syscall1(SYS_rmdir, (long)T22_DIR); check(r == 0, "raw rmdir removes dir"); r = __syscall1(SYS_rmdir, (long)T22_DIR2); check(r == 0, "raw rmdir removes dir2"); } /* The failure scenarios. Exit via raw SYS_exit_group: the library's errno * writes on these paths corrupt glibc's private dtv slot at %fs:0+8, so * host cleanup must never run (house pattern). */ static void failure_scenarios(void) { struct stat st = {0}; check(stat(T22_MISSING, &st) == -1, "stat(nonexistent) returns -1"); check(lstat(T22_MISSING, &st) == -1, "lstat(nonexistent) returns -1"); check(fstatat(T22_AT_FDCWD, T22_MISSING, &st, 0) == -1, "fstatat(AT_FDCWD, nonexistent) returns -1"); check(fstat(-1, &st) == -1, "fstat(-1) returns -1"); check(mkdir(T22_FDIR, 0755) == 0, "mkdir(fdir) succeeds for the negative"); check(mkdir(T22_FDIR, 0755) == -1, "mkdir(existing dir) returns -1"); check(mkfifo("/nonexistent-vlibc-zzz/x", 0644) == -1, "mkfifo in missing dir returns -1"); check(chmod(T22_MISSING, 0600) == -1, "chmod(nonexistent) returns -1"); check(fchmod(-1, 0600) == -1, "fchmod(-1) returns -1"); check(fchmodat(T22_AT_FDCWD, T22_MISSING, 0600, 0) == -1, "fchmodat(nonexistent) returns -1"); check(chown(T22_MISSING, (uid_t)-1, (gid_t)-1) == -1, "chown(nonexistent) returns -1"); check(lchown(T22_MISSING, (uid_t)-1, (gid_t)-1) == -1, "lchown(nonexistent) returns -1"); check(fchown(-1, (uid_t)-1, (gid_t)-1) == -1, "fchown(-1) returns -1"); check(fchownat(T22_AT_FDCWD, T22_MISSING, (uid_t)-1, (gid_t)-1, 0) == -1, "fchownat(nonexistent) returns -1"); check(utimensat(T22_AT_FDCWD, T22_MISSING, 0, 0) == -1, "utimensat(nonexistent) returns -1"); check(futimens(-1, 0) == -1, "futimens(-1) returns -1"); #if VLIBC_LEVEL_GE(2) check(mknod(T22_FDIR, 0644, 0) == -1, "mknod without a type bit returns -1"); check(mknodat(T22_AT_FDCWD, T22_FDIR, 0644, 0) == -1, "mknodat without a type bit returns -1"); check(mknod("/nonexistent-vlibc-zzz/x", S_IFIFO | 0644, 0) == -1, "mknod in missing dir returns -1"); #endif /* VLIBC_LEVEL_GE(2) */ __syscall1(SYS_rmdir, (long)T22_FDIR); if (failures == 0) { say(1, "all stat failure scenarios passed\n"); } else { say(1, "FAILURES: "); say_dec(1, (unsigned long)failures); say(1, "\n"); } __syscall1(SYS_exit_group, failures == 0 ? 0 : 1); } int main(int argc, char **argv) { if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0') { failure_scenarios(); } dev_null_scenario(); file_scenario(); mkdir_scenario(); symlink_scenario(); mkfifo_scenario(); #if VLIBC_LEVEL_GE(2) mknod_scenario(); #endif /* VLIBC_LEVEL_GE(2) */ cleanup_scenario(); if (failures == 0) { say(1, "all stat tests passed\n"); } else { say(1, "FAILURES: "); say_dec(1, (unsigned long)failures); say(1, "\n"); } return failures == 0 ? 0 : 1; }