exec() Details
Intermediate
3 Programs
7 Questions
Three Ways to Pass Arguments to exec()
When you run a new program via exec(), you need to tell it: (1) what arguments it receives and (2) what environment it sees. Different exec() variants handle this differently. This part covers those details, plus a special secure form: fexecve().
27.2.2 — Passing Arguments as a List (execl, execlp, execle)
When you know at compile time exactly how many arguments you’ll pass, using the list style (execl, execle, execlp) is cleaner — no need to build an array manually.
| Style | Example Call | When to Use |
|---|---|---|
| List (l) | execl("/bin/ls", "ls", "-l", NULL) |
Fixed, known number of args |
| Array (v) | execv("/bin/ls", argv) |
Dynamic args, built at runtime |
Example 1: execle() vs execve() — Same Result, Different Style
Both programs below do the same thing: exec a helper program with two arguments and a custom environment. Compare how different they look.
/* example1_execle_vs_execve.c
* gcc -o ex1 example1_execle_vs_execve.c
* gcc -o show_env show_env.c (reuse from Part 1)
* ./ex1 ./show_env
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char *prog;
char *envVec[] = { "GREET=Namaste", "BYE=Alvida", NULL };
if (argc != 2) {
fprintf(stderr, "Usage: %s <program-path>\n", argv[0]);
exit(1);
}
/* Extract basename from the path */
prog = strrchr(argv[1], '/');
if (prog != NULL)
prog++; /* skip the '/' */
else
prog = argv[1];
/* ---- METHOD A: execve() with array ---- */
/*
char *argVec[] = { prog, "hello", "goodbye", NULL };
execve(argv[1], argVec, envVec);
*/
/* ---- METHOD B: execle() with list (cleaner) ---- */
execle(argv[1], /* pathname */
prog, /* argv[0]: program name */
"hello", /* argv[1] */
"goodbye", /* argv[2] */
(char *)NULL, /* end of arg list */
envVec); /* environment */
fprintf(stderr, "exec failed: %s\n", strerror(errno));
exit(1);
}
argv[0] = show_envargv[1] = helloargv[2] = goodbyeenviron: GREET=Namasteenviron: BYE=Alvida27.2.3 — Passing the Caller’s Environment (execl, execv, execlp, execvp)
When you don’t provide a custom envp, the child inherits the parent’s environment automatically. This means all variables that exist in the parent — like PATH, HOME, USER — are available to the child too.
The exec() functions without an ‘e’ suffix use the global
environ pointer (which points to the current process’s environment). They pass this as the envp to the underlying execve() call.Example 2: Inheriting and Modifying Environment
This example shows how execl() passes the parent’s environment, and how you can modify it first using putenv().
/* example2_env_inherit.c
* gcc -o ex2 example2_env_inherit.c && ./ex2
*
* Uses execl to run printenv — which prints USER and SHELL.
* We modify USER before exec so the child sees the new value.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(void)
{
const char *original_user;
original_user = getenv("USER");
printf("Original USER: %s\n", original_user ? original_user : "(not set)");
/* Modify environment BEFORE exec — child inherits this change */
if (putenv("USER=embedded_dev") != 0) {
perror("putenv");
exit(1);
}
printf("Modified USER to: embedded_dev\n");
printf("Now exec'ing printenv...\n\n");
/* execl: uses caller's environ (which now has USER=embedded_dev) */
execl("/usr/bin/printenv",
"printenv",
"USER", /* only print USER variable */
"HOME", /* and HOME variable */
(char *)NULL);
fprintf(stderr, "execl failed: %s\n", strerror(errno));
exit(1);
}
/* Expected output:
* Original USER: ravi
* Modified USER to: embedded_dev
* Now exec'ing printenv...
*
* embedded_dev <-- child sees the modified value
* /home/ravi <-- HOME inherited from parent unchanged
*/
27.2.4 — fexecve(): Execute by File Descriptor
fexecve() is like execve() but instead of giving a pathname, you give an open file descriptor. Why would you want this?
| Problem with execve() | How fexecve() Solves It |
|---|---|
| You open a file, verify its checksum, then exec it by name | The file could be swapped between your check and exec! |
| (Time-of-check vs time-of-use race condition) | fexecve() holds the fd open — exec happens on the same file you verified |
#define _GNU_SOURCE
#include <unistd.h>
int fexecve(int fd, char *const argv[], char *const envp[]);
/* Returns: never on success, -1 on error */
Example 3: fexecve() — Secure Exec by File Descriptor
/* example3_fexecve.c
* gcc -D_GNU_SOURCE -o ex3 example3_fexecve.c && ./ex3
*
* Secure pattern:
* 1. Open file → get fd
* 2. Verify the file (checksum, permissions, etc.)
* 3. Execute using fd — no TOCTOU race!
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
/* Simulate a checksum check — in real code use SHA256 etc. */
int verify_executable(int fd)
{
struct stat sb;
if (fstat(fd, &sb) == -1) return 0;
/* Check: must be a regular file with execute permission */
if (!S_ISREG(sb.st_mode)) {
printf("VERIFY FAIL: not a regular file\n");
return 0;
}
if (!(sb.st_mode & S_IXUSR)) {
printf("VERIFY FAIL: no execute permission\n");
return 0;
}
printf("VERIFY OK: regular file, size=%ld bytes, executable\n",
(long)sb.st_size);
return 1;
}
int main(void)
{
int fd;
char *argv[] = { "date", NULL };
char *envp[] = { "TZ=Asia/Kolkata", NULL };
/* Step 1: Open the program file */
fd = open("/bin/date", O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}
printf("Opened /bin/date: fd=%d\n", fd);
/* Step 2: Verify it is what we expect */
if (!verify_executable(fd)) {
fprintf(stderr, "Security check failed!\n");
close(fd);
exit(1);
}
/* Step 3: Execute using fd — SAME file we just verified */
printf("Executing via fexecve...\n\n");
fexecve(fd, argv, envp);
/* Only reached on error */
fprintf(stderr, "fexecve failed: %s\n", strerror(errno));
close(fd);
return 1;
}
/* Expected output:
* Opened /bin/date: fd=3
* VERIFY OK: regular file, size=XXXX bytes, executable
* Executing via fexecve...
*
* Thu Jun 5 10:45:00 IST 2025
*/
/proc/self/fd/N to translate the fd back to a path for execve(). If /proc is not mounted, fexecve() will fail with ENOSYS.❓ Interview Questions — Args, Env & fexecve()
Answer: When the number of arguments is fixed and known at compile time. execle() lets you write them as a comma-separated list in the function call — cleaner code. execve() requires building a char* array manually. Both accept a custom envp[].
Answer: They internally pass the global
environ variable (pointer to the current environment array) as the envp argument to execve(). Any changes made via putenv() or setenv() before exec() will be visible to the child.Answer: A race condition where you check a file’s properties, then use the file — but between check and use, an attacker replaces the file. fexecve() prevents this by holding an open file descriptor, ensuring you exec the exact same file you verified.
Answer: Linux’s fexecve() is implemented in glibc using /proc/self/fd/N — it opens the symlink /proc/self/fd/<fd> to get the path, then calls execve(). Without /proc mounted, this path doesn’t exist and fexecve() fails with ENOSYS.
Answer: Use execve() or execle() and pass an empty environment:
char *empty_env[] = { NULL };. The ‘e’ variants are the only ones that let you supply a custom envp, including an empty one.Answer: putenv() modifies the process’s own environment. In a multi-threaded process, another thread might read PATH between your putenv() call and the execlp() call. Also, putenv() with a stack-allocated string is dangerous — if the stack frame exits, the string is gone but environ still points to it (use heap-allocated strings).
Answer: This is undefined behavior — passing NULL instead of a valid array pointer. The correct way to pass an empty environment is to pass a pointer to an array containing only NULL:
char *e[] = {NULL}; execve(path, argv, e);Next: Interpreter Scripts (#! shebang)
Learn how Linux handles Python, Bash, and other scripts via exec()
