Skip to content
FreeBSDDeep Dive Published Updated 7 min readViews unavailable

FreeBSD getfsstat(2): Mount Inventories Without False Freshness

Use FreeBSD getfsstat(2) with byte-sized buffers, bounded MNT_NOWAIT retries, and honest handling of stale or disappearing mounts.

getfsstat(2) returns statfs records for mounted file systems visible to the caller, making it useful for inventories, status dashboards, and periodic operational checks. Its signature looks small, but two details shape a safe collector: the buffer length is measured in bytes, and MNT_NOWAIT returns information that can be stale while avoiding waits on some unresponsive filesystems. A correct consumer records what it observed and when; it does not turn a best-effort inventory into a claim that every host mount is visible, current, or permanently present.

This interface sits at the boundary between an application and FreeBSD’s Virtual File System layer. It complements tools such as mount and df, but serves a different need when software wants structured records rather than parsing command output. For VFS context, see FreeBSD’s VFS layer; for mounts that appear on first access, compare the inventory behavior with FreeBSD autofs operations.

Count first, allocate in bytes, and retry boundedly

The manual declares the interface as:

int getfsstat(struct statfs *buf, long bufsize, int mode);

With buf == NULL, the call returns only the number of mounted filesystems visible in that caller’s mount view. With a buffer, bufsize is the number of bytes it can hold, not the number of struct statfs elements. On success the function returns the number of records; on failure it returns -1 and sets errno. If an application makes a count call and a later fill call, the mount table may have changed between them. Because the API does not distinguish an exactly full buffer from a truncated one, allocate headroom and treat a full buffer as inconclusive. A bounded retry can handle the rare race where enough newly visible mounts consume that headroom; the retry limit is a final backstop, not a guarantee that every kind of mount churn will be detected.

This collector uses MNT_NOWAIT, includes overflow checks for both allocation size and the long byte-count argument, and fails closed if the buffer remains saturated after four attempts:

#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>

int main(void)
{
    struct statfs *items = NULL;
    int used = -1;

    for (unsigned attempt = 0; attempt < 4; ++attempt) {
        int observed = getfsstat(NULL, 0, MNT_NOWAIT);
        if (observed == -1) {
            perror("getfsstat count");
            free(items);
            return 1;
        }
        if (observed > INT_MAX - 16) {
            fputs("mount count exceeds safe capacity arithmetic\n", stderr);
            free(items);
            return 1;
        }

        size_t capacity = (size_t)observed + 16;
        if (capacity > SIZE_MAX / sizeof(*items) ||
            capacity > (size_t)LONG_MAX / sizeof(*items)) {
            fputs("mount buffer size overflows\n", stderr);
            free(items);
            return 1;
        }

        struct statfs *replacement = calloc(capacity, sizeof(*items));
        if (replacement == NULL) {
            perror("calloc");
            free(items);
            return 1;
        }
        free(items);
        items = replacement;

        long bytes = (long)(capacity * sizeof(*items));
        int got = getfsstat(items, bytes, MNT_NOWAIT);
        if (got == -1) {
            perror("getfsstat data");
            free(items);
            return 1;
        }
        if ((size_t)got < capacity) {
            used = got;
            break;
        }
    }

    if (used < 0) {
        fputs("mount inventory remained saturated; sample rejected\n", stderr);
        free(items);
        return 2;
    }

    printf("observed %d mount records (MNT_NOWAIT)\n", used);
    for (int i = 0; i < used; ++i) {
        printf("%s\t%s\t%s\tflags=%#llx\n",
            items[i].f_fstypename,
            items[i].f_mntonname,
            items[i].f_mntfromname,
            (unsigned long long)items[i].f_flags);
    }
    free(items);
    return 0;
}

