openat2(2)

๐Ÿ’… openat2(2) โ€” System Calls Manual ๐Ÿ’…

okay bestie, welcome to openat2, the extended little sibling of openat(2) that said "let me be โœจextraโœจ". this is the openat2(2) manual page and it's long so buckle up ๐ŸŽ€


โœจ NAME โœจ

openat2 โ€” open and possibly create a file (extended) ๐Ÿ’–


๐Ÿ“š LIBRARY ๐Ÿ“š

Standard C library (libc, -lc)


๐ŸŽ€ SYNOPSIS ๐ŸŽ€

#include <fcntl.h>          /* Definition of O_* and S_* constants */
#include <linux/openat2.h>  /* Definition of RESOLVE_* constants */
#include <sys/syscall.h>    /* Definition of SYS_* constants */
#include <unistd.h>

long syscall(SYS_openat2, int dirfd, const char *path,
             struct open_how *how, size_t size);
heads up, bestie: glibc provides no wrapper for openat2(), so you literally have to go through syscall(2). no cap. ๐Ÿซถ

๐Ÿ’– DESCRIPTION ๐Ÿ’–

so openat2() is the glow-up of openat(2) โ€” it's an extension of it and gives you a superset of its functionality. it's giving main character energy fr fr.

openat2() opens the file specified by path. and if that file doesn't exist? it may optionally be created โ€” but only if O_CREAT is specified in how.flags. (:sparkles:)

just like with openat(2): if path is relative, it gets interpreted relative to the directory that the file descriptor dirfd refers to โ€” or, if dirfd is the special value AT_FDCWD, relative to the calling process's current working directory. and if path is absolute, then dirfd is ignoredโ€ฆ unless how.resolve contains RESOLVE_IN_ROOT, in which case path gets resolved relative to dirfd instead. iykyk ๐Ÿ˜Œ

and here's the slay part: instead of taking a single flags argument, it takes an extensible structure (how) so it can grow new features later. the size argument must be specified as sizeof(struct open_how). no substitutions, bestie.


๐ŸŒธ The open_how structure ๐ŸŒธ

the how argument is the moment. it specifies how path should be opened, and it acts as a superset of the flags and mode arguments to openat(2). it's a pointer to an open_how structure, which is described over in open_how(2type). ๐Ÿ‘‘

any future extensions to openat2() will show up as new fields appended to the open_how structure, and a zero value in a new field means the kernel behaves as though that extension field wasn't even there. so therefore โ€” and this is important, girlies โ€” the caller MUST zero-fill this structure on initialization. (there's more about why this is necessary in the "Extensibility" section of the NOTES. check it later!)

the fields of the open_how structure are, in order:

๐Ÿฆ‹ flags

this field specifies the file creation and file status flags to use when opening the file. all of the O_* flags defined for openat(2) are valid openat2() flag values. โœ…

now here's where it gets spicy ๐ŸŒถ๏ธ โ€” whereas openat(2) ignores unknown bits in its flags argument, openat2() returns an error if unknown or conflicting flags are specified in how.flags. it does not let things slide.

openat2() also supports this additional flag:

  • OPENAT2_REGULAR (since Linux 7.2) โ€” open the file specified by path only if it is a regular file. ๐Ÿ’…

๐Ÿ“ mode

this field specifies the mode for the new file, with identical semantics to the mode argument of openat(2).

but again, the glow-up: whereas openat(2) ignores bits other than those in the range 07777 in its mode argument, openat2() returns an error if how.mode contains bits other than 07777. and similarly, an error is returned if openat2() is called with a nonzero how.mode and how.flags does not contain O_CREAT or O_TMPFILE. so don't be messy with your bits. ๐Ÿ™ˆ

๐Ÿ’œ resolve

this one is the moment. it's a bit-mask of flags that modify the way all components of path will be resolved. (for background, go peek at path_resolution(7).)

the primary use case for these flags is letting trusted programs restrict how untrusted paths (or paths inside untrusted directories) are resolved. ๐Ÿ” the full list of resolve flags is:

  • RESOLVE_BENEATH โ€” do not permit the path resolution to succeed if any component of the resolution is not a descendant of the directory indicated by dirfd. this causes absolute symbolic links (and absolute values of path) to be rejected. ๐Ÿšซ

