$RodHat_
Console Tips

execveat(2): execute a binary you already have open

Published by · Updated

This article may contain Amazon affiliate links. I may earn a commission if you buy through them. Disclosure.

Photo by Christina Morillo
Photo: Christina Morillo / Pexels
On this page · 7 sections
  1. Synopsis
  2. AT_EMPTY_PATH: exec what you already opened
  3. Relative execution
  4. AT_SYMLINK_NOFOLLOW
  5. The memfd angle
  6. The glibc situation
  7. When not to bother

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.

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.

Sources

  1. execveat(2) Linux man page (man7.org)
  2. execveat — execute program relative to a file descriptor (LWN) (LWN.net)