vfork() Dangers & Obsolescence

vfork() Dangers & Obsolescence
Topic 5 → Subtopic 3  |  What can go wrong and why vfork() is obsolescent
Topic 5
Subtopic 3

Danger Zone
3
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.

Keywords:

undefined behavior stack corruption double flush atexit() obsolescent SUSv4 safe vs unsafe embedded use case

⚠ What is Safe vs Dangerous in a vfork() Child
✓ SAFE in vfork() child
Call exec() variants (execl, execv, etc.)
Call _exit()
Read (not write) memory
Call async-signal-safe functions only
Open files (before exec)
✗ DANGEROUS / Undefined Behavior
Modify any local (stack) variable
Call exit() (flushes parent’s stdio)
Return from the function that called vfork()
Call any function that may use stack deeply
Call malloc() / free() (heap shared)
Call printf() or any stdio function
Use global or static variables (shared)
Call longjmp()
Install signal handlers

💻 Code Example 1: The stdio Double-Flush Danger

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 */
Bug: Output may print twice. On some systems it crashes. Always use _exit() in vfork() child — it never flushes stdio.

💻 Code Example 2: Stack Corruption When Child Returns
#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;
}

💻 Code Example 3: Legitimate vfork() Use — Embedded Systems

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;
}
Rule of thumb: If you’re not writing embedded code without an MMU, prefer fork() + exec() over vfork() + exec(). The safety guarantee isn’t worth the marginal speed gain on modern hardware.

🚫 Why POSIX Declared vfork() Obsolescent

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
Recommendation: On modern Linux desktop/server: always use fork() + exec(). On embedded without MMU: use posix_spawn() if available, or vfork() with the strict safe template above.

🅾 Interview Questions
Q1: List 3 things that are undefined behavior in a vfork() child.

(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).

Q2: Why is calling printf() dangerous in a vfork() child?

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.

Q3: On an embedded system without MMU, what is the alternative to vfork()?

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.

Series Navigation
Topic 5 → Subtopic 3 of 3  |  Next: Topic 6 → Race Conditions

← Previous Next: Race Conditions → Index

Leave a Reply

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