Chapter 27.4 — File Descriptors and exec()

Chapter 27.4 — File Descriptors and exec()
FD Inheritance · FD_CLOEXEC · I/O Redirection · Shell Internals · EmbeddedPathashala
📌 Topic
FDs & exec()
🧠 Level
Intermediate
💻 Examples
3 Programs
❓ Q&A
8 Questions

File Descriptors Survive exec() — By Default

When a process calls exec(), the code and memory are replaced — but open file descriptors are inherited by the new program by default. This is by design — it’s how the shell implements I/O redirection (the > and < operators).

Sometimes you don’t want this — for example, when exec’ing an untrusted third-party program, you don’t want it inheriting your open files. The FD_CLOEXEC flag solves this.

Default FD Behavior Across exec()

File Descriptor Default after exec() With FD_CLOEXEC
stdin (fd 0) ✅ Open and inherited Closed on exec
stdout (fd 1) ✅ Open and inherited Closed on exec
stderr (fd 2) ✅ Open and inherited Closed on exec
Any file you opened (fd 3,4,…) ✅ Open and inherited Closed on exec

How Shell I/O Redirection Works (ls /tmp > dir.txt)

Step Shell Action Why
1 fork() — create child shell Parent shell must keep running
2 Child: open(“dir.txt”, O_WRONLY|O_CREAT…) → fd = 3 Open the output file
3 Child: dup2(3, STDOUT_FILENO) — copy fd 3 to fd 1 Now stdout (fd 1) points to dir.txt
4 Child: close(3) — no longer need extra fd Clean up
5 Child: execve(“/bin/ls”, …) — replace with ls fd 1 (→ dir.txt) survives exec!
6 ls writes to stdout (fd 1) — goes to dir.txt Redirection complete

FD_CLOEXEC — The Close-On-Exec Flag

When you set the FD_CLOEXEC flag on a file descriptor, it gets automatically closed when exec() succeeds. If exec() fails, it stays open.

#include <fcntl.h>

/* Method: use fcntl to set FD_CLOEXEC */
int flags = fcntl(fd, F_GETFD);           /* Get current flags */
flags |= FD_CLOEXEC;                       /* Add close-on-exec bit */
fcntl(fd, F_SETFD, flags);                /* Set updated flags */

/* Modern shortcut: set it at open time (Linux 2.6.23+) */
int fd = open("file.txt", O_RDONLY | O_CLOEXEC);
Important: When you use dup(), dup2(), or fcntl() to duplicate a file descriptor, the close-on-exec flag is always cleared for the duplicate. This is a POSIX requirement — the duplicate starts with FD_CLOEXEC off.

Example 1: Proving FDs Survive exec()

/* example1_fd_inherit.c
 * gcc -o ex1 example1_fd_inherit.c && ./ex1
 *
 * Write to a file in parent via fd, then exec cat which reads it.
 * Shows that the fd number persists across exec.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

int main(void)
{
    int fd;
    pid_t child;
    int status;

    /* Create a temp file */
    fd = open("/tmp/test_fd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); exit(1); }

    write(fd, "Hello from parent!\n", 19);
    write(fd, "FDs survive exec()\n", 19);
    close(fd);

    /* Now open file for reading */
    fd = open("/tmp/test_fd.txt", O_RDONLY);
    if (fd == -1) { perror("open for read"); exit(1); }

    printf("Parent: opened file on fd %d\n", fd);
    printf("Now fork+exec'ing 'cat' — it will inherit fd %d\n\n", fd);

    child = fork();
    if (child == -1) { perror("fork"); exit(1); }

    if (child == 0) {
        /* Redirect fd to stdin, then exec cat */
        if (dup2(fd, STDIN_FILENO) == -1) { perror("dup2"); exit(1); }
        close(fd);  /* original fd no longer needed */

        /* cat reads from stdin — which is now our file */
        execlp("cat", "cat", (char *)NULL);
        perror("execlp");
        exit(1);
    }

    waitpid(child, &status, 0);
    close(fd);
    printf("\nParent: cat finished with status %d\n", WEXITSTATUS(status));
    return 0;
}

/* Expected output:
 * Parent: opened file on fd 3
 * Now fork+exec'ing 'cat' — it will inherit fd 3
 *
 * Hello from parent!
 * FDs survive exec()
 *
 * Parent: cat finished with status 0
 */

Example 2: FD_CLOEXEC — Preventing FD Inheritance