The capacity = observed + 16 margin is a policy choice, not an API guarantee. In FreeBSD 15.1, the MNT_NOWAIT count path applies the caller-visibility checks, while the fill path applies those same checks and then also skips mounts that cannot be made busy immediately, such as a mount being unmounted. On a stable mount list, the fill is therefore more selective than the count and ordinarily returns fewer records than the allocated capacity. The got == capacity branch is chiefly a backstop for exceptional growth between the two calls: after accounting for mounts the fill pass skips, at least sixteen additional visible records would need to enter the fill result to consume all the headroom. Because getfsstat() does not tell the caller whether a full buffer was exact or truncated, the sample conservatively retries and ultimately rejects a persistently full result rather than claiming completeness. A production collector should send that error to monitoring and avoid publishing a possibly incomplete inventory as complete. The program is also intentionally not a security decision engine: a mount source name, path, filesystem type, or flag snapshot should be checked against the applicable policy and current path access before a sensitive operation.

Choose freshness behavior deliberately

MNT_NOWAIT tells the kernel to return information already available without requesting an update from each filesystem. The manual warns that some information can therefore be out of date. Its benefit is that the call does not block waiting for a filesystem that cannot respond. It also skips a filesystem currently being unmounted, even if that unmount later fails. A monitoring sample can thus omit a transient mount or report cached statistics; this is not evidence that the underlying filesystem was cleanly removed.

MNT_WAIT is the documented normal mode and requests updated information. That can be appropriate for an interactive administrative query, but it changes the failure and latency budget of a collector. If a remote or unhealthy filesystem is slow, a request for refreshed statistics can delay a process that polls all filesystems together. Select a mode based on the caller’s purpose, and put an external deadline around the collector if its overall service must remain responsive. Do not confuse that deadline with cancellation of a kernel call unless the chosen mechanism actually provides that guarantee.

Record the collection timestamp, mode, number of returned structures, and collection errors. When comparing counters, identify the filesystem record by more than a display name where the application needs a stable join, and tolerate fields that the manual says are undefined for a particular filesystem: FreeBSD sets such fields to -1. The statfs documentation cautions that fields retain traditional UNIX semantics and that some filesystems can report similar-looking values with different meanings. For example, for FAT12/FAT16 msdosfs can report f_files and f_ffree as total and free root-directory entries rather than inode counts.

Separate a mount inventory from path authorization

An inventory is an observation of caller-visible mount records, not a promise about a later pathname lookup or a host-wide view. FreeBSD’s kernel filters mounts that a process’s prison cannot see and can apply MAC mount-visibility checks, so a jailed or policy-constrained collector may omit mounts visible elsewhere on the host. A mount can also appear, disappear, or change after the collector returns. If a program needs information for a particular open file, fstatfs(fd, ...) asks about the filesystem associated with that file descriptor. If it needs to inspect a path, statfs(path, ...) asks about the filesystem containing that path and can fail because of path lookup or permission errors. Keep this caller-visible inventory separate from authorization, storage quota decisions, and operations that require a fresh per-path check.

The source and mount-on fields are useful diagnostic labels, but they are not universally canonical identities. A remote mount string, a device alias, or a stacked filesystem can describe a view rather than the final storage resource. Likewise, free-space fields are filesystem-provided counters, not a reservation for the next writer. For an allocation-sensitive operation, perform the actual operation and handle its error path; a successful inventory call cannot reserve capacity or stop another process from changing it.

Test churn and failure paths

Test the collector with no mounts beyond the base environment, with a large number of filesystems, and while a disposable mount is repeatedly attached and detached. Verify the byte calculation using capacity * sizeof(struct statfs), and force allocation-failure handling in a test build. To exercise the saturation branch reliably, put the syscall behind a test wrapper and simulate a count followed by exactly capacity returned records; verify that the collector retries only up to its limit and then rejects a persistently full sample. Also simulate the count including a mount that the later MNT_NOWAIT fill skips because it is being unmounted; confirm the shorter result is accepted, since the kernel documents this omission behavior. A stable fill below observed + 16 should succeed immediately. A zero-headroom test can demonstrate the ambiguity of an exactly full result, but should not be described as proof that the native API reported truncation. Where available, test with an unresponsive network filesystem in a controlled environment: compare MNT_NOWAIT responsiveness with the deliberate MNT_WAIT path without using production data.

The goal is not to eliminate every race in a changing mount table. It is to make the observation bounded, correctly sized, and honest about freshness so a monitoring system can distinguish a complete-enough sample from a missing or transient record.

Related:

Sources:

Comments