Exposure of File Descriptor to Unintended Control Sphere ('File Descriptor Leak')

Description

Exposure of File Descriptor to Unintended Control Sphere occurs when a process fails to close sensitive file descriptors before spawning a child process, enabling the child to perform unauthorized I/O operations using those descriptors. When a new process is forked or executed, the child inherits open file descriptors from its parent. This creates a vulnerability when the child has fewer privileges than the parent but can still access the inherited descriptor, potentially circumventing access controls on the associated file or resource.

Risk

File descriptor leaks have severe implications. Child processes access parent's privileged files. Privilege escalation through inherited descriptors. Unauthorized read/write to sensitive files. Bypass of file permissions. Socket hijacking. Database connection theft. Log file manipulation. High likelihood when privilege separation is implemented without proper fd management.

Solution

Close all sensitive file descriptors before spawning child processes during implementation phase. Use O_CLOEXEC flag when opening files. Set FD_CLOEXEC on existing descriptors. Implement close-all-fds pattern before exec. Audit descriptor inheritance across privilege boundaries during architecture and design phase.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Unauthorized read access to application data through inherited file descriptors.
IntegrityScope: Integrity

Unauthorized modification of application data through inherited write descriptors.

Example Code

Vulnerable Code

// Vulnerable: File descriptor leak to child process

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>

// VULNERABLE: Privileged fd inherited by unprivileged child
void vulnerable_privilege_separation() {
    // Parent opens sensitive file as root
    int shadow_fd = open("/etc/shadow", O_RDWR);
    if (shadow_fd < 0) {
        return;
    }

    pid_t pid = fork();

    if (pid == 0) {
        // Child process - drop privileges
        setuid(getuid());  // Drop to regular user

        // VULNERABLE: Child still has shadow_fd open!
        // Can read/write /etc/shadow despite being unprivileged

        char buffer[1024];
        read(shadow_fd, buffer, sizeof(buffer));  // Works!

        // Attacker can read password hashes
        printf("Shadow contents: %s\n", buffer);

        exit(0);
    }

    close(shadow_fd);
    waitpid(pid, NULL, 0);
}

// VULNERABLE: Socket descriptor leak
void vulnerable_inetd_style() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    // ... bind, listen ...

    while (1) {
        int client_fd = accept(server_fd, NULL, NULL);

        pid_t pid = fork();
        if (pid == 0) {
            // Child handles this client

            // VULNERABLE: server_fd still open
            // Child could accept other connections
            // or interfere with server operation

            // Drop privileges for handling untrusted client
            setuid(untrusted_uid);

            // Process client request
            handle_request(client_fd);

            // VULNERABLE: Can still use server_fd
            // to accept connections as the original user

            exit(0);
        }

        close(client_fd);  // Parent closes client fd
        // But child has both fds!
    }
}

// VULNERABLE: Log file descriptor leak
void vulnerable_logging() {
    // Open log file with elevated privileges
    int log_fd = open("/var/log/secure.log", O_WRONLY | O_APPEND);

    // Fork worker process
    pid_t pid = fork();

    if (pid == 0) {
        // Worker with reduced privileges
        setgid(worker_gid);
        setuid(worker_uid);

        // VULNERABLE: Can still write to secure log
        // Could inject false log entries
        const char* fake = "root logged in from 127.0.0.1\n";
        write(log_fd, fake, strlen(fake));

        do_work();
        exit(0);
    }

    close(log_fd);
    waitpid(pid, NULL, 0);
}
# Vulnerable: Python file descriptor inheritance

import os
import subprocess

# VULNERABLE: File descriptor inherited by subprocess
def vulnerable_subprocess():
    # Open sensitive configuration
    config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)

    # VULNERABLE: subprocess inherits the fd
    proc = subprocess.Popen(
        ['untrusted_program'],
        # close_fds defaults to False in older Python!
    )

    # untrusted_program can access the config file
    # via /proc/self/fd/{config_fd}

    proc.wait()
    os.close(config_fd)

# VULNERABLE: Multiprocessing with shared descriptors
from multiprocessing import Process

class VulnerableWorker:
    def __init__(self):
        # Open privileged resource
        self.secret_file = open('/root/.ssh/id_rsa', 'r')

    def start_worker(self):
        # VULNERABLE: Worker process inherits secret_file
        p = Process(target=self.worker_main)
        p.start()
        return p

    def worker_main(self):
        # Worker can read the SSH key!
        key_data = self.secret_file.read()
        # ... potential exfiltration ...

Fixed Code

// Fixed: Proper file descriptor management

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <dirent.h>

// FIXED: Close sensitive fd before fork
void safe_privilege_separation() {
    int shadow_fd = open("/etc/shadow", O_RDWR);
    if (shadow_fd < 0) {
        return;
    }

    // Read what we need while privileged
    char shadow_data[4096];
    ssize_t len = read(shadow_fd, shadow_data, sizeof(shadow_data) - 1);

    // FIXED: Close before fork
    close(shadow_fd);
    shadow_fd = -1;

    pid_t pid = fork();

    if (pid == 0) {
        // Child - no access to shadow_fd
        setuid(getuid());

        // Process data that was already read
        // Cannot access /etc/shadow directly

        exit(0);
    }

    // Clear sensitive data
    memset(shadow_data, 0, sizeof(shadow_data));
    waitpid(pid, NULL, 0);
}

// FIXED: Use O_CLOEXEC flag
void safe_with_cloexec() {
    // FIXED: File will be closed on exec
    int secret_fd = open("/etc/shadow", O_RDONLY | O_CLOEXEC);

    pid_t pid = fork();

    if (pid == 0) {
        // If we call exec, secret_fd will be closed automatically
        // For fork-only, we still need to close manually

        // FIXED: Close in child if not using exec
        close(secret_fd);

        // Now safe to drop privileges
        setuid(getuid());

        // ... do work ...
        exit(0);
    }

    close(secret_fd);
    waitpid(pid, NULL, 0);
}

// FIXED: Close all file descriptors
void close_all_fds(void) {
    // Method 1: Use /proc/self/fd
    DIR* dir = opendir("/proc/self/fd");
    if (dir) {
        int dir_fd = dirfd(dir);
        struct dirent* entry;

        while ((entry = readdir(dir)) != NULL) {
            int fd = atoi(entry->d_name);
            if (fd > STDERR_FILENO && fd != dir_fd) {
                close(fd);
            }
        }
        closedir(dir);
        return;
    }

    // Method 2: Iterate through possible fds
    struct rlimit rl;
    getrlimit(RLIMIT_NOFILE, &rl);

    for (int fd = STDERR_FILENO + 1; fd < (int)rl.rlim_cur; fd++) {
        close(fd);
    }
}

// FIXED: Safe server with fd management
void safe_inetd_style() {
    // FIXED: Server socket with close-on-exec
    int server_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    // ... bind, listen ...

    while (1) {
        // FIXED: Accept with close-on-exec
        int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC);

        pid_t pid = fork();
        if (pid == 0) {
            // FIXED: Close server fd in child
            close(server_fd);

            // Clear close-on-exec for client_fd if needed
            fcntl(client_fd, F_SETFD, 0);

            // Now safe to drop privileges
            setuid(untrusted_uid);

            handle_request(client_fd);
            close(client_fd);
            exit(0);
        }

        close(client_fd);
    }
}

// FIXED: Set close-on-exec on existing fd
void set_cloexec(int fd) {
    int flags = fcntl(fd, F_GETFD);
    if (flags >= 0) {
        fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
    }
}
# Fixed: Python with proper fd isolation

import os
import subprocess
import fcntl

# FIXED: Close fds before subprocess
def safe_subprocess():
    config_fd = os.open('/etc/app/secrets.conf', os.O_RDONLY)

    # Read what we need
    config_data = os.read(config_fd, 4096)

    # FIXED: Close before subprocess
    os.close(config_fd)

    # FIXED: close_fds=True (default in Python 3.2+)
    proc = subprocess.Popen(
        ['untrusted_program'],
        close_fds=True  # Explicitly set for clarity
    )

    proc.wait()

# FIXED: Use O_CLOEXEC equivalent
def safe_open_cloexec(path, flags):
    fd = os.open(path, flags)
    # FIXED: Set close-on-exec flag
    fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
    return fd

# FIXED: Multiprocessing with proper isolation
from multiprocessing import Process
import multiprocessing

class SafeWorker:
    def __init__(self):
        # Don't open sensitive files in parent
        self.secret_path = '/root/.ssh/id_rsa'

    def start_worker(self, task_data):
        # FIXED: Pass only necessary data, not file handles
        p = Process(target=self.worker_main, args=(task_data,))
        p.start()
        return p

    def worker_main(self, task_data):
        # Worker opens its own files with appropriate permissions
        # No inherited sensitive descriptors
        pass

# FIXED: Context manager for automatic cleanup
class SecureFD:
    def __init__(self, path, flags):
        self.fd = os.open(path, flags | os.O_CLOEXEC)

    def __enter__(self):
        return self.fd

    def __exit__(self, *args):
        os.close(self.fd)

# Usage
with SecureFD('/etc/shadow', os.O_RDONLY) as fd:
    data = os.read(fd, 4096)
# fd automatically closed, safe to fork/exec

CVE Examples

  • CVE-2003-0740: Sendmail file descriptor leak allowed privilege escalation.
  • CVE-2003-0937: OpenSSH file descriptor leak in privilege separation.
  • CVE-2006-5397: Screen file descriptor leak permitted unauthorized access.

  • CWE-402: Transmission of Private Resources into a New Sphere (parent)
  • CWE-399: Resource Management Errors (category)

References

  1. MITRE Corporation. "CWE-403: Exposure of File Descriptor to Unintended Control Sphere." https://cwe.mitre.org/data/definitions/403.html
  2. CERT C. "FIO42-C. Close files when they are no longer needed"
  3. OpenBSD. "Privilege Separation Guidelines"