currently, this flag also disables magic-link resolution (see below). however, this may change in the future. therefore, to ensure that magic links are not resolved, the caller should explicitly specify RESOLVE_NO_MAGICLINKS. (future-proofing, bestie, it's a vibe ๐Ÿ’…)

  • RESOLVE_IN_ROOT โ€” treat the directory referred to by dirfd as the root directory while resolving path. absolute symbolic links get interpreted relative to dirfd. if a prefix component of path equates to dirfd, then an immediately following .. component likewise equates to dirfd (just as /.. is traditionally equivalent to /). and if path is absolute, it is also interpreted relative to dirfd.

the effect of this flag is as though the calling process had used chroot(2) to (temporarily) modify its root directory โ€” specifically to the directory referred to by dirfd. but โ€” and this is the key difference โ€” unlike chroot(2) (which changes the filesystem root permanently for a process), RESOLVE_IN_ROOT lets a program efficiently restrict path resolution on a per-open basis. iconic behavior honestly ๐Ÿ‘‘

currently, this flag also disables magic-link resolution. however, this may change in the future. therefore, to ensure that magic links are not resolved, the caller should explicitly specify RESOLVE_NO_MAGICLINKS. (yes this is the same warning as above, it's important, don't skip it ๐ŸŒธ)

  • RESOLVE_NO_MAGICLINKS โ€” disallow all magic-link resolution during path resolution.

magic links are symbolic-link-like objects most notably found in proc(5); examples include /proc/pid/exe and /proc/pid/fd/*. (see symlink(7) for more deets.)

unknowingly opening magic links can be risky for some applications. examples of such risks include:

  • if the process opening a pathname is a controlling process that currently has no controlling terminal (see credentials(7)), then opening a magic link inside /proc/pid/fd that happens to refer to a terminal would cause the process to acquire a controlling terminal. ๐Ÿ˜ณ
  • in a containerized environment, a magic link inside /proc may refer to an object outside the container, and thus may provide a means to escape from the container. ๐Ÿ’€

because of such risks, an application may prefer to disable magic link resolution using the RESOLVE_NO_MAGICLINKS flag. smart girl behavior.

and one more thing: if the trailing component (i.e., basename) of path is a magic link, how.resolve contains RESOLVE_NO_MAGICLINKS, and how.flags contains both O_PATH and O_NOFOLLOW, then an O_PATH file descriptor referencing the magic link will be returned. โœจ

  • RESOLVE_NO_SYMLINKS โ€” disallow resolution of symbolic links during path resolution. this option implies RESOLVE_NO_MAGICLINKS. two birds, one flag ๐Ÿฆ‹

if the trailing component (i.e., basename) of path is a symbolic link, how.resolve contains RESOLVE_NO_SYMLINKS, and how.flags contains both O_PATH and O_NOFOLLOW, then an O_PATH file descriptor referencing the symbolic link will be returned.

note that the effect of the RESOLVE_NO_SYMLINKS flag โ€” which affects the treatment of symbolic links in all of the components of path โ€” differs from the effect of the O_NOFOLLOW file creation flag (in how.flags), which affects the handling of symbolic links only in the final component of path. yes, they are different, do not confuse them, bestie. ๐Ÿ™ˆ

applications that employ the RESOLVE_NO_SYMLINKS flag are encouraged to make its use configurable (unless it's used for a specific security purpose), because symbolic links are very widely used by end-users. setting this flag indiscriminately โ€” i.e., for purposes not specifically related to security โ€” for all uses of openat2() may result in spurious errors on previously functional systems. ๐Ÿ˜ญ this may occur if, for example, a system pathname that is used by an application is modified (e.g., in a new distribution release) so that a pathname component (now) contains a symbolic link. so likeโ€ฆ don't just slap it everywhere, okay?

  • RESOLVE_NO_XDEV โ€” disallow traversal of mount points during path resolution (including all bind mounts). consequently, path must either be on the same mount as the directory referred to by dirfd, or on the same mount as the current working directory if dirfd is specified as AT_FDCWD.

applications that employ the RESOLVE_NO_XDEV flag are encouraged to make its use configurable (unless it's used for a specific security purpose), as bind mounts are widely used by end-users. setting this flag indiscriminately โ€” i.e., for purposes not specifically related to security โ€” for all uses of openat2() may result in spurious errors on previously functional systems. ๐Ÿ˜ญ this may occur if, for example, a system pathname that is used by an application is modified (e.g., in a new distribution release) so that a pathname component (now) contains a bind mount.

  • RESOLVE_CACHED (since Linux 5.12) โ€” make the open operation fail unless all path components are already present in the kernel's lookup cache. if any kind of revalidation or I/O is needed to satisfy the lookup, openat2() fails with the error EAGAIN. ๐Ÿ”ฅ this is useful in providing a fast-path open that can be performed without resorting to thread offload, or other mechanisms that an application might use to offload slower operations. speed, bestie ๐Ÿ’จ

and finally, the plot twist: if any bits other than those listed above are set in how.resolve, an error is returned. ๐Ÿšจ


๐Ÿ”ฅ RETURN VALUE ๐Ÿ”ฅ

on success, a new file descriptor is returned. ๐ŸŽ‰

on error, -1 is returned, and errno is set to indicate the error. ๐Ÿ˜ญ


โ€ฆand that's just part 1, bestie, there's more coming ๐ŸŒˆ

๐Ÿ’ฅ ERRORS ๐Ÿ’ฅ

okay bestie, so openat2() throws everything that its little sibling openat(2) throws โ€” no cap โ€” plus these extra ones below. take notes, this is the good part ๐Ÿ“โœจ

  • E2BIG โ€” someone passed an extension in how that this kernel straight up does not support. (peep the Extensibility section down in NOTES for the whole saga of how extensions get handled ๐Ÿ‘€)
  • EAGAIN โ€” how.resolve has either RESOLVE_IN_ROOT or RESOLVE_BENEATH set, and the kernel could not guarantee that a ".." component didn't just escape (either a race condition happened or somebody's being shady, lowkey an attack). the caller gets to pick: retry the openat2() call, it's giving second-chance energy ๐Ÿ’…
  • EAGAIN โ€” RESOLVE_CACHED was set, but nope, this open operation cannot be done using only cached info. caller should retry with RESOLVE_CACHED not set in how.resolve. rip ๐Ÿ™ˆ
  • EINVAL โ€” an unknown flag or an invalid value got dropped into how.
  • EINVAL โ€” mode is nonzero, but how.flags doesn't contain O_CREAT or O_TMPFILE. the audacity ๐Ÿ’€
  • EINVAL โ€” size was smaller than any known version of struct open_how.
  • ELOOP โ€” how.resolve contains RESOLVE_NO_SYMLINKS, and one of the path components was a symbolic link (or a magic link). scandalous โœจ
  • ELOOP โ€” how.resolve contains RESOLVE_NO_MAGICLINKS, and one of the path components was a magic link. double scandal ๐Ÿฆ‹
  • EXDEV โ€” how.resolve contains either RESOLVE_IN_ROOT or RESOLVE_BENEATH, and an escape from the root during path resolution was detected. caught in 4k ๐Ÿ“ธ
  • EXDEV โ€” how.resolve contains RESOLVE_NO_XDEV, and a path component crosses a mount point.
  • EFTYPE โ€” how.flags contains the OPENAT2_REGULAR flag and the path is not a regular file. bestie it's simply not that girl ๐ŸŽ€

๐Ÿ“œ STANDARDS ๐Ÿ“œ

Linux. that's it. that's the standard. periodt ๐Ÿ‘‘


๐Ÿ•ฐ๏ธ HISTORY ๐Ÿ•ฐ๏ธ

dropped in Linux 5.6 โ€” vintage, iconic, we love her ๐Ÿ’–

the semantics of RESOLVE_BENEATH were modeled after FreeBSD's O_BENEATH, but โ€” and this is the tea โ˜• โ€” Linux avoided a well-known correctness bug in FreeBSD's implementation that made it effectively insecure. later, FreeBSD 13 introduced O_RESOLVE_BENEATH to replace the insecure O_BENEATH. FreeBSD's O_RESOLVE_BENEATH semantics are based on Linux's RESOLVE_BENEATH and the two are now functionally equivalent. the glow-up is real ๐Ÿฆ‹โœจ


๐Ÿ“ NOTES ๐Ÿ“

๐Ÿงฉ Extensibility ๐Ÿงฉ

so, to allow for future extensibility and keep things cute, openat2() makes the user-space application specify the size of the open_how structure it's passing in. by handing over that info, openat2() gets both forwards- and backwards-compatibility, with size acting like an implicit version number. (because new extension fields always get appended, the structure size will always increase โ€” it only goes up, it's giving growth mindset ๐Ÿชด). this extensibility design is very similar to other system calls like sched_setattr(2), perf_event_open(2), and clone3(2).

okay so if we let usize be the size of the structure as specified by the user-space application, and ksize be the size of the structure that the kernel supports, there are like three cases to think about:

  • if ksize equals usize, then there's no version mismatch and how can be used verbatim. slay ๐Ÿ’…
  • if ksize is larger than usize, then there are some extension fields the kernel supports that the user-space application doesn't even know about. because a zero value in any added extension field means no-op, the kernel treats every extension field the user-space app didn't provide as having zero values. this provides backwards-compatibility.
  • if ksize is smaller than usize, then there are some extension fields the user-space application is aware of but the kernel does not support. because any extension field must have its zero values mean no-op, the kernel can safely ignore the unsupported extension fields if they're all-zero. but if any unsupported extension field is nonzero โ€” boom ๐Ÿ’ฅ โ€” -1 is returned and errno is set to E2BIG. this provides forwards-compatibility.

because the definition of struct open_how may change in the future (new fields getting added whenever system headers are updated), user-space applications should zero-fill struct open_how to make sure that recompiling the program with new headers won't result in spurious errors at run time. ๐Ÿ˜ญ the simplest way is a designated initializer:

struct open_how how = { .flags = O_RDWR,
                        .resolve = RESOLVE_IN_ROOT };

or you can do it explicitly with memset(3) or similar:

struct open_how how;
memset(&how, 0, sizeof(how));
how.flags = O_RDWR;
how.resolve = RESOLVE_IN_ROOT;

and if a user-space application wants to find out which extensions the running kernel supports? so easy bestie โ€” do a binary search on size with a structure that has every byte nonzero (the goal is finding the largest value that doesn't produce an error of E2BIG). detective work, but make it โœจalgorithmicโœจ ๐Ÿ”


๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

openat(2), open_how(2type), path_resolution(7), symlink(7)

go read those, they're all bangers, iykyk ๐Ÿซถ


and that's a wrap on openat2(2) bestie!! this page came to you from Linux man-pages 6.20, dated 2026-07-02 โ€” go forth and resolve some paths safely, you absolute main character ๐Ÿ‘‘๐ŸŒˆ๐Ÿ’œ

๐Ÿ“ฆ 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