From 98f4c3169c829fd70448d0768ba27ec444ba43bb Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 16:35:50 -0400 Subject: [PATCH] feat(stat): stat/mkdir/chmod and mode macros --- include/sys/stat.h | 294 +++++++++++++++++++++++++++++++++ src/stat/chmod.c | 32 ++++ src/stat/chown.c | 40 +++++ src/stat/fstatat.c | 39 +++++ src/stat/mkdir.c | 24 +++ src/stat/mkfifo.c | 25 +++ src/stat/mknod.c | 31 ++++ src/stat/stat_impl.h | 22 +++ src/stat/umask.c | 18 ++ src/stat/utimensat.c | 26 +++ tests/test_stat.c | 384 +++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 935 insertions(+) create mode 100644 include/sys/stat.h create mode 100644 src/stat/chmod.c create mode 100644 src/stat/chown.c create mode 100644 src/stat/fstatat.c create mode 100644 src/stat/mkdir.c create mode 100644 src/stat/mkfifo.c create mode 100644 src/stat/mknod.c create mode 100644 src/stat/stat_impl.h create mode 100644 src/stat/umask.c create mode 100644 src/stat/utimensat.c create mode 100644 tests/test_stat.c diff --git a/include/sys/stat.h b/include/sys/stat.h new file mode 100644 index 0000000..2696cb0 --- /dev/null +++ b/include/sys/stat.h @@ -0,0 +1,294 @@ +#ifndef VLIBC_SYS_STAT_H +#define VLIBC_SYS_STAT_H + +/* + * vlibc — . + * + * File status: the x86_64 Linux struct stat layout and the S_* mode-bit + * macros, plus the stat/mkdir/chmod/utimensat/chown wrapper families + * (POSIX.1-2008 base). Every function is an unbuffered pass-through to the + * kernel: failures are reported as -1 with errno set by the syscall layer. + * + * Level 1 (onlyposix): stat, fstat, lstat, fstatat, mkdir, mkdirat, + * mkfifo, mkfifoat, chmod, fchmod, fchmodat, umask, + * utimensat, futimens, chown, fchown, lchown, + * fchownat (Issue 7 moved lchown from XSI to base). + * Level 2 (muslmimic): mknod, mknodat (XSI). + * + * struct stat matches the x86_64 kernel layout exactly (144 bytes), pinned + * by static assertions — the same layout src/stdio/stdio.c transcribed + * privately as stdio_stat for its fstat-based isatty(). The + * st_atime/st_mtime/st_ctime spellings are glibc-style macros onto + * st_atim.tv_sec &c for POSIX source compatibility. + * + * The AT_* flag constants (AT_FDCWD, AT_SYMLINK_NOFOLLOW, ...) belong to + * (todo 21) and are deliberately not defined here; the flag + * arguments below are plain int and take their values from that header. + * + * 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 + +/* + * Provisional struct timespec definition. The canonical home of struct + * timespec is (todo 41); until it lands, sys/stat.h needs the + * definition for the st_*tim members and utimensat/futimens. Todo 41 must + * move (or reconcile) this definition — it currently exists nowhere else + * (include/threads.h only forward-declares it). + */ +struct timespec +{ + time_t tv_sec; /* seconds */ + long tv_nsec; /* nanoseconds (0..999999999) */ +}; + +/* + * File status, x86_64 Linux kernel layout. Field widths are LP64: the + * comment column gives the byte offset of each member. The order and the + * two padding areas are kernel-ABI facts, not style choices. + */ +struct stat +{ + dev_t st_dev; /* 0: device containing the file */ + ino_t st_ino; /* 8: inode number */ + nlink_t st_nlink; /* 16: hard link count */ + mode_t st_mode; /* 24: file type + permissions */ + uid_t st_uid; /* 28: owner user id */ + gid_t st_gid; /* 32: owner group id */ + unsigned int st_pad0; /* 36: kernel padding (int __pad0) */ + dev_t st_rdev; /* 40: device id (if device file) */ + off_t st_size; /* 48: size in bytes */ + blksize_t st_blksize; /* 56: preferred I/O block size */ + blkcnt_t st_blocks; /* 64: 512-byte blocks allocated */ + struct timespec st_atim; /* 72: last access time */ + struct timespec st_mtim; /* 88: last modification time */ + struct timespec st_ctim; /* 104: last status change time */ + long st_unused[3]; /* 120: kernel padding (__unused) */ +}; + +/* Pin the kernel layout: sizeof and the two offsets stdio consumes. */ +_Static_assert(sizeof(struct stat) == 144, "struct stat must match the x86_64 kernel layout"); +_Static_assert(offsetof(struct stat, st_mode) == 24, "st_mode must sit at offset 24"); +_Static_assert(offsetof(struct stat, st_size) == 48, "st_size must sit at offset 48"); + +/* File type bits (st_mode & S_IFMT). */ +#define S_IFMT 0170000 /* type-of-file mask */ +#define S_IFSOCK 0140000 /* socket */ +#define S_IFLNK 0120000 /* symbolic link */ +#define S_IFREG 0100000 /* regular file */ +#define S_IFBLK 0060000 /* block device */ +#define S_IFDIR 0040000 /* directory */ +#define S_IFCHR 0020000 /* character device */ +#define S_IFIFO 0010000 /* FIFO (named pipe) */ + +/* Special permission bits. */ +#define S_ISUID 04000 /* set-user-id on execution */ +#define S_ISGID 02000 /* set-group-id on execution */ +#define S_ISVTX 01000 /* sticky bit (restricted deletion on dirs) */ + +/* Owner permission bits. */ +#define S_IRWXU 0700 /* read, write, execute/search by owner */ +#define S_IRUSR 0400 /* read permission, owner */ +#define S_IWUSR 0200 /* write permission, owner */ +#define S_IXUSR 0100 /* execute/search permission, owner */ + +/* Group permission bits. */ +#define S_IRWXG 070 /* read, write, execute/search by group */ +#define S_IRGRP 040 /* read permission, group */ +#define S_IWGRP 020 /* write permission, group */ +#define S_IXGRP 010 /* execute/search permission, group */ + +/* Others permission bits. */ +#define S_IRWXO 07 /* read, write, execute/search by others */ +#define S_IROTH 04 /* read permission, others */ +#define S_IWOTH 02 /* write permission, others */ +#define S_IXOTH 01 /* execute/search permission, others */ + +/* File type predicates over the type bits. */ +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) +#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) +#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) + +/* POSIX compatibility spellings of the struct timespec members. */ +#define st_atime st_atim.tv_sec +#define st_mtime st_mtim.tv_sec +#define st_ctime st_ctim.tv_sec + +/* Level 1 (POSIX base). */ + +/* + * Store the status of the file named by path into buf. Follows symbolic + * links; return 0, or -1 with errno set. + */ +int +stat(const char *path, struct stat *buf); + +/* + * Store the status of the file descriptor fd into buf; return 0, or -1 + * with errno set. + */ +int +fstat(int fd, struct stat *buf); + +/* + * Like stat(), but a symbolic link is reported itself, not its target; + * return 0, or -1 with errno set. + */ +int +lstat(const char *path, struct stat *buf); + +/* + * Like stat(), but path is relative to the directory named by fd (use + * AT_FDCWD from for the current working directory); flag may + * hold AT_SYMLINK_NOFOLLOW to report the link itself. Return 0, or -1 + * with errno set. + */ +int +fstatat(int fd, const char *path, struct stat *buf, int flag); + +/* + * Create the directory named by path with the access mode mode (masked by + * the process umask); return 0, or -1 with errno set. + */ +int +mkdir(const char *path, mode_t mode); + +/* + * Like mkdir(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory); return 0, or -1 with errno + * set. + */ +int +mkdirat(int fd, const char *path, mode_t mode); + +/* + * Create the FIFO (named pipe) named by path with mode; return 0, or -1 + * with errno set. + */ +int +mkfifo(const char *path, mode_t mode); + +/* + * Like mkfifo(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory); return 0, or -1 with errno + * set. + */ +int +mkfifoat(int fd, const char *path, mode_t mode); + +/* + * Change the access mode of the file named by path to mode; return 0, or + * -1 with errno set. + */ +int +chmod(const char *path, mode_t mode); + +/* + * Change the access mode of the file descriptor fd to mode; return 0, or + * -1 with errno set. + */ +int +fchmod(int fd, mode_t mode); + +/* + * Like chmod(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory); flag may hold + * AT_SYMLINK_NOFOLLOW. Return 0, or -1 with errno set. + */ +int +fchmodat(int fd, const char *path, mode_t mode, int flag); + +/* + * Set the process file-mode creation mask to cmask and return the previous + * mask. Never fails. + */ +mode_t +umask(mode_t cmask); + +/* + * Set the access and modification times of the file named by path + * (relative to fd, or AT_FDCWD) to times[0] (access) and times[1] + * (modification). A NULL times sets both to the current time; a tv_nsec + * of UTIME_NOW/UTIME_OMIT (from ) selects per-member behavior. + * Return 0, or -1 with errno set. + */ +int +utimensat(int fd, const char *path, const struct timespec times[2], int flag); + +/* + * Like utimensat() on the file descriptor fd (the path is implicit); + * return 0, or -1 with errno set. + */ +int +futimens(int fd, const struct timespec times[2]); + +/* + * Change the owner and group of the file named by path to owner/group + * (a value of (uid_t)-1 leaves the current one unchanged). Follows + * symbolic links; return 0, or -1 with errno set. + */ +int +chown(const char *path, uid_t owner, gid_t group); + +/* + * Change the owner and group of the file descriptor fd; return 0, or -1 + * with errno set. + */ +int +fchown(int fd, uid_t owner, gid_t group); + +/* + * Like chown(), but a symbolic link is changed itself, not its target; + * return 0, or -1 with errno set. + */ +int +lchown(const char *path, uid_t owner, gid_t group); + +/* + * Like chown(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory); flag may hold + * AT_SYMLINK_NOFOLLOW. Return 0, or -1 with errno set. + */ +int +fchownat(int fd, const char *path, uid_t owner, gid_t group, int flag); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI. */ + +/* + * Create a special file named by path with mode (a file-type bit like + * S_IFIFO or S_IFCHR must be set) and device id dev; return 0, or -1 with + * errno set. Creating device nodes requires privilege. XSI. + */ +int +mknod(const char *path, mode_t mode, dev_t dev); + +/* + * Like mknod(), but path is relative to the directory named by fd (use + * AT_FDCWD for the current working directory); return 0, or -1 with errno + * set. XSI. + */ +int +mknodat(int fd, const char *path, mode_t mode, dev_t dev); +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_STAT_H */ diff --git a/src/stat/chmod.c b/src/stat/chmod.c new file mode 100644 index 0000000..0a077a4 --- /dev/null +++ b/src/stat/chmod.c @@ -0,0 +1,32 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "stat_impl.h" + +/* + * chmod/chmodat over SYS_fchmodat (the *at form is the modern kernel ABI) + * and fchmod over SYS_fchmod. chmod() passes AT_FDCWD and no flags; the + * kernel silently ignores the file-type bits of mode and applies only the + * permission + special bits. + */ +int +fchmodat(int fd, const char *path, mode_t mode, int flag) +{ + return syscall_ret(__syscall4(SYS_fchmodat, fd, (long)path, (long)mode, flag)); +} + +int +chmod(const char *path, mode_t mode) +{ + return fchmodat(VLIBC_STAT_AT_FDCWD, path, mode, 0); +} + +int +fchmod(int fd, mode_t mode) +{ + return syscall_ret(__syscall2(SYS_fchmod, fd, (long)mode)); +} diff --git a/src/stat/chown.c b/src/stat/chown.c new file mode 100644 index 0000000..882a1c2 --- /dev/null +++ b/src/stat/chown.c @@ -0,0 +1,40 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "stat_impl.h" + +/* + * chown family over SYS_fchownat (the *at form is the modern kernel ABI) + * and fchown over SYS_fchown. A uid/gid of (uid_t)-1 (or (gid_t)-1) means + * "leave unchanged" — the kernel checks for the all-ones value, so the + * wrapper passes the arguments through as-is. chown() passes AT_FDCWD and + * no flags; lchown() adds AT_SYMLINK_NOFOLLOW (its whole point: the link + * itself is chowned, not the target). + */ +int +fchownat(int fd, const char *path, uid_t owner, gid_t group, int flag) +{ + return syscall_ret(__syscall5(SYS_fchownat, fd, (long)path, (long)owner, (long)group, flag)); +} + +int +chown(const char *path, uid_t owner, gid_t group) +{ + return fchownat(VLIBC_STAT_AT_FDCWD, path, owner, group, 0); +} + +int +lchown(const char *path, uid_t owner, gid_t group) +{ + return fchownat(VLIBC_STAT_AT_FDCWD, path, owner, group, VLIBC_STAT_AT_SYMLINK_NOFOLLOW); +} + +int +fchown(int fd, uid_t owner, gid_t group) +{ + return syscall_ret(__syscall3(SYS_fchown, fd, (long)owner, (long)group)); +} diff --git a/src/stat/fstatat.c b/src/stat/fstatat.c new file mode 100644 index 0000000..6eb50a0 --- /dev/null +++ b/src/stat/fstatat.c @@ -0,0 +1,39 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "stat_impl.h" + +/* + * The stat family over SYS_newfstatat (the modern *at form; SYS_stat/ + * SYS_lstat exist only for compatibility) and SYS_fstat for the + * descriptor-based form. stat()/lstat() are newfstatat on AT_FDCWD with + * flag 0 / AT_SYMLINK_NOFOLLOW; the kernel fills the full 144-byte + * struct stat pinned by the static assertions in . + */ +int +fstatat(int fd, const char *path, struct stat *buf, int flag) +{ + return syscall_ret(__syscall4(SYS_newfstatat, fd, (long)path, (long)buf, flag)); +} + +int +stat(const char *path, struct stat *buf) +{ + return fstatat(VLIBC_STAT_AT_FDCWD, path, buf, 0); +} + +int +lstat(const char *path, struct stat *buf) +{ + return fstatat(VLIBC_STAT_AT_FDCWD, path, buf, VLIBC_STAT_AT_SYMLINK_NOFOLLOW); +} + +int +fstat(int fd, struct stat *buf) +{ + return syscall_ret(__syscall2(SYS_fstat, fd, (long)buf)); +} diff --git a/src/stat/mkdir.c b/src/stat/mkdir.c new file mode 100644 index 0000000..d53e9b8 --- /dev/null +++ b/src/stat/mkdir.c @@ -0,0 +1,24 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * mkdir over SYS_mkdir and mkdirat over SYS_mkdirat (the *at form takes + * the directory fd, so mkdir() cannot simply delegate to it — SYS_mkdir + * is the direct ABI). The kernel applies the process umask to mode. + */ +int +mkdirat(int fd, const char *path, mode_t mode) +{ + return syscall_ret(__syscall3(SYS_mkdirat, fd, (long)path, (long)mode)); +} + +int +mkdir(const char *path, mode_t mode) +{ + return syscall_ret(__syscall2(SYS_mkdir, (long)path, (long)mode)); +} diff --git a/src/stat/mkfifo.c b/src/stat/mkfifo.c new file mode 100644 index 0000000..4ed0d92 --- /dev/null +++ b/src/stat/mkfifo.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "stat_impl.h" + +/* + * mkfifo/mkfifoat over SYS_mknodat with the type bits fixed to S_IFIFO: + * creating a FIFO is mknodat(..., mode | S_IFIFO, dev = 0). POSIX defines + * mkfifo with no device argument, so the dev slot is always 0 here. + */ +int +mkfifoat(int fd, const char *path, mode_t mode) +{ + return syscall_ret(__syscall4(SYS_mknodat, fd, (long)path, (long)(mode | S_IFIFO), 0)); +} + +int +mkfifo(const char *path, mode_t mode) +{ + return mkfifoat(VLIBC_STAT_AT_FDCWD, path, mode); +} diff --git a/src/stat/mknod.c b/src/stat/mknod.c new file mode 100644 index 0000000..fb6fd93 --- /dev/null +++ b/src/stat/mknod.c @@ -0,0 +1,31 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" +#include "stat_impl.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * mknod/mknodat over SYS_mknodat (XSI, level 2). The mode argument must + * carry a file-type bit (S_IFIFO, S_IFCHR, S_IFBLK, S_IFREG, ...); dev is + * the raw device id for the S_IFCHR/S_IFBLK cases and is otherwise + * ignored by the kernel. Creating device nodes requires privilege + * (CAP_MKNOD); creating a FIFO or a regular file does not. + */ +int +mknodat(int fd, const char *path, mode_t mode, dev_t dev) +{ + return syscall_ret(__syscall4(SYS_mknodat, fd, (long)path, (long)mode, (long)dev)); +} + +int +mknod(const char *path, mode_t mode, dev_t dev) +{ + return mknodat(VLIBC_STAT_AT_FDCWD, path, mode, dev); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/stat/stat_impl.h b/src/stat/stat_impl.h new file mode 100644 index 0000000..276f044 --- /dev/null +++ b/src/stat/stat_impl.h @@ -0,0 +1,22 @@ +#ifndef VLIBC_STAT_INTERNAL_H +#define VLIBC_STAT_INTERNAL_H + +/* + * vlibc — internal constants for the stat wrappers (temporary). + * + * include/fcntl.h is owned by todo 21; until it lands, the *at wrappers + * need AT_FDCWD and AT_SYMLINK_NOFOLLOW locally. These names carry the + * VLIBC_STAT_ 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: + * + * AT_FDCWD -100, AT_SYMLINK_NOFOLLOW 0x100. + */ + +/* *at syscall base directory (kernel UAPI). */ +#define VLIBC_STAT_AT_FDCWD (-100) + +/* *at flag: operate on the link itself, not its target (kernel UAPI). */ +#define VLIBC_STAT_AT_SYMLINK_NOFOLLOW 0x100 + +#endif /* VLIBC_STAT_INTERNAL_H */ diff --git a/src/stat/umask.c b/src/stat/umask.c new file mode 100644 index 0000000..6149ee5 --- /dev/null +++ b/src/stat/umask.c @@ -0,0 +1,18 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * umask over SYS_umask. The kernel always returns the previous mask (even + * through the syscall_ret error translation, which never fires here — + * umask cannot fail), so no special handling is needed. + */ +mode_t +umask(mode_t cmask) +{ + return (mode_t)syscall_ret(__syscall1(SYS_umask, (long)cmask)); +} diff --git a/src/stat/utimensat.c b/src/stat/utimensat.c new file mode 100644 index 0000000..1fd0baf --- /dev/null +++ b/src/stat/utimensat.c @@ -0,0 +1,26 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * utimensat/futimens over SYS_utimensat. A NULL times array is passed + * through unchanged: the kernel sets both timestamps to the current time + * (the POSIX contract). futimens() is utimensat on the descriptor itself + * — Linux applies utimensat(fd, NULL, times, 0) to the file referenced by + * fd when the path is NULL, which is exactly futimens(fd, times). + */ +int +utimensat(int fd, const char *path, const struct timespec times[2], int flag) +{ + return syscall_ret(__syscall4(SYS_utimensat, fd, (long)path, (long)times, flag)); +} + +int +futimens(int fd, const struct timespec times[2]) +{ + return syscall_ret(__syscall4(SYS_utimensat, fd, 0, (long)times, 0)); +} diff --git a/tests/test_stat.c b/tests/test_stat.c new file mode 100644 index 0000000..7948b61 --- /dev/null +++ b/tests/test_stat.c @@ -0,0 +1,384 @@ +/* + * 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; +}