open_by_handle_at(2)
โจ NAME โจ
name_to_handle_at, open_by_handle_at โ obtain handle for a pathname and open file via a handle ๐ซถ
okay bestie so this is the open_by_handle_at(2) page and it's lowkey one of the coolest syscall pairs in Linux, no cap. two calls, one vibe: grab a magic little token for a file, then use that token later to open the file. let's GO ๐
๐ LIBRARY ๐
Standard C library (libc, -lc) โ yes you need to link the C library, that's just how it is ๐
๐ธ SYNOPSIS ๐ธ
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h>
int name_to_handle_at(int dirfd, const char *path,
struct file_handle *handle,
int *mount_id, int flags);
int open_by_handle_at(int mount_fd, struct file_handle *handle,
int flags);
that #define _GNU_SOURCE at the top?? not optional bestie, it's literally the whole reason you get to play with these. slay โจ
๐ DESCRIPTION ๐
okay so picture this: the way that openat(2) works, it does like EVERYTHING in one go. these two calls said "not me, we're splitting the main character energy into two parts":
name_to_handle_at()hands you back an opaque handle that corresponds to a specified file. it's giving โจmysteryโจ.open_by_handle_at()takes a handle from a previousname_to_handle_at()call and opens that file, returning an open file descriptor. iconic behavior.
๐ฆ name_to_handle_at() ๐ฆ
name_to_handle_at() returns a file handle AND a mount ID for whatever file the dirfd and path arguments are pointing at. the file handle comes back through the handle argument, which is a pointer to a struct that literally looks like this:
struct file_handle {
unsigned int handle_bytes; /* Size of f_handle [in, out] */
int handle_type; /* Handle type [out] */
unsigned char f_handle[0]; /* File identifier (sized by
caller) [out] */
};
real talk: you are the one who has to allocate this struct, and you have to make it big enough to hold whatever handle comes back in f_handle. so before you call, you initialize the handle_bytes field to the size you allocated for f_handle. (there's a constant, MAX_HANDLE_SZ, defined in <fcntl.h>, that tells you the maximum expected size for a file handle. but heads up โ it is not a guaranteed upper limit, because future filesystems might need more space ๐.) on a successful return, handle_bytes gets updated to hold the number of bytes actually written into f_handle.
you can also be sneaky and discover the required size yourself: just make a call where handle->handle_bytes is zero. in that case the call fails with the error EOVERFLOW, and handle->handle_bytes gets set to the required size โ then you go allocate a struct of the correct size and try again (peep the EXAMPLES below ๐
).
โ ๏ธ okay but pay attention here bestie, some care is needed:EOVERFLOWcan ALSO mean that there's just no file handle available for this particular name, on a filesystem that normally does support file-handle lookup. you can tell that case apart whenEOVERFLOWcomes back withouthandle_bytesbeing increased. subtle. adorable. tricky.
other than that one handle_bytes field, treat the file_handle struct like an opaque data type, periodt. the handle_type and f_handle fields can be used in a later open_by_handle_at() call. and you can also use the opaque file_handle to compare the identity of filesystem objects that were queried at different times and possibly at different paths. the fanotify(7) subsystem can report events with an information record containing a file_handle to identify the filesystem object too. fancy ๐
the flags argument is a bit mask you build by ORing together zero or more of these babies: AT_HANDLE_FID, AT_HANDLE_MNT_ID_UNIQUE, AT_HANDLE_CONNECTABLE, AT_EMPTY_PATH, and AT_SYMLINK_FOLLOW. here's the tea on each:
AT_HANDLE_FID(since Linux 6.5) โ whenflagscontains this, you're telling the kernel that the returnedfile_handleis needed to identify the filesystem object, and NOT for opening the file later. so like, expect that a lateropen_by_handle_at()call with that handle may fail. it's giving "i just wanna know who you are, not take you home" ๐ญAT_HANDLE_MNT_ID_UNIQUE(since Linux 6.12) โ whenflagscontains this, you're saying that the width of themount_idbuffer is at least 64 bits. and then the mount id returned in that buffer is the unique mount id, the same one you'd get fromstatx(2)with theSTATX_MNT_ID_UNIQUEflag. consistency queen ๐AT_HANDLE_CONNECTABLE(since Linux 6.13) โ whenflagscontains this, you're saying the returnedfile_handleis needed to open a file with a known path later. so then: a lateropen_by_handle_at()with that handle may fail if the file was moved. but otherwise, the path of the opened file is expected to be visible from the/proc/pid/fd/*magic link. this flag can NOT be used in combination with the flagsAT_HANDLE_FIDand/orAT_EMPTY_PATH. no mixing, bestie, that's the rule ๐
together, the path and dirfd arguments identify which file you're getting a handle for. and there are four distinct cases, so let's break them down:
- if
pathis a nonempty string containing an absolute pathname, then you get a handle for the file that pathname refers to. in this case,dirfdis literally ignored. bye. - if
pathis a nonempty string containing a relative pathname anddirfdhas the special valueAT_FDCWD, thenpathis interpreted relative to the current working directory of the caller, and you get a handle for the file it refers to. - if
pathis a nonempty string containing a relative pathname anddirfdis a file descriptor referring to a directory, thenpathis interpreted relative to the directory thatdirfdrefers to, and you get a handle for the file it refers to. (seeopenat(2)for an explanation of why "directory file descriptors" are useful, it's a whole thing.) - if
pathis an empty string andflagsspecifies the valueAT_EMPTY_PATH, thendirfdcan be an open file descriptor referring to any type of file, orAT_FDCWD(meaning the current working directory), and you get a handle for the file it refers to. versatile ๐
the mount_id argument returns an identifier for the filesystem mount that corresponds to path. and get this โ it corresponds to the first field in one of the records in /proc/self/mountinfo. if you open the pathname in the fifth field of that record, you get a file descriptor for the mount point, and that fd can be used in a subsequent open_by_handle_at() call. mount_id is returned both for a successful call AND for a call that results in the error EOVERFLOW. giving in every situation, honestly ๐ซถ
by default, name_to_handle_at() does not dereference path if it's a symbolic link, so you get a handle for the link itself. if AT_SYMLINK_FOLLOW is specified in flags, then path is dereferenced if it's a symbolic link (so the call returns a handle for the file the link refers to). pick your fighter โจ
and here's a niche one: name_to_handle_at() does not trigger a mount when the final component of the pathname is an automount point. when a filesystem supports both file handles and automount points, a name_to_handle_at() call on an automount point will return with error EOVERFLOW without having increased handle_bytes. this can happen since Linux 4.13 with NFS when accessing a directory which is on a separate filesystem on the server. in that case, you can trigger the automount by adding a "/" to the end of the pathname. sneaky little slash, doing the most ๐ธ
๐ฅ open_by_handle_at() ๐ฅ
open_by_handle_at() opens the file referred to by handle, which is a file handle returned by a previous call to name_to_handle_at(). the glow-up from token to file descriptor, we love to see it ๐
the mount_fd argument is a file descriptor for any object (file, directory, etc.) in the mounted filesystem with respect to which handle should be interpreted. and you can also pass the special value AT_FDCWD, meaning the current working directory of the caller. flexible ๐
the flags argument is as for open(2). but heads up: if handle refers to a symbolic link, the caller must specify the O_PATH flag, and the symbolic link is not dereferenced. and the O_NOFOLLOW flag, if specified, is ignored. rude but okay.
๐จ and the big one: the caller must have theCAP_DAC_READ_SEARCHcapability to invokeopen_by_handle_at(). no cap. you literally cannot do this without it. check your caps, bestie, before you come crying ๐ญ
๐ RETURN VALUE ๐
on success, name_to_handle_at() returns 0, and open_by_handle_at() returns a file descriptor (a nonnegative integer). zero for the vibes, fd for the wins โจ
in the event of an error, both system calls return -1 and set errno to indicate the error. classic.
๐ ERRORS ๐
name_to_handle_at() and open_by_handle_at() can fail for the same errors as openat(2). and on top of that, they can fail with the errors noted below. so many ways to be humbled, honestly ๐
name_to_handle_at() can fail with the following errors:
EFAULTโpath,mount_id, orhandlepoints outside your accessible address space. bestie you gave it a bad pointer, it's not its fault ๐EINVALโflagsincludes an invalid bit value or an invalid bit combination.EINVALโhandle->handle_bytesis greater thanMAX_HANDLE_SZ.ENOENTโpathis an empty string, butAT_EMPTY_PATHwas not specified inflags. empty path with no permission slip, denied ๐ซENOTDIRโ the file descriptor supplied indirfddoes not refer to a directory, and it is not the case that bothflagsincludesAT_EMPTY_PATHandpathis an empty string.EOPNOTSUPPโ the filesystem does not support decoding of a pathname to a file handle. not every filesystem is built for this, and that's okay ๐ธEOVERFLOWโ thehandle->handle_bytesvalue passed into the call was too small. when this error occurs,handle->handle_bytesis updated to indicate the required size for the handle. this is the "oops my bad, here's the size, try again" error ๐
open_by_handle_at() can fail with the following errors:
EBADFโmount_fdis not an open file descriptor.EBADFโpathis relative butdirfdis neitherAT_FDCWDnor a valid file descriptor.EFAULTโhandlepoints outside your accessible address space.EINVALโhandle->handle_bytesis greater thanMAX_HANDLE_SZor is equal to zero. zero is not a valid flex here ๐ซELOOPโhandlerefers to a symbolic link, butO_PATHwas not specified inflags. shoulda usedO_PATH, bestie ๐ญEPERMโ the caller does not have theCAP_DAC_READ_SEARCHcapability. told you. it's in the description. it was right there.ESTALEโ the specified handle is not valid for opening a file. this error will occur if, for example, the file has been deleted. this error can also occur if the handle was acquired using theAT_HANDLE_FIDflag and the filesystem does not supportopen_by_handle_at(). this error can also occur if the handle was acquired using theAT_HANDLE_CONNECTABLEflag and the file was moved to a different parent. the handle is giving expired-milk energy at this point, let it go ๐ฅ
๐ VERSIONS ๐
FreeBSD has a broadly similar pair of system calls in the form of getfh() and fhopen(). cute cousins across the pond ๐
โญ STANDARDS โญ
Linux. just Linux, bestie. that's the whole entry.
๐ HISTORY ๐
Linux 2.6.39, glibc 2.14. ancient history in tech years but still serving looks โจ
and okay that's NOT even the whole thing, there's literally more of this page coming so don't go anywhere ๐
๐ NOTES ๐
Okay bestie, big energy here โ a file handle can be made in one process with name_to_handle_at() and then used later in a completely different process that calls open_by_handle_at(). Iconic crossover event โจ
Some filesystems literally do not support turning pathnames into file handles โ looking at you /proc, /sys, and a whole bunch of network filesystems ๐ค And there's a second level of betrayal: some filesystems absolutely will translate pathnames to file handles, but then refuse to let you use those handles in open_by_handle_at(). Rude behavior, frankly.
A file handle can go stale (they literally call it "stale", it's giving week-old bread ๐) if a file gets deleted, or for other filesystem-specific reasons. When a handle is invalid, open_by_handle_at() tells you by throwing an ESTALE error. So if you see ESTALE, RIP to that handle ๐ชฆ
These syscalls are built for user-space file servers. Like, imagine a user-space NFS server generates a file handle and hands it to an NFS client. Later, when the client wants to open that file, it can pass the handle right back to the server. This whole setup is what lets a user-space file server operate in a stateless fashion with respect to the files it serves. Main character design decisions only ๐
Now here's a detail you don't wanna sleep on: if path refers to a symbolic link and flags does not specify AT_SYMLINK_FOLLOW, then name_to_handle_at() returns a handle for the link itself (not the file it points to). And the process receiving that handle can later do stuff to the symlink by converting the handle into a file descriptor with open_by_handle_at() using the O_PATH flag, then passing that file descriptor as the dirfd argument in system calls like readlinkat(2) and fchownat(2). Big brain move fr fr ๐ง โจ
๐งญ Obtaining a persistent filesystem ID ๐งญ
Important lore drop: the mount IDs in /proc/self/mountinfo can get reused as filesystems are unmounted and mounted. So the mount ID that name_to_handle_at() hands you back in *mount_id should not be treated as a persistent identifier for the corresponding mounted filesystem. It's giving "this seat is takenโฆ oh wait no it's free again" ๐ญ
But! An application can take the info in the mountinfo record that matches the mount ID and derive a persistent identifier from it. Character development ๐
For example, you can grab the device name in the fifth field of the mountinfo record and use it to search for the corresponding device UUID via the symbolic links in /dev/disks/by-uuid. (A more comfortable way of obtaining the UUID is to use the libblkid(3) library โ way less manual labor, bestie.) Then that whole process can be reversed: use the UUID to look up the device name, then get the corresponding mount point, so you can produce the mount_fd argument that open_by_handle_at() wants. Full circle, chef's kiss ๐
๐ธ EXAMPLES ๐ธ
The two programs below demonstrate the use of name_to_handle_at() and open_by_handle_at(). Let's get into it ๐ซถ
- The first program (
t_name_to_handle_at.c) usesname_to_handle_at()to obtain the file handle and mount ID for the file specified in its command-line argument. The handle and the mount ID get written to standard output. โจ - The second program (
t_open_by_handle_at.c) reads a mount ID and file handle from standard input. Then it usesopen_by_handle_at()to open the file using that handle. If you supply an optional command-line argument, themount_fdargument foropen_by_handle_at()is obtained by opening the directory named in that argument. Otherwise,mount_fdis obtained by scanning/proc/self/mountinfoto find a record whose mount ID matches the mount ID read from standard input, and the mount directory specified in that record gets opened. (Heads up: these programs do not deal with the fact that mount IDs are not persistent โ that's on you, bestie ๐)
The following shell session demonstrates the use of these two programs:
$ echo 'Can you please think about it?' > cecilia.txt;
$ ./t_name_to_handle_at cecilia.txt > fh;
$ ./t_open_by_handle_at < fh;
open_by_handle_at: Operation not permitted
$ sudo ./t_open_by_handle_at < fh; # Need CAP_SYS_ADMIN
Read 31 bytes
$ rm cecilia.txt;
Now we delete and (quickly) re-create the file so that it has the same content and (by chance) the same inode. Nevertheless, open_by_handle_at() recognizes that the original file referred to by the file handle no longer exists. The drama!! ๐
$ stat --printf="%i\n" cecilia.txt; # Display inode number
4072121
$ rm cecilia.txt;
$ echo 'Can you please think about it?' > cecilia.txt;
$ stat --printf="%i\n" cecilia.txt; # Check inode number
4072121
$ sudo ./t_open_by_handle_at < fh;
open_by_handle_at: Stale NFS file handle
๐ Program source: t_name_to_handle_at.c ๐
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char *argv[])
{
int mount_id, fhsize, flags, dirfd;
char *path;
struct file_handle *fhp;
if (argc != 2) {
fprintf(stderr, "Usage: %s path\n", argv[0]);
exit(EXIT_FAILURE);
}
path = argv[1];
/* Allocate file_handle structure. */
fhsize = sizeof(*fhp);
fhp = malloc(fhsize);
if (fhp == NULL)
err(EXIT_FAILURE, "malloc");
/* Make an initial call to name_to_handle_at() to discover
the size required for file handle. */
dirfd = AT_FDCWD; /* For name_to_handle_at() calls */
flags = 0; /* For name_to_handle_at() calls */
fhp->handle_bytes = 0;
if (name_to_handle_at(dirfd, path, fhp, &mount_id, flags) != -1
|| errno != EOVERFLOW)
{
fprintf(stderr, "Unexpected result from name_to_handle_at()\n");
exit(EXIT_FAILURE);
}
/* Reallocate file_handle structure with correct size. */
fhsize = sizeof(*fhp) + fhp->handle_bytes;
fhp = realloc(fhp, fhsize); /* Copies fhp->handle_bytes */
if (fhp == NULL)
err(EXIT_FAILURE, "realloc");
/* Get file handle from pathname supplied on command line. */
if (name_to_handle_at(dirfd, path, fhp, &mount_id, flags) == -1)
err(EXIT_FAILURE, "name_to_handle_at");
/* Write mount ID, file handle size, and file handle to stdout,
for later reuse by t_open_by_handle_at.c. */
printf("%d\n", mount_id);
printf("%u %d ", fhp->handle_bytes, fhp->handle_type);
for (size_t j = 0; j < fhp->handle_bytes; j++)
printf(" %02x", fhp->f_handle[j]);
printf("\n");
exit(EXIT_SUCCESS);
}
๐ Program source: t_open_by_handle_at.c ๐
#define _GNU_SOURCE
#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define streq(...) (strcmp(__VA_ARGS__) == 0)
/* Scan /proc/self/mountinfo to find the line whose mount ID matches
'mount_id'. (An easier way to do this is to install and use the
'libmount' library provided by the 'util-linux' project.)
Open the corresponding mount path and return the resulting file
descriptor. */
static int
open_mount_path_by_id(int mount_id)
{
int mi_mount_id, found;
char mount_path[PATH_MAX];
char *linep;
FILE *fp;
size_t lsize;
ssize_t nread;
fp = fopen("/proc/self/mountinfo", "r");
if (fp == NULL)
err(EXIT_FAILURE, "fopen");
found = 0;
linep = NULL;
while (!found) {
nread = getline(&linep, &lsize, fp);
if (nread == -1)
break;
nread = sscanf(linep, "%d %*d %*s %*s %s",
&mi_mount_id, mount_path);
if (nread != 2) {
fprintf(stderr, "Bad sscanf()\n");
exit(EXIT_FAILURE);
}
if (mi_mount_id == mount_id)
found = 1;
}
free(linep);
fclose(fp);
if (!found) {
fprintf(stderr, "Could not find mount point\n");
exit(EXIT_FAILURE);
}
return open(mount_path, O_RDONLY);
}
int
main(int argc, char *argv[])
{
int mount_id, fd, mount_fd, handle_bytes;
char buf[1000];
#define LINE_SIZE 100
char line1[LINE_SIZE], line2[LINE_SIZE];
char *nextp;
ssize_t nread;
struct file_handle *fhp;
if ((argc > 1 && streq(argv[1], "--help")) || argc > 2) {
fprintf(stderr, "Usage: %s [mount-path]\n", argv[0]);
exit(EXIT_FAILURE);
}
/* Standard input contains mount ID and file handle information:
Line 1: <mount_id>
Line 2: <handle_bytes> <handle_type> <bytes of handle in hex>
*/
if (fgets(line1, sizeof(line1), stdin) == NULL ||
fgets(line2, sizeof(line2), stdin) == NULL)
{
fprintf(stderr, "Missing mount_id / file handle\n");
exit(EXIT_FAILURE);
}
mount_id = atoi(line1);
handle_bytes = strtoul(line2, &nextp, 0);
/* Given handle_bytes, we can now allocate file_handle structure. */
fhp = malloc(sizeof(*fhp) + handle_bytes);
if (fhp == NULL)
err(EXIT_FAILURE, "malloc");
fhp->handle_bytes = handle_bytes;
fhp->handle_type = strtoul(nextp, &nextp, 0);
for (size_t j = 0; j < fhp->handle_bytes; j++)
fhp->f_handle[j] = strtoul(nextp, &nextp, 16);
/* Obtain file descriptor for mount point, either by opening
the pathname specified on the command line, or by scanning
/proc/self/mounts to find a mount that matches the 'mount_id'
that we received from stdin. */
if (argc > 1)
mount_fd = open(argv[1], O_RDONLY);
else
mount_fd = open_mount_path_by_id(mount_id);
if (mount_fd == -1)
err(EXIT_FAILURE, "opening mount fd");
/* Open file using handle and mount point. */
fd = open_by_handle_at(mount_fd, fhp, O_RDONLY);
if (fd == -1)
err(EXIT_FAILURE, "open_by_handle_at");
/* Try reading a few bytes from the file. */
nread = read(fd, buf, sizeof(buf));
if (nread == -1)
err(EXIT_FAILURE, "read");
printf("Read %zd bytes\n", nread);
exit(EXIT_SUCCESS);
}
๐ SEE ALSO ๐
Go read these next, bestie โ they're all part of the same lore:
Also! The libblkid and libmount documentation in the latest util-linux release lives at https://www.kernel.org/pub/linux/utils/util-linux/ โ go get that lore if you want the comfy library versions of this whole saga ๐โจ
And that's a wrap on open_by_handle_at(2) ๐ This page came from Linux man-pages 6.20, dated 2026-02-22 โ so you know it's fresh. Go forth and handle those files, but only with CAP_SYS_ADMIN, okay? Stay hydrated, stay stateless, slay ๐ฆ๐๐
๐ฆ where this page comes from
- arch rolling: man-pages 6.20-1 ยท BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
- nixpkgs unstable: man-pages 6.20
- ๐ the page's license: Linux-man-pages-copyleft
- Copyright 2014, Michael Kerrisk <mtk.manpages@gmail.com>
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms