openat2(2)
Open a file with kernel-enforced path-resolution constraints (RESOLVE_* flags).
- Since
- Linux 5.6check your kernel →
- glibc
- none (use syscall(2))
- System
- Linux
- Man page
- openat2(2)
Synopsis
#include <fcntl.h>
#include <linux/openat2.h>
#include <sys/syscall.h>
#include <unistd.h>
long syscall(SYS_openat2, int dirfd, const char *path,
struct open_how *how, size_t size);Rod's take
chroot needs root, leaks through any directory fd you forgot to close, and was never a security boundary. openat2 doesn't fix chroot. It fixes the real bug: open() never let you say "don't resolve outside this tree." RESOLVE_BENEATH turns zip slip into an EXDEV instead of an overwritten file, and RESOLVE_NO_SYMLINKS kills the symlink games inside the tree too. No root, no subprocess, one struct. Unknown struct fields get E2BIG instead of being silently ignored, which is the first time open() has grown a spine in decades. Handle ENOSYS on old kernels and get on with your life.
Flags and arguments that matter
| Name | What it does |
|---|---|
| RESOLVE_BENEATH | Fail if any path component resolves outside dirfd's tree; absolute paths and absolute symlinks are rejected (EXDEV). |
| RESOLVE_IN_ROOT | Treat dirfd as the root for this lookup, like a per-open chroot(2): absolute paths and '..' are clamped to dirfd. |
| RESOLVE_NO_SYMLINKS | Refuse to follow any symlink in any component (ELOOP). Implies RESOLVE_NO_MAGICLINKS. |
| RESOLVE_NO_MAGICLINKS | Refuse procfs-style magic links such as /proc/self/fd/N (ELOOP). |
| RESOLVE_NO_XDEV | Refuse to cross mount points, bind mounts included (EXDEV). |
| RESOLVE_CACHEDLinux 5.12 | Fail with EAGAIN unless every component is already in the lookup cache; retry without it. |
| OPENAT2_REGULARLinux 7.2 | Open only if the target is a regular file; otherwise fail with EFTYPE. |
| struct open_how | Versioned argument struct: flags, mode, resolve. Pass sizeof(struct open_how) as size. |
Errors you will actually hit
| errno | Meaning |
|---|---|
| EXDEV | RESOLVE_BENEATH/RESOLVE_IN_ROOT detected an escape, or RESOLVE_NO_XDEV saw a mount crossing. |
| ELOOP | A symlink (or magic link) was hit with RESOLVE_NO_SYMLINKS or RESOLVE_NO_MAGICLINKS set. |
| E2BIG | how contains an extension this kernel doesn't support (non-zero unknown fields). |
| EAGAIN | RESOLVE_CACHED needed I/O, or a '..' race could not be ruled out under BENEATH/IN_ROOT; retry. |
| EINVAL | Unknown flag or bad value in how, mode set without O_CREAT/O_TMPFILE, or size too small. |
Example
int base = open(root, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
struct open_how how = {
.flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
};
long fd = syscall(SYS_openat2, base, untrusted_path, &how, sizeof(how));
if (fd < 0) {
if (errno == EXDEV || errno == ELOOP)
warnx("rejected: %s escapes or uses symlinks", untrusted_path);
else if (errno == ENOSYS)
warnx("kernel < 5.6: no openat2");
}Trimmed from the full walkthrough in openat2(2): path resolution you can actually trust.
BSD equivalent
FreeBSD: O_RESOLVE_BENEATH flag to openat(2) (FreeBSD 13), which fails with ENOTCAPABLE on escape. OpenBSD: none; closest is unveil(2).
The long version, from the console
Sources
Version numbers are read off the cited man pages, not memory. Spot one that's wrong? Tell Rod.
