statx(2): what stat(2) should have been
Published by RodHat

stat(2) has been in the kernel since the days when a file system fitting on a floppy disk was considered ambitious. The struct it fills has not meaningfully grown since POSIX froze it. Three timestamps: atime, mtime, ctime. No birth time. No way to ask for just the size without triggering a full VFS lookup. No inode attribute flags. No mount ID. If you want to know whether a file is immutable or encrypted, you use an ioctl like some kind of cave person.
Linux 4.11, shipped in 2017, added statx(2). It costs you a syscall() call instead of a glibc wrapper, because glibc only added a wrapper in version 2.28, and if you are on an older system you are doing it by hand regardless. The tradeoff is worth it for any code that actually needs what statx provides.
The signature
#include <sys/syscall.h>
#include <linux/stat.h>
#include <unistd.h>
int statx(int dirfd, const char *pathname, int flags,
unsigned int mask, struct statx *statxbuf);
dirfd and pathname work the same as openat(2): pass AT_FDCWD for current directory, or an open directory fd for relative lookups. flags controls symlink handling and cache behavior. mask is where things get interesting.
mask: ask for what you need
stat(2) fills the whole struct or fails. statx takes a bitmask of what you want:
STATX_TYPE — st_mode file type bits
STATX_MODE — st_mode permission bits
STATX_NLINK — stx_nlink
STATX_UID — stx_uid
STATX_GID — stx_gid
STATX_ATIME — stx_atime
STATX_MTIME — stx_mtime
STATX_CTIME — stx_ctime
STATX_INO — stx_ino
STATX_SIZE — stx_size
STATX_BLOCKS — stx_blocks
STATX_BASIC_STATS — all of the above (compatibility with stat)
STATX_BTIME — stx_btime (birth time)
STATX_MNT_ID — stx_mnt_id (mount ID, added 5.8)
STATX_DIOALIGN — stx_dio_mem_align, stx_dio_offset_align (direct I/O alignment, added 6.1)
STATX_ALL — everything the kernel knows
The kernel is not obligated to return only what you asked for, and it may return more. But the stx_mask field in the returned struct tells you what it actually filled in. Check it before reading any field you care about:
struct statx sx;
if (statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_BTIME, &sx) == 0) {
if (sx.stx_mask & STATX_BTIME) {
/* birth time is valid */
printf("born: %lld.%09u\n",
(long long)sx.stx_btime.tv_sec,
sx.stx_btime.tv_nsec);
} else {
/* filesystem doesn't track birth time; field is zeroed */
}
}
This is how you handle the real world: ext4 on Linux tracks birth time as crtime in the inode since 2008, but FAT32 does not, tmpfs does not, and some network filesystems may not surface it. The mask tells you whether you got a real answer.
Birth time
stx_btime is the reason most people want statx. stat(2) has no equivalent. ctime in POSIX means “inode change time,” not “creation time,” which has confused approximately every programmer who ever touched it for the first time.
Filesystems that support birth time on Linux: ext4, xfs (since 5.10), btrfs, tmpfs does not, NFS depends on the server. ZFS on Linux supports it. Check stx_mask & STATX_BTIME before trusting the field.
On FreeBSD, stat(2) has had st_birthtime since forever. The BSDs got this right before you were born. Noted without further comment.
Inode attribute flags
stx_attributes is a bitmask of per-inode flags that stat(2) had no way to surface. stx_attributes_mask tells you which flags the filesystem supports; only bits set in the mask are meaningful in stx_attributes.
STATX_ATTR_COMPRESSED — filesystem compressed the file
STATX_ATTR_IMMUTABLE — file cannot be modified or deleted (chattr +i)
STATX_ATTR_APPEND — file is append-only (chattr +a)
STATX_ATTR_NODUMP — file excluded from dump(8)
STATX_ATTR_ENCRYPTED — file requires a key to access (ext4, f2fs, fscrypt)
STATX_ATTR_AUTOMOUNT — this is an automount point
STATX_ATTR_MOUNT_ROOT — this is a mount root (stx_mnt_id refers to the mount below)
STATX_ATTR_VERITY — fs-verity is enabled (kernel validates reads via Merkle tree)
STATX_ATTR_DAX — file is in DAX (direct access) mode; no page cache
Checking immutability without statx required opening the file and running ioctl(fd, FS_IOC_GETFLAGS, &flags). Now you can do it with a path and no open fd:
struct statx sx;
if (statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT,
STATX_BASIC_STATS, &sx) == 0) {
if ((sx.stx_attributes_mask & STATX_ATTR_IMMUTABLE) &&
(sx.stx_attributes & STATX_ATTR_IMMUTABLE)) {
/* file is immutable */
}
}
Mount ID
stx_mnt_id (requires STATX_MNT_ID in the mask, available since 5.8) gives you the kernel’s internal mount ID for the mount that contains this file. Two files with the same stx_mnt_id are on the same mount. Two files with different IDs are not, even if their device numbers match (which can happen with bind mounts, stacked filesystems, and network shares that export multiple paths from one underlying device).
If you have a newer kernel (6.8+), STATX_MNT_ID_UNIQUE gives you a 64-bit ID that does not reuse across mount lifetimes; the regular stx_mnt_id is only 64 bits wide but the lower bits can reuse after a mount is unmounted and a new mount takes the same slot.
Cross-referencing stx_mnt_id against /proc/self/mountinfo gives you the full mount information: where it came from, what filesystem type, what mount options. Better than parsing df output.
Cache control flags
The flags argument controls how the kernel fetches the data, which matters on network filesystems:
AT_STATX_SYNC_AS_STAT — kernel default; matches stat(2) behavior
AT_STATX_FORCE_SYNC — force a revalidation from the server (NFS, CIFS)
AT_STATX_DONT_SYNC — return whatever is cached, even if stale
AT_STATX_DONT_SYNC is useful when you are scanning a large NFS directory tree and want rough metadata without hammering the server for per-file revalidation. The data may be stale. You know the tradeoff.
Full example
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <linux/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <string.h>
static void print_ts(const char *label, struct statx_timestamp *ts)
{
char buf[64];
time_t t = ts->tv_sec;
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", gmtime(&t));
printf(" %-8s %s.%09u UTC\n", label, buf, ts->tv_nsec);
}
int main(int argc, char **argv)
{
const char *path = argc > 1 ? argv[1] : ".";
struct statx sx;
if (statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_ALL, &sx) < 0) {
fprintf(stderr, "statx %s: %s\n", path, strerror(errno));
return 1;
}
printf("path: %s\n", path);
printf("ino: %llu\n", (unsigned long long)sx.stx_ino);
printf("size: %llu\n", (unsigned long long)sx.stx_size);
printf("mode: %06o\n", sx.stx_mode);
printf("uid/gid: %u/%u\n", sx.stx_uid, sx.stx_gid);
if (sx.stx_mask & STATX_ATIME) print_ts("atime:", &sx.stx_atime);
if (sx.stx_mask & STATX_MTIME) print_ts("mtime:", &sx.stx_mtime);
if (sx.stx_mask & STATX_CTIME) print_ts("ctime:", &sx.stx_ctime);
if (sx.stx_mask & STATX_BTIME) print_ts("btime:", &sx.stx_btime);
else printf(" btime: (not available on this filesystem)\n");
if (sx.stx_mask & STATX_MNT_ID)
printf("mnt_id: %llu\n", (unsigned long long)sx.stx_mnt_id);
printf("attr: 0x%016llx (mask 0x%016llx)\n",
(unsigned long long)sx.stx_attributes,
(unsigned long long)sx.stx_attributes_mask);
if ((sx.stx_attributes_mask & STATX_ATTR_IMMUTABLE) &&
(sx.stx_attributes & STATX_ATTR_IMMUTABLE))
printf(" IMMUTABLE\n");
if ((sx.stx_attributes_mask & STATX_ATTR_ENCRYPTED) &&
(sx.stx_attributes & STATX_ATTR_ENCRYPTED))
printf(" ENCRYPTED (fscrypt)\n");
if ((sx.stx_attributes_mask & STATX_ATTR_VERITY) &&
(sx.stx_attributes & STATX_ATTR_VERITY))
printf(" VERITY\n");
if ((sx.stx_attributes_mask & STATX_ATTR_DAX) &&
(sx.stx_attributes & STATX_ATTR_DAX))
printf(" DAX\n");
return 0;
}
Compile with gcc -o xstat xstat.c. No special libraries. Run it on any file. On ext4 with a file that was never touched since creation, btime and mtime will match. Interesting when they do not.
glibc wrapper
glibc 2.28 (2018) shipped statx(3) as a proper wrapper. If your toolchain is recent enough, you can include <sys/stat.h> and call statx() directly without the syscall() boilerplate. The struct name changed slightly between the kernel header (linux/stat.h) and the glibc header (sys/stat.h) in some versions, so pick one header and stick with it per translation unit.
Musl still does not have a statx wrapper at time of writing. Call syscall(SYS_statx, ...) directly if you are targeting musl.
When to use it
Use statx when: you need birth time; you need to check inode flags (immutable, append-only, encrypted, verity) without opening the file; you need the mount ID to confirm two paths are on the same mount; you are on NFS and want explicit cache control; or you are scanning many files and want to request only a small subset of fields to reduce VFS overhead on network filesystems.
For plain local-filesystem work where you just want size and mtime and do not need birth time or attribute flags, stat(2) via glibc is fine. There is no point wrapping everything in statx for novelty. Use the right tool.
The new mount API post covers stx_mnt_id in context: once you have detached mount fds, statx is how you confirm which mount a file actually landed on. The openat2(2) post pairs naturally here: both are part of the same generation of “we finally got this right” syscalls that came out of the 4.x/5.x era. And if you are checking STATX_ATTR_ENCRYPTED on fscrypt files, the landlock post covers restricting what paths a process can reach in the first place.