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 previous name_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: EOVERFLOW can 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 when EOVERFLOW comes back without handle_bytes being 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) โ€” when flags contains this, you're telling the kernel that the returned file_handle is needed to identify the filesystem object, and NOT for opening the file later. so like, expect that a later open_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) โ€” when flags contains this, you're saying that the width of the mount_id buffer 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 from statx(2) with the STATX_MNT_ID_UNIQUE flag. consistency queen ๐Ÿ‘‘
  • AT_HANDLE_CONNECTABLE (since Linux 6.13) โ€” when flags contains this, you're saying the returned file_handle is needed to open a file with a known path later. so then: a later open_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 flags AT_HANDLE_FID and/or AT_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 path is a nonempty string containing an absolute pathname, then you get a handle for the file that pathname refers to. in this case, dirfd is literally ignored. bye.
  • if path is a nonempty string containing a relative pathname and dirfd has the special value AT_FDCWD, then path is interpreted relative to the current working directory of the caller, and you get a handle for the file it refers to.
  • if path is a nonempty string containing a relative pathname and dirfd is a file descriptor referring to a directory, then path is interpreted relative to the directory that dirfd refers to, and you get a handle for the file it refers to. (see openat(2) for an explanation of why "directory file descriptors" are useful, it's a whole thing.)
  • if path is an empty string and flags specifies the value AT_EMPTY_PATH, then dirfd can be an open file descriptor referring to any type of file, or AT_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 the CAP_DAC_READ_SEARCH capability to invoke open_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, or handle points outside your accessible address space. bestie you gave it a bad pointer, it's not its fault ๐Ÿ’€
  • EINVAL โ€” flags includes an invalid bit value or an invalid bit combination.
  • EINVAL โ€” handle->handle_bytes is greater than MAX_HANDLE_SZ.
  • ENOENT โ€” path is an empty string, but AT_EMPTY_PATH was not specified in flags. empty path with no permission slip, denied ๐Ÿšซ
  • ENOTDIR โ€” the file descriptor supplied in dirfd does not refer to a directory, and it is not the case that both flags includes AT_EMPTY_PATH and path is 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 โ€” the handle->handle_bytes value passed into the call was too small. when this error occurs, handle->handle_bytes is 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_fd is not an open file descriptor.
  • EBADF โ€” path is relative but dirfd is neither AT_FDCWD nor a valid file descriptor.
  • EFAULT โ€” handle points outside your accessible address space.
  • EINVAL โ€” handle->handle_bytes is greater than MAX_HANDLE_SZ or is equal to zero. zero is not a valid flex here ๐Ÿซ 
  • ELOOP โ€” handle refers to a symbolic link, but O_PATH was not specified in flags. shoulda used O_PATH, bestie ๐Ÿ˜ญ
  • EPERM โ€” the caller does not have the CAP_DAC_READ_SEARCH capability. 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 the AT_HANDLE_FID flag and the filesystem does not support open_by_handle_at(). this error can also occur if the handle was acquired using the AT_HANDLE_CONNECTABLE flag 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) uses name_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 uses open_by_handle_at() to open the file using that handle. If you supply an optional command-line argument, the mount_fd argument for open_by_handle_at() is obtained by opening the directory named in that argument. Otherwise, mount_fd is obtained by scanning /proc/self/mountinfo to 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
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms