$RodHat_/search
Console Tips

name_to_handle_at(2): file handles that survive remounts

Published by · Updated

This article may contain Amazon affiliate links. I may earn a commission if you buy through them. Disclosure.

Editorial card: name_to_handle_at(2): file handles that survive remounts
Editorial card: RodHat
On this page · 6 sections
  1. What you get
  2. Reopening from a handle
  3. Filesystem support
  4. When to use this
  5. Pitfalls
  6. Availability

An inotify watch descriptor is just an integer in your process. The kernel’s inode is the real identity, but you only have the wd. Unmount the filesystem or move the container’s bind mount and that wd is gone, the path you stored is stale, and your daemon is tracking nothing. Open file descriptors are the same: process-local, gone when the fd closes or the process restarts.

If you need to identify a file in a way that survives remounts and can be handed to a fresh process to reopen later, the POSIX alternatives (path strings, st_ino + st_dev pairs) all have holes. name_to_handle_at(2) is what the kernel gives you instead.

What you get

#define _GNU_SOURCE
#include <sys/stat.h>
#include <fcntl.h>

int name_to_handle_at(int dirfd, const char *pathname,
                      struct file_handle *handle,
                      int *mount_id, int flags);

int open_by_handle_at(int mount_fd, struct file_handle *handle, int flags);

name_to_handle_at returns two things: a struct file_handle (an opaque byte string the filesystem owns) and a mount_id integer that identifies which mounted filesystem the file lives on. You can serialize that handle, stash it in a database, and hand it to a completely different process. That process calls open_by_handle_at with a file descriptor open to the mount point and gets back a normal fd pointing at the original file, wherever it has moved inside that filesystem.

The struct:

struct file_handle {
    unsigned int  handle_bytes;  /* in/out: size of f_handle[] */
    int           handle_type;   /* out: filesystem-defined type */
    unsigned char f_handle[0];   /* out: opaque handle bytes */
};

The handle_bytes field is both input (how much buffer you allocated) and output (how many bytes the filesystem filled). On first call, pass zero bytes and the kernel tells you how much to allocate. On second call, pass the right size.

struct file_handle *fh;
int mount_id, fd;

/* First call: discover required size */
fh = malloc(sizeof(*fh));
fh->handle_bytes = 0;
if (name_to_handle_at(AT_FDCWD, "/srv/data/important.db", fh, &mount_id, 0) == -1) {
    if (errno != EOVERFLOW) { perror("name_to_handle_at"); exit(1); }
}

/* Reallocate and retry */
fh = realloc(fh, sizeof(*fh) + fh->handle_bytes);
if (name_to_handle_at(AT_FDCWD, "/srv/data/important.db", fh, &mount_id, 0) == -1) {
    perror("name_to_handle_at"); exit(1);
}

/* fh and mount_id are now serializable */

No special capability needed to call name_to_handle_at. Any process that can stat the file can get its handle.

Reopening from a handle

open_by_handle_at is where the capability check lives. The mount_fd argument must be an open fd for any file or directory on the same mounted filesystem as the original file. The calling process needs CAP_DAC_READ_SEARCH. Without it, the call fails with EPERM.

int mount_fd = open("/srv/data", O_RDONLY);
int target_fd = open_by_handle_at(mount_fd, fh, O_RDWR);
if (target_fd == -1) { perror("open_by_handle_at"); }

The capability requirement is deliberate. A handle encodes an inode directly; a process with one can bypass normal path-based permission checks and open files it cannot reach by name. The kernel is not going to let you do that without CAP_DAC_READ_SEARCH.

AT_EMPTY_PATH in the flags argument to name_to_handle_at lets you get a handle for a file you already have open by fd, with no pathname at all:

name_to_handle_at(existing_fd, "", fh, &mount_id, AT_EMPTY_PATH);

Useful when you want to checkpoint a file’s identity at fd-open time and retain it past a potential rename or unlink.

Filesystem support

Not every filesystem implements the export operations this syscall pair requires. ext4, XFS, and Btrfs all support it. tmpfs does not: you will get EOPNOTSUPP. NFS, FUSE, and overlayfs vary by kernel version and configuration. Before relying on this in production, test on the actual filesystem the deployment will use.

The mount_id output matches the IDs in /proc/self/mountinfo. Cross-referencing lets you find which mount point to open for open_by_handle_at, and it lets a daemon track whether the underlying filesystem has been remounted between the handle-capture and the reopen.

When to use this

The practical cases are narrow but real:

NFS server userspace. Linux’s nfsd exports file handles to NFS clients. When a client presents a handle on a subsequent request, the server has to reopen the file without relying on a path. This syscall pair is the kernel hook that makes that work for userspace NFS implementations like nfs-ganesha.

Backup and dedup daemons. You snapshot a directory tree, get handles for every file, then later (after bind-mount juggling or snapshot cycling) reopen files by handle instead of crawling paths. This is faster and correct across renames.

Container-aware file-change trackers. A process inside a container has a different mount namespace than the host. A handle obtained inside the namespace can be handed to a privileged host process that opens it from the host’s view of the same underlying filesystem, bypassing the namespace boundary cleanly.

Pitfalls

Handles are filesystem-specific and opaque. You cannot copy a handle from one filesystem to another and expect it to work. If you store handles in a database, store the filesystem UUID alongside them so you can detect filesystem replacements.

The mount_id changes on remount. The handle itself (the opaque f_handle bytes) is stable as long as the inode exists on that filesystem, even across remounts. The mount_id is just bookkeeping for finding the right mount_fd to pass to open_by_handle_at.

If the file is deleted, the handle still contains the old inode number. Whether open_by_handle_at returns a valid fd to the unlinked-but-still-open inode, a stale handle error (ESTALE), or a new file at the recycled inode depends on the filesystem and timing.

Availability

Linux 2.6.39+. No BSD equivalent: FreeBSD has no name_to_handle_at; the closest analog is fhopen(2), which works only with superuser privileges and is specific to the NFS exporter path. macOS has nothing.

For the broader fd-as-identity pattern, the file descriptor series covers related syscalls: pidfd_open for processes, memfd_create for anonymous memory, and SCM_RIGHTS for passing fds across UNIX sockets. The inotify tip covers the watch descriptor model that name_to_handle_at complements when you need handle-level stability.

Sources

  1. name_to_handle_at(2) Linux man page (man7.org)
  2. fs/fhandle.c: kernel implementation of name_to_handle_at and open_by_handle_at (Linux kernel source (Bootlin Elixir))