Chapter 27.2.2–27.2.4 — Args, Environment & fexecve()

Chapter 27.2.2–27.2.4 — Args, Environment & fexecve()
Passing Arguments as Lists · Inheriting Environment · Exec by File Descriptor · EmbeddedPathashala
📌 Topic
exec() Details
🧠 Level
Intermediate
💻 Examples
3 Programs
❓ Q&A
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);
}
Output (when run as ./ex1 ./show_env):
argv[0] = show_env
argv[1] = hello
argv[2] = goodbye
environ: GREET=Namaste
environ: BYE=Alvida

27.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.

How it works internally:
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
 */
⚠️ fexecve() Note: Available in glibc 2.3.2+. Internally on Linux, it uses /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()

Q1. When would you choose execle() over execve()?
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[].
Q2. How does a child inherit the parent’s environment in exec functions without ‘e’?
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.
Q3. What is a TOCTOU (Time-Of-Check Time-Of-Use) vulnerability?
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.
Q4. Why does fexecve() need /proc to be mounted on Linux?
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.
Q5. If you want to run a command with a completely clean environment (no parent variables), which exec do you use?
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.
Q6. What is the security risk of calling putenv() to set PATH before using execlp()?
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).
Q7. What happens if you pass NULL as envp to execve()?
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()

→ Part 4: Interpreter Scripts 🏠 All Tutorials

Leave a Reply

Your email address will not be published. Required fields are marked *