openat2 Path Confinement Still Needs a Trusted Directory Descriptor
A service that accepts a user-supplied filename has two problems to solve. It must keep the resolved path inside an intended directory, and it must avoid a race in which an attacker swaps a path component between validation and use. Linux openat2() gives developers useful tools for both problems, but the call is not a complete authorization system.
The first design decision is the directory reference. openat2() resolves a path relative to a directory file descriptor supplied by the caller. Opening that descriptor once, from a trusted base, gives the process a stable reference to the intended tree. Resolving the same pathname from the process working directory is weaker because the working directory can be changed or selected by surrounding code.
The open_how structure controls resolution. RESOLVE_BENEATH rejects pathnames that escape the directory tree or reach an absolute path. RESOLVE_IN_ROOT treats the supplied directory as a temporary root and resolves absolute paths as if they started there. The two modes express different policies. A program should choose the one that matches its input contract and then test the behavior with .., absolute paths, symbolic links and magic-link cases.
struct open_how how = { .flags = O_RDONLY | O_CLOEXEC, .resolve = RESOLVE_BENEATH, }; int fd = syscall(SYS_openat2, base_fd, user_path, &how, sizeof(how)); The descriptor flags deserve equal attention. O_CLOEXEC prevents the descriptor from leaking into an unrelated child process. The returned descriptor should be the object the program later reads, examines or passes to a worker. Reopening the pathname after the call creates a new resolution problem.
openat2() does not expand the caller's file permissions. Standard discretionary access checks still apply, and a directory descriptor does not make a protected file readable. It also does not prove that content is safe. A file can be inside the allowed tree and still contain hostile data. Path confinement and content validation are separate controls.
The error path matters. EXDEV, ELOOP and EAGAIN can indicate a blocked escape, a symbolic-link conflict or a race that the kernel could not resolve. Treating all failures as "not found" hides policy problems from operators. Logging the error class helps distinguish malicious input from a broken deployment, while the log should not echo an attacker-controlled path without encoding it safely.
For privilege separation, combine a stable base descriptor with least-privilege file access and a minimal syscall surface. Tests should cover symlinks that point outside the tree, renamed directories, bind mounts and concurrent replacement attempts. Those tests exercise the boundary the application claims to enforce.
The practical rule is simple. Keep a trusted directory descriptor, choose one explicit resolution policy, use the returned descriptor, and test the error cases. openat2() reduces a class of pathname races. It does not replace access control, sandboxing or a careful review of what the opened file is allowed to contain.

