execve(2) takes a path. The kernel opens it, maps the ELF, hands off control.
Clean. Except there is a gap between the moment your code inspects the file
(stat, read, verify the signature) and the moment the kernel opens it again to
exec. On a writable filesystem, another process can replace the file in that
window. The check passed; what the kernel ran is different.
This is the TOCTOU (time-of-check/time-of-use) race in exec, and it is not
theoretical. Package managers, update daemons, and any service launcher that
verifies a binary before running it all have this window. execveat(2) closes it.
Synopsis
#include <fcntl.h>
#include <unistd.h>
int execveat(int dirfd, const char *pathname,
const char *const argv[], const char *const envp[],
int flags);
No glibc wrapper until 2.34 (2021). On older systems:
#include <sys/syscall.h>
#include <unistd.h>
static inline int sys_execveat(int dirfd, const char *path,
char *const argv[], char *const envp[],
int flags) {
return (int)syscall(SYS_execveat, dirfd, path, argv, envp, flags);
}
AT_EMPTY_PATH: exec what you already opened
The useful flag. When pathname is "" and flags includes AT_EMPTY_PATH,
dirfd is treated as the file to exec, not a directory to resolve from. You
open the binary, you verify it, you exec it, and the kernel never re-opens a
path:
int fd = open("/usr/local/bin/worker", O_PATH);
if (fd < 0) { perror("open"); return -1; }
/* verify: stat, read /proc/self/fd/N for signature check, whatever */
char *argv[] = { "worker", NULL };
char *envp[] = { NULL };
execveat(fd, "", argv, envp, AT_EMPTY_PATH);
/* exec replaces the process; reaching here is an error */
perror("execveat");
The O_PATH flag opens a file descriptor that holds a reference to the inode
without granting read/write access. You can fstat(2) it, pass it to
execveat, and that is it. Cheaper than O_RDONLY when all you need is a
stable handle. Even if someone unlinks or renames the file after your open(2)
call, your fd is still pointing at the original inode and execveat will run it.
That last part is not a bug to suppress, it is the point: the fd is your verified handle. Hold it; trust it; exec from it.
Relative execution
Like openat(2), you can pass a dirfd and a relative path. The kernel resolves
pathname relative to dirfd instead of the process’s cwd:
int dirfd = open("/opt/app/bin", O_DIRECTORY | O_PATH);
execveat(dirfd, "worker", argv, envp, 0);
Not as powerful as the AT_EMPTY_PATH pattern, but it does mean you can pin
execution inside a specific directory tree without trusting $PATH. Pair it
with openat2(2) and RESOLVE_BENEATH on the dirfd if you want the kernel to
refuse to escape the tree.
For more on locking down path resolution, the
openat2(2) tip covers the
RESOLVE_* flags in detail.
AT_SYMLINK_NOFOLLOW
Pass this flag and execveat fails with ELOOP if the last component of
pathname is a symlink. Not a concern with AT_EMPTY_PATH (you already opened
the file), but worth knowing for the dirfd + relative path case.
The memfd angle
execveat + AT_EMPTY_PATH works with any fd that refers to a regular file,
including memfds. You can write an ELF binary into a memfd_create(2) buffer,
seal it, and exec it without it ever touching the filesystem:
int mfd = memfd_create("worker", MFD_CLOEXEC | MFD_ALLOW_SEALING);
/* write ELF bytes into mfd via write(2) */
/* seal it: */
fcntl(mfd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);
execveat(mfd, "", argv, envp, AT_EMPTY_PATH);
This is useful for self-extracting installers and for running a JIT-compiled
helper that never lands on disk. It is also how “fileless malware” works on
Linux, so now you know. If you are writing seccomp policy for a service that
should never exec from memory, you want to filter execveat with
AT_EMPTY_PATH combined with a check that the fd does not point at a memfd.
The sandboxing hub has the seccomp-BPF machinery to
build that filter.
The glibc situation
glibc 2.34 (August 2021) added the wrapper. Anything on glibc 2.33 or older,
which includes Ubuntu 20.04, RHEL 8, and a lot of container base images, needs
the syscall(2) shim above. SYS_execveat is defined in <sys/syscall.h>
even on older glibc versions that lack the wrapper.
When not to bother
If you are just exec-ing something you looked up by name and you are not doing
any pre-exec verification, execve(2) is fine. The fd pattern earns its keep
when you need the “what I checked is what ran” guarantee: service launchers
verifying signatures, container runtimes exec-ing the user binary after
namespace setup, and any code that hands off to a subprocess after privilege
drop. That last case pairs naturally with the clone3(2) + pidfd
tip, which covers the fork side of the
same verified-subprocess pattern.
execveat is Linux 3.19. FreeBSD’s fexecve(2) predates it and gives you the
same AT_EMPTY_PATH semantics via /dev/fd/N, if you care.
