Skip to content
LinuxDeep Dive Published Updated 8 min readViews unavailable

Linux Mount Inventory with listmount() and statmount()

Enumerate Linux mount IDs and inspect structured mount metadata while handling namespaces, feature detection, pagination, and changing topology safely.

Linux exposes mount topology through more than path lookup. Since Linux 6.8, listmount() can enumerate mount IDs beneath a mount in the caller’s visible mount tree, and statmount() can return structured information for an individual ID. Together they let an inventory tool correlate mount IDs, parent IDs, mount points, filesystem types, propagation attributes, and selected option data without parsing a text record for every field.

These are inspection interfaces. They do not attach, clone, reconfigure, or unmount anything. That is a different job from the file-descriptor mount-creation workflow built around fsopen(), fsconfig(), fsmount(), and move_mount(). Likewise, STATMOUNT_BY_FD on newer kernels asks about the mount associated with an already-existing descriptor; it does not create a mount.

Enumeration and per-mount status are separate operations

listmount() takes a request structure, a buffer for 64-bit mount IDs, a capacity, and flags. LSMT_ROOT selects the root of the current mount namespace; another accepted ID narrows the search to a subtree. The IDs are handles for follow-up queries, not durable names for storage systems or filesystems.

statmount() takes one such mount ID and a bitmask of the data groups the caller wants. Its result can include mount and parent IDs, mount attributes and propagation state, filesystem type, mount root and mount-point strings, superblock information, and selected options. Request only the groups your inventory needs. The returned statmount.mask is the authority for which groups are valid: a kernel or filesystem may omit a requested group, and the returned mask need not equal the request mask.

Text fields such as fs_type, mnt_root, and mnt_point are offsets into the result’s trailing str area, not independent pointers. Check the corresponding mask bit before reading a field, validate an offset against the returned buffer size, and use the string area specified by the UAPI. If the buffer cannot hold required strings, statmount() fails with EOVERFLOW; allocate a larger buffer and retry rather than treating a partial structure as complete.

Mount IDs are not file metadata

statx() answers a different question: it looks up one path or descriptor and returns metadata for that object. When STATX_MNT_ID_UNIQUE is requested and returned in stx_mask, stx_mnt_id identifies the mount containing that object and can seed a mount query. statx() does not enumerate the other mounts or return the whole mount tree. Conversely, listmount() discovers IDs, and statmount() describes a mount rather than the inode at a path.

Use the returned parent IDs to reconstruct relationships instead of assuming that a textual path prefix tells you which mount is the parent. Bind mounts, stacked mounts, process roots, and mount namespaces can make path-based intuition misleading. Treat mount IDs as runtime correlation values, not persistent identifiers across reboot.

Paginate with the last ID, not an array offset

The param member of the listmount() request is a cursor: initialize it to zero, then set it to the final mount ID returned by each successful nonempty call. A return value greater than zero is the number of IDs written; zero means there are no more results for that query. Always check -1 and errno separately from the zero-results case. The following read-only loop shows the forward, flags-zero path and prints the IDs it discovers.

#define _GNU_SOURCE
#include <errno.h>
#include <linux/mount.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

int main(void)
{
#if !defined(SYS_listmount) || !defined(LSMT_ROOT) || \
    !defined(MNT_ID_REQ_SIZE_VER0)
    fputs("Build with Linux UAPI headers that define listmount\n", stderr);
    return 2;
#else
    uint64_t ids[128];
    uint64_t cursor = 0;
    struct mnt_id_req req = {
        .size = MNT_ID_REQ_SIZE_VER0,
        .mnt_id = LSMT_ROOT,
        .param = 0,
    };

    for (;;) {
        req.param = cursor;
        long count = syscall(SYS_listmount, &req, ids,
                             sizeof(ids) / sizeof(ids[0]), 0U);
        if (count == -1) {
            if (errno == ENOSYS)
                fputs("The running kernel does not implement listmount\n", stderr);
            else
                perror("listmount");
            return 1;
        }
        if (count == 0)
            break;

        for (long i = 0; i < count; i++)
            printf("%llu\n", (unsigned long long)ids[i]);
        cursor = ids[count - 1];
    }
    return 0;
#endif
}

