feat(statvfs): statvfs/fstatvfs (L2 XSI)

This commit is contained in:
2026-09-05 23:10:06 -04:00
parent 9ffe86fc55
commit f9a055f547
3 changed files with 325 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
#ifndef VLIBC_SYS_STATVFS_H
#define VLIBC_SYS_STATVFS_H
/*
* vlibc — <sys/statvfs.h>.
*
* 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 <linux/statfs.h>
* 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 <sys/stat.h>).
*
* 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 <vlibc/features.h>
#include <sys/types.h> /* 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 */
+88
View File
@@ -0,0 +1,88 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/statvfs.h>
#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) */
+155
View File
@@ -0,0 +1,155 @@
/*
* vlibc — sys/statvfs.h test (todo 34).
*
* Exercises the whole <sys/statvfs.h> 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: <sys/statvfs.h> is entirely level 2, so
* at level 1 this TU compiles to a no-op runner.
*/
#include <sys/statvfs.h>
#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 <sys/statvfs.h> 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: <sys/statvfs.h> is level 2, not available here\n");
return 0;
}
#endif /* VLIBC_LEVEL_GE(2) */