Chapter 27.4 — File Descriptors and exec()
FD Inheritance · FD_CLOEXEC · I/O Redirection · Shell Internals · EmbeddedPathashala
📌 Topic
FDs & exec()
FDs & exec()
🧠 Level
Intermediate
Intermediate
💻 Examples
3 Programs
3 Programs
❓ Q&A
8 Questions
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.
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.
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.
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.
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():
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.
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.
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.
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()