/* example2_fd_cloexec.c
 * gcc -o ex2 example2_fd_cloexec.c && ./ex2
 *
 * Shows: fd WITHOUT FD_CLOEXEC → stays open after exec
 *        fd WITH FD_CLOEXEC    → auto-closed on exec
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

void show_fd_status(int fd, const char *label)
{
    int flags = fcntl(fd, F_GETFD);
    if (flags == -1) {
        printf("%s: fd %d — CLOSED (or invalid)\n", label, fd);
        return;
    }
    printf("%s: fd %d — OPEN, FD_CLOEXEC=%s\n",
           label, fd,
           (flags & FD_CLOEXEC) ? "SET" : "NOT SET");
}

int main(void)
{
    int fd_normal, fd_cloexec, flags;
    pid_t child;
    int status;

    /* Open two files: one normal, one with close-on-exec */
    fd_normal = open("/tmp/normal.txt",
                     O_WRONLY | O_CREAT | O_TRUNC, 0644);
    fd_cloexec = open("/tmp/cloexec.txt",
                      O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (fd_normal == -1 || fd_cloexec == -1) {
        perror("open"); exit(1);
    }

    /* Set FD_CLOEXEC on second fd */
    flags = fcntl(fd_cloexec, F_GETFD);
    flags |= FD_CLOEXEC;
    fcntl(fd_cloexec, F_SETFD, flags);

    printf("=== Before exec ===\n");
    show_fd_status(fd_normal,  "Parent");
    show_fd_status(fd_cloexec, "Parent");
    printf("\n");

    /* Alternative: open with O_CLOEXEC directly */
    int fd_modern = open("/tmp/modern.txt",
                         O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
    printf("Modern O_CLOEXEC at open time:\n");
    show_fd_status(fd_modern, "Parent");
    printf("\n");

    child = fork();
    if (child == 0) {
        /* In child: check fds, then exec ls to check /proc */
        printf("=== Child (after fork, before exec) ===\n");
        show_fd_status(fd_normal,  "Child");
        show_fd_status(fd_cloexec, "Child");
        show_fd_status(fd_modern,  "Child");
        printf("\n");

        /* Exec ls -l /proc/self/fd to see which fds survive */
        execlp("ls", "ls", "-la", "/proc/self/fd", (char *)NULL);
        perror("execlp"); exit(1);
    }

    waitpid(child, &status, 0);

    close(fd_normal);
    close(fd_cloexec);
    close(fd_modern);
    return 0;
}

/* In the exec'd ls output:
 * fd_normal WILL appear (no FD_CLOEXEC)
 * fd_cloexec WILL NOT appear (FD_CLOEXEC was set)
 * fd_modern  WILL NOT appear (opened with O_CLOEXEC)
 */

Example 3: Implement Shell I/O Redirection (ls > file.txt)

/* example3_io_redirect.c
 * gcc -o ex3 example3_io_redirect.c && ./ex3
 *
 * Implements: ls -la /tmp > /tmp/ls_output.txt
 * Like a shell would do it internally.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    pid_t child;
    int status;
    const char *outfile = "/tmp/ls_output.txt";

    printf("Running: ls -la /tmp > %s\n\n", outfile);

    child = fork();
    if (child == -1) { perror("fork"); exit(1); }

    if (child == 0) {
        int fd;

        /* Step 1: Open the output file */
        fd = open(outfile,
                  O_WRONLY | O_CREAT | O_TRUNC,
                  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
        if (fd == -1) { perror("open"); exit(1); }

        /* Step 2: Redirect stdout to this file */
        if (fd != STDOUT_FILENO) {
            if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2"); exit(1); }
            close(fd);  /* Close original fd; stdout now points to file */
        }

        /* Step 3: exec ls — its stdout writes go to our file */
        execlp("ls", "ls", "-la", "/tmp", (char *)NULL);
        perror("execlp");
        exit(1);
    }

    /* Parent waits */
    waitpid(child, &status, 0);

    if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
        printf("Redirect successful! Showing first 5 lines of output:\n\n");
        /* Read and display the output file */
        FILE *f = fopen(outfile, "r");
        if (f) {
            char line[256];
            int count = 0;
            while (fgets(line, sizeof(line), f) && count < 5) {
                printf("  %s", line);
                count++;
            }
            fclose(f);
        }
    } else {
        printf("Command failed\n");
    }

    return 0;
}

❓ Interview Questions — File Descriptors and exec()

Q1. By default, are file descriptors inherited across exec()?
Answer: Yes. All open file descriptors remain open after exec() by default. The new program inherits them at the same fd numbers. This enables features like I/O redirection in shells.
Q2. What is FD_CLOEXEC and when should you use it?
Answer: FD_CLOEXEC is a per-fd flag. When set, the fd is automatically closed when exec() succeeds. Use it when: exec’ing an unknown/untrusted program, opening files in a library that shouldn’t leak to child programs, or any security-sensitive context where you don’t want the child to inherit open files.
Q3. How does the shell implement “ls > output.txt”?
Answer: fork() → child opens output.txt getting fd N → dup2(N, STDOUT_FILENO=1) to make stdout point to output.txt → close(N) → execve(“ls”, …). When ls writes to stdout (fd 1), it goes to the file. fd inheritance across exec makes this work.
Q4. What happens to the FD_CLOEXEC flag after dup() or dup2()?
Answer: The duplicate fd always has FD_CLOEXEC cleared (off), regardless of the original fd’s setting. This is required by POSIX. So if you dup() an fd with FD_CLOEXEC, the duplicate does NOT have FD_CLOEXEC and will NOT be closed on exec.
Q5. How can you set FD_CLOEXEC at file open time (without a separate fcntl call)?
Answer: Use the O_CLOEXEC flag with open(): fd = open("file", O_RDONLY | O_CLOEXEC);. Available since Linux 2.6.23. Preferred over the two-step fcntl approach in multithreaded programs to avoid a race condition between open() and fcntl().
Q6. Why is it a security concern if file descriptors are inherited by exec’d programs?
Answer: If a privileged process (running setUID) opens sensitive files and then exec’s a less-trusted program, the child inherits those open fds. The child can read/write those files even without permission to open them directly. Best practice: set FD_CLOEXEC on all sensitive fds before exec, or close them explicitly.
Q7. Why should library functions set FD_CLOEXEC on files they open?
Answer: Library functions open files as an implementation detail — they have no mechanism to force the calling program to close those fds before exec. By setting FD_CLOEXEC themselves, libraries ensure their internal fds don’t accidentally leak into exec’d programs.
Q8. Why is the two-step approach (open then fcntl to set FD_CLOEXEC) dangerous in multithreaded programs?
Answer: Between open() and fcntl(), another thread could call fork()+exec(). The fd exists but FD_CLOEXEC isn’t set yet, so it gets inherited. O_CLOEXEC solves this by setting the flag atomically during open(), with no gap for a race.

Next: Signals and exec()

What happens to signal handlers and the signal mask across exec()

→ Part 6: Signals & exec() 🏠 All Tutorials

Leave a Reply

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