Subtopic 3
Danger Zone
Code Examples
vfork() is a Loaded Weapon
vfork() is one of the most dangerous system calls in Linux. Because the child runs in the parent’s address space with the parent suspended, almost any action in the child can silently corrupt the parent’s state. This subtopic covers the specific failure modes, what is safe vs unsafe, and why POSIX declared it obsolescent.
Using exit() instead of _exit() in a vfork() child flushes the parent’s stdio buffers, causing duplicate output:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
/* DO NOT disable buffering — leave it enabled to show the bug */
/* fprintf goes to stdio buffer, not yet to terminal */
fprintf(stdout, "Parent: this may print TWICE if child calls exit()\n");
pid_t pid = vfork();
if (pid == -1) { perror("vfork"); exit(1); }
if (pid == 0) {
/* WRONG: calling exit() in vfork() child.
exit() flushes stdio buffers.
But we share parent's stdio — parent's buffer gets flushed here.
When parent exits, it flushes AGAIN = double print. */
exit(0); /* BUG: use _exit(0) here! */
}
wait(NULL);
/* Parent exits: flushes stdio again = output appears TWICE */
return 0;
}
/* CORRECT version: replace exit(0) with _exit(0) in child */
_exit() in vfork() child — it never flushes stdio.#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* DANGEROUS: calling a function in vfork() child */
void dangerous_function(void)
{
/* This function pushes a new frame onto the PARENT's stack.
When it returns, it pops that frame.
If this function calls _exit(), the parent's stack pointer
may be in an inconsistent state. */
printf("In dangerous_function — WRONG!\n"); /* UB: printf in vfork child */
_exit(0);
}
/* SAFE version: minimal vfork() child function */
static int child_exec_result; /* avoid stack modification */
int main(void)
{
printf("Demonstrating vfork() stack safety rules.\n");
printf("The safe pattern:\n\n");
pid_t pid = vfork();
if (pid == -1) { perror("vfork"); exit(1); }
if (pid == 0) {
/* SAFE: exec immediately, no stack manipulation */
char *argv[] = { "true", NULL }; /* /bin/true exits 0 */
execvp("true", argv);
/* exec failed: only _exit() here, NOTHING else */
_exit(127);
}
int status;
waitpid(pid, &status, 0);
printf("Safe vfork+exec completed. Exit=%d\n",
WEXITSTATUS(status));
return 0;
}
In embedded RTOS environments without MMU, vfork() is sometimes the only way to “create” a process. This pattern shows the minimal safe template:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
/* Safe vfork + exec helper
Returns: exit status of program, or -1 on error
This is the ONLY correct usage pattern for vfork(). */
int safe_vfork_exec(const char *path, char *const argv[],
char *const envp[])
{
pid_t pid;
int status;
/* All setup (open files, pipes, etc.) BEFORE vfork */
/* ... any pre-exec setup here ... */
pid = vfork();
if (pid == -1) return -1;
if (pid == 0) {
/* ============================================
* ONLY these actions are allowed here:
* 1. Call exec()
* 2. If exec fails: call _exit() ONLY
* NO variable modification
* NO return
* NO stdio
* NO malloc/free
* ============================================ */
if (envp)
execve(path, argv, envp);
else
execv(path, argv);
/* exec failed — use write() not printf() */
const char *msg = "exec failed\n";
write(STDERR_FILENO, msg, strlen(msg));
_exit(127);
}
/* Parent: wait for exec'd program to finish */
if (waitpid(pid, &status, 0) == -1) return -1;
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
int main(void)
{
/* Example: run /bin/date via vfork */
char *argv[] = { "date", NULL };
int ret = safe_vfork_exec("/bin/date", argv, NULL);
printf("date returned: %d\n", ret);
/* Example: run /bin/ls /tmp */
char *argv2[] = { "ls", "/tmp", NULL };
ret = safe_vfork_exec("/bin/ls", argv2, NULL);
printf("ls returned: %d\n", ret);
return 0;
}
fork() + exec() over vfork() + exec(). The safety guarantee isn’t worth the marginal speed gain on modern hardware.SUSv4 (Single UNIX Specification version 4, 2008) marked vfork() as obsolescent:
- The behavior is undefined if the child modifies any data, calls any function other than exec/exit, or returns from the calling function
- On Linux with CoW, fork() is nearly as fast as vfork() for the exec case
- posix_spawn() provides a portable, safe alternative for embedded systems
- vfork() semantics vary across implementations — code using it is non-portable
- The dangers far outweigh the marginal performance benefit on modern systems
fork() + exec(). On embedded without MMU: use posix_spawn() if available, or vfork() with the strict safe template above.(1) Calling exit() — flushes parent’s stdio buffers causing duplicate output. (2) Modifying any local variable — corrupts parent’s stack frame. (3) Returning from the calling function — unwinds parent’s stack, likely crashes both processes. Also: calling malloc/free (corrupts shared heap), calling printf (shared stdio buffers).
printf() uses stdio’s internal buffers, which are shared (same address space). The child writing to stdio buffers can corrupt the parent’s buffered output. When the parent eventually flushes its buffers, it may print the child’s partial writes, causing garbled or duplicate output. Use write() (syscall directly) if you must output from a vfork() child.
posix_spawn() is the POSIX-recommended alternative. It combines process creation and program loading in a single call with a well-defined, safe API. On systems without MMU, posix_spawn() typically uses vfork() internally but hides the dangers behind a safe interface. It also accepts file_actions and spawn_attrs arguments for I/O setup without the vfork() risks.
