From f9a055f547585234b4e0c1f82310388d56740e1e Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 23:10:06 -0400 Subject: [PATCH] feat(statvfs): statvfs/fstatvfs (L2 XSI) --- include/sys/statvfs.h | 82 ++++++++++++++++++++++ src/stat/statvfs.c | 88 ++++++++++++++++++++++++ tests/test_statvfs.c | 155 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 include/sys/statvfs.h create mode 100644 src/stat/statvfs.c create mode 100644 tests/test_statvfs.c diff --git a/include/sys/statvfs.h b/include/sys/statvfs.h new file mode 100644 index 0000000..39ebeac --- /dev/null +++ b/include/sys/statvfs.h @@ -0,0 +1,82 @@ +#ifndef VLIBC_SYS_STATVFS_H +#define VLIBC_SYS_STATVFS_H + +/* + * vlibc — . + * + * Filesystem statistics in an ABI-independent POSIX shape: block and file + * counts as unsigned counts over a reported block size, plus the read-only + * and setuid-disabling mount flags and the longest file name. This header + * carries only the portable view; the Linux kernel's private + * layout (signed longs, an int-pair f_fsid, a f_spare tail) never leaks + * into it. + * + * Level 2 (muslmimic): the whole header (statvfs and fstatvfs are XSI + * [CX] in POSIX.1-2008, not base; the base filesystem + * queries are stat()/fstat() in ). + * + * f_flag holds ST_RDONLY and/or ST_NOSUID, and f_fsid is a best-effort + * filesystem identifier (the kernel does not populate it for every + * filesystem). The block counts are measured in f_frsize units (which is + * f_bsize when the filesystem does not distinguish a transfer size). + * + * None of these declarations carries an intent attribute: both functions + * perform I/O with side effects and report failures through errno, so + * const/pure would be unsound. + */ + +#include + +#include /* fsblkcnt_t, fsfilcnt_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI. */ + +/* + * Filesystem statistics. f_flag is a bitwise OR of ST_RDONLY and + * ST_NOSUID; f_frsize is the fundamental block size the count fields are + * expressed in. f_fsid is 0 when the filesystem provides no identifier. + */ +struct statvfs +{ + unsigned long f_bsize; /* preferred I/O block size, bytes */ + unsigned long f_frsize; /* fundamental block size, bytes */ + fsblkcnt_t f_blocks; /* total blocks, in f_frsize units */ + fsblkcnt_t f_bfree; /* free blocks */ + fsblkcnt_t f_bavail; /* free blocks available to an unprivileged process */ + fsfilcnt_t f_files; /* total file serial numbers (inodes) */ + fsfilcnt_t f_ffree; /* free file serial numbers */ + fsfilcnt_t f_favail; /* free serial numbers available to an unprivileged process */ + unsigned long f_fsid; /* filesystem ID (best effort) */ + unsigned long f_flag; /* mount flags: ST_RDONLY | ST_NOSUID */ + unsigned long f_namemax; /* maximum file name length */ +}; + +/* f_flag values (also the kernel's f_flags bits on Linux). */ +#define ST_RDONLY 1 /* read-only filesystem */ +#define ST_NOSUID 2 /* set-user-ID/set-group-ID bits ignored on exec */ + +/* + * Store statistics for the filesystem holding path into buf; return 0, or + * -1 with errno set. XSI. + */ +int +statvfs(const char *restrict path, struct statvfs *restrict buf); + +/* + * Like statvfs(), for the filesystem holding the open descriptor fildes; + * return 0, or -1 with errno set. XSI. + */ +int +fstatvfs(int fildes, struct statvfs *buf); +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_STATVFS_H */ diff --git a/src/stat/statvfs.c b/src/stat/statvfs.c new file mode 100644 index 0000000..bdf1f99 --- /dev/null +++ b/src/stat/statvfs.c @@ -0,0 +1,88 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * Linux struct statfs (kernel ABI, x86_64; linux/statfs.h). Layout and + * sizes are LP64 facts: f_type..f_ffree are longs, f_fsid is an int pair, + * then f_namelen/f_frsize/f_flags/f_spare[4] complete the 120 bytes the + * kernel copies out (the same layout src/misc/pathconf.c pins for + * pathconf()/fpathconf()). The signed kernel longs are non-negative on + * every real filesystem, so they convert straight into the unsigned POSIX + * counts below. + */ +struct vlibc_kstatfs +{ + long f_type; + long f_bsize; + long f_blocks; + long f_bfree; + long f_bavail; + long f_files; + long f_ffree; + int f_fsid[2]; + long f_namelen; + long f_frsize; + long f_flags; + long f_spare[4]; +}; + +/* Map one kernel statfs record into the portable POSIX statvfs view. */ +static void +statvfs_fill(struct statvfs *out, const struct vlibc_kstatfs *in) +{ + out->f_bsize = (unsigned long)in->f_bsize; + out->f_frsize = in->f_frsize != 0 ? (unsigned long)in->f_frsize : (unsigned long)in->f_bsize; + out->f_blocks = (unsigned long)in->f_blocks; + out->f_bfree = (unsigned long)in->f_bfree; + out->f_bavail = (unsigned long)in->f_bavail; + out->f_files = (unsigned long)in->f_files; + out->f_ffree = (unsigned long)in->f_ffree; + out->f_favail = (unsigned long)in->f_ffree; + /* Reconstruct the kernel's 64-bit fsid (val[0] holds the low word on + * the little-endian x86_64; the same packing glibc's statvfs reports). */ + out->f_fsid = ((unsigned long)(unsigned int)in->f_fsid[1] << 32) | + (unsigned long)(unsigned int)in->f_fsid[0]; + out->f_flag = (unsigned long)(in->f_flags & (ST_RDONLY | ST_NOSUID)); + out->f_namemax = (unsigned long)in->f_namelen; +} + +/* + * statvfs()/fstatvfs(): filesystem statistics in the portable POSIX shape, + * over SYS_statfs/SYS_fstatfs (which share the same kernel struct and the + * same mapping). The kernel returns 0 on success after filling all 120 + * bytes; failures come back as -errno and are translated by syscall_ret. + */ +int +statvfs(const char *restrict path, struct statvfs *restrict buf) +{ + struct vlibc_kstatfs fs; + + if (syscall_ret(__syscall2(SYS_statfs, (long)path, (long)&fs)) != 0) + { + return -1; + } + statvfs_fill(buf, &fs); + return 0; +} + +int +fstatvfs(int fildes, struct statvfs *buf) +{ + struct vlibc_kstatfs fs; + + if (syscall_ret(__syscall2(SYS_fstatfs, fildes, (long)&fs)) != 0) + { + return -1; + } + statvfs_fill(buf, &fs); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_statvfs.c b/tests/test_statvfs.c new file mode 100644 index 0000000..1477398 --- /dev/null +++ b/tests/test_statvfs.c @@ -0,0 +1,155 @@ +/* + * vlibc — sys/statvfs.h test (todo 34). + * + * Exercises the whole surface end to end: + * + * 1. statvfs("/") returns 0 with sane values: a positive block size, a + * positive transfer size, a positive total block count whose free + * and available sub-counts stay within it (unsigned compares — all + * counts are fsblkcnt_t/fsfilcnt_t = unsigned long), and a positive + * maximum name length. + * 2. fstatvfs on a raw-open descriptor of "/" returns 0 and agrees + * with statvfs("/") on the stable fields (block size, total blocks, + * name length), since both name the same filesystem. + * + * The -f mode runs the failure shapes — statvfs on a nonexistent path and + * fstatvfs on a closed descriptor (-1) — asserting return values only + * (errno is never read: vlibc's errno slot collides with the host TCB). It + * leaves via a raw SYS_exit_group so no host cleanup runs after the + * library's errno writes (house pattern, tests/test_systime.c). + * + * No host libc headers are included (the -Iinclude path would shadow + * GCC's internal headers); diagnostics go through raw SYS_write. The whole + * body mirrors the header's gate: is entirely level 2, so + * at level 1 this TU compiles to a no-op runner. + */ + +#include + +#include "../src/internal/syscall.h" + +/* 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); +} + +#if VLIBC_LEVEL_GE(2) + +/* Kernel-UAPI open values, local to this test (include/fcntl.h). */ +#define T34_AT_FDCWD (-100) +#define T34_O_RDONLY 0 + +#define T34_MISSING "/nonexistent-vlibc-t34-xyz" + +static int failures; + +static void +check(int ok, const char *msg) +{ + if (ok) + { + say(1, "ok "); + } + else + { + say(1, "FAIL "); + failures++; + } + say(1, msg); + say(1, "\n"); +} + +/* 1 + 2. statvfs("/") sanity, then fstatvfs on a raw descriptor of "/". */ +static void +happy_scenarios(void) +{ + struct statvfs s = {0}; + struct statvfs d = {0}; + long fd; + + check(statvfs("/", &s) == 0, "statvfs(\"/\") returns 0"); + check(s.f_bsize > 0, "statvfs f_bsize > 0"); + check(s.f_frsize > 0, "statvfs f_frsize > 0"); + check(s.f_blocks > 0, "statvfs f_blocks > 0"); + check(s.f_bfree <= s.f_blocks, "statvfs f_bfree within f_blocks (unsigned)"); + check(s.f_bavail <= s.f_bfree, "statvfs f_bavail within f_bfree (unsigned)"); + check(s.f_namemax > 0, "statvfs f_namemax > 0"); + + fd = __syscall4(SYS_openat, T34_AT_FDCWD, (long)"/", T34_O_RDONLY, 0); + check(fd >= 0, "raw openat(\"/\") opens the root directory"); + if (fd >= 0) + { + check(fstatvfs((int)fd, &d) == 0, "fstatvfs(fd of \"/\") returns 0"); + check(d.f_bsize == s.f_bsize, "fstatvfs f_bsize matches statvfs"); + check(d.f_blocks == s.f_blocks, "fstatvfs f_blocks matches statvfs"); + check(d.f_namemax == s.f_namemax, "fstatvfs f_namemax matches statvfs"); + check(__syscall1(SYS_close, fd) == 0, "raw close of the root descriptor"); + } + + if (failures == 0) + { + say(1, "all statvfs tests passed\n"); + } + else + { + say(1, "FAILURES\n"); + } +} + +/* The failure shapes; return values only, never errno. Exit via a 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, tests/test_systime.c). */ +static void +failure_scenarios(void) +{ + struct statvfs s = {0}; + + check(statvfs(T34_MISSING, &s) == -1, "-f statvfs(nonexistent) returns -1"); + check(fstatvfs(-1, &s) == -1, "-f fstatvfs(-1) returns -1"); + + if (failures == 0) + { + say(1, "all statvfs failure scenarios passed\n"); + } + __syscall1(SYS_exit_group, failures == 0 ? 0 : 1); + /* not reached */ +} + +int +main(int argc, char **argv) +{ + if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0') + { + failure_scenarios(); + } + + happy_scenarios(); + return failures == 0 ? 0 : 1; +} + +#else /* !VLIBC_LEVEL_GE(2) */ + +/* + * Level 1: every symbol is gated at level 2, so there is + * nothing to run. Keep the TU compilable at any configured profile. + */ +int +main(void) +{ + say(1, "SKIP: is level 2, not available here\n"); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */