$RodHat_
Syscall reference · Filesystem

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

NameWhat it does
RESOLVE_BENEATHFail if any path component resolves outside dirfd's tree; absolute paths and absolute symlinks are rejected (EXDEV).
RESOLVE_IN_ROOTTreat dirfd as the root for this lookup, like a per-open chroot(2): absolute paths and '..' are clamped to dirfd.
RESOLVE_NO_SYMLINKSRefuse to follow any symlink in any component (ELOOP). Implies RESOLVE_NO_MAGICLINKS.
RESOLVE_NO_MAGICLINKSRefuse procfs-style magic links such as /proc/self/fd/N (ELOOP).
RESOLVE_NO_XDEVRefuse to cross mount points, bind mounts included (EXDEV).
RESOLVE_CACHEDLinux 5.12Fail with EAGAIN unless every component is already in the lookup cache; retry without it.
OPENAT2_REGULARLinux 7.2Open only if the target is a regular file; otherwise fail with EFTYPE.
struct open_howVersioned argument struct: flags, mode, resolve. Pass sizeof(struct open_how) as size.

Errors you will actually hit

errnoMeaning
EXDEVRESOLVE_BENEATH/RESOLVE_IN_ROOT detected an escape, or RESOLVE_NO_XDEV saw a mount crossing.
ELOOPA symlink (or magic link) was hit with RESOLVE_NO_SYMLINKS or RESOLVE_NO_MAGICLINKS set.
E2BIGhow contains an extension this kernel doesn't support (non-zero unknown fields).
EAGAINRESOLVE_CACHED needed I/O, or a '..' race could not be ruled out under BENEATH/IN_ROOT; retry.
EINVALUnknown 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

  1. openat2(2), man7.org
  2. open(2), FreeBSD
  3. unveil(2), OpenBSD

Version numbers are read off the cited man pages, not memory. Spot one that's wrong? Tell Rod.