This example requires headers that define the syscall number and Linux mount UAPI. The first published request structure is 24 bytes, represented by MNT_ID_REQ_SIZE_VER0; use the version matching the fields you actually pass rather than guessing a size when targeting a range of kernels. syscall() has no glibc wrapper for these interfaces, and a build-time constant does not prove that the running kernel implements the syscall. ENOSYS means the requested system call number is not implemented. Do not treat every EINVAL as a missing feature: it can also indicate an invalid flag, request size, or request combination.

For each ID, issue a separate statmount() request with only the needed STATMOUNT_* bits, then validate the returned mask and string offsets. Pagination and detail lookups are separate calls, not an atomic snapshot. A mount can be added or disappear between pages or lookups, so the observed set can change; handle ENOENT as changed topology and decide whether to retry, skip, or restart the inventory. Do not silently turn that race into a claim that the mount was absent from the original observation.

Namespace scope and authority

With the basic request, listmount() enumerates the caller’s current mount namespace, and LSMT_ROOT means that namespace’s root mount. Access requires CAP_SYS_ADMIN in the relevant user namespace. statmount() has the same capability boundary for reading mount status; knowing or receiving an ID is not permission to inspect every namespace on the machine.

The interface has gained versioned ways to query a foreign mount namespace: a namespace ID selector is available since Linux 6.11, and a namespace file-descriptor selector since Linux 6.18. The documented foreign namespace relationship is constrained: it must be a child of the caller’s namespace. These extensions are not needed for a self-namespace inventory. Feature-detect the actual headers and kernel behavior, and keep namespace selection explicit rather than silently substituting the caller’s namespace when a requested target cannot be queried.

Version and flag compatibility

Capability Linux version Safe compatibility rule
listmount() and statmount() 6.8 Build with headers that define SYS_listmount, SYS_statmount, and the request constants; at runtime distinguish ENOSYS, permission errors, invalid requests, and disappearing IDs.
Reverse listmount() ordering 6.11 LISTMOUNT_REVERSE is an optional flag in newer UAPI. This example passes zero flags; use reverse order only when both headers and the running kernel support it.
Foreign namespace ID selector 6.11 Use the versioned request fields only when the target supports that request version and the namespace relationship and permission checks pass.
STATMOUNT_MNT_OPTS 6.11 Request it explicitly and check the result mask; it is not present on the original 6.8 interface.
Filesystem subtype, source, and option arrays 6.13 These are later field groups; support can differ by kernel and filesystem.
UID/GID maps and STATMOUNT_SUPPORTED_MASK 6.15 Treat these as optional returned fields and validate their mask bits and counts.
Foreign namespace file-descriptor selector 6.18 Newer request structure; do not set this field when using the original request version.
STATMOUNT_BY_FD 7.0 Queries the mount associated with an existing mount FD. This is still an inspection operation, not fsmount() or another mount-creation call.

Linux distributions can backport kernel features, so a release string is a planning hint, not proof of support. Prefer compiling against suitable UAPI headers, using only fields and flags your program needs, and handling the syscall result and returned mask. Keep listmount() flags at zero unless reverse ordering is required; an unsupported or malformed flag may produce EINVAL.

A careful operator check with findmnt

On a util-linux build that supports its experimental listmount backend, compare the structured syscall path with the traditional procfs-backed view in the same shell and mount namespace:

uname -r
findmnt --version
findmnt --kernel=listmount --list
findmnt --kernel=mountinfo --list

These commands inspect the current mount table; they do not modify it. The listmount backend may omit details such as SOURCE, and the man page labels it experimental. A failure can reflect an older kernel or util-linux build, missing syscall support, namespace scope, or insufficient capability. It does not mean that the machine has no mounts. For scripts that require a stable machine-readable contract, specify output columns and validate the chosen backend on the exact target release instead of parsing the default human-oriented tree.

Use /proc/self/mountinfo as the compatibility path when the syscalls or userspace tooling are unavailable. Its format is extensible: parse the separator and escaped fields according to the documented format, ignore unknown optional fields, and do not confuse per-mount options with superblock options. Whether using procfs or the syscall pair, record the namespace context and observation time with an inventory. A mount table is a view of one namespace, not a host-wide inventory and not a security authorization decision.

Related:

Sources:

Comments