Use of Expired File Descriptor

Description

Use of Expired File Descriptor occurs when software uses or accesses a file descriptor after it has been closed. Once a file descriptor is closed, it is released back to the operating system and becomes available for reassignment to new file operations. If the program continues to use the closed file descriptor, it may inadvertently read from or write to a completely different file, device, or socket that has been assigned the same descriptor number. This leads to data corruption, information disclosure, or unexpected program behavior.

Risk

Using expired file descriptors can have serious consequences. Data intended for one file may be written to a different file, causing corruption or information leakage. The program may read data from an unintended source, leading to incorrect processing. Network sockets may be confused, causing data to be sent to wrong recipients. Critical files may be corrupted when writes go to unintended destinations. Programs may crash when attempting operations on invalid descriptors. In multi-threaded applications, race conditions around file descriptor reuse increase the likelihood and severity of this issue.

Solution

Track file descriptor lifecycle carefully and ensure descriptors are not used after closure. Set file descriptors to -1 after closing to prevent accidental reuse. Use wrapper structures that track open/closed state. Implement defensive checks before file operations. In multi-threaded code, use proper synchronization when accessing shared file descriptors. Consider using higher-level abstractions (streams, file objects) that manage descriptor lifecycle automatically. Use static analysis tools to detect use-after-close patterns. In languages with resource management, use RAII patterns or try-with-resources to ensure proper cleanup.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Program may read data from wrong file if descriptor was reassigned.
IntegrityScope: Integrity

Modify Application Data - Writes to closed descriptor may corrupt unintended files or send data to wrong recipients.
AvailabilityScope: Availability

DoS: Crash/Exit/Restart - Accessing closed file descriptor can cause crashes or undefined behavior.

Example Code

Vulnerable Code

// Vulnerable: Using file descriptor after close
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void vulnerable_file_operation() {
    int fd = open("/tmp/data.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        return;
    }

    write(fd, "Initial data\n", 13);

    // Close the file
    close(fd);

    // ... other code that may open new files ...
    // fd value (e.g., 3) may be reassigned to new file

    // Vulnerable: Using fd after close
    // May write to wrong file!
    write(fd, "More data\n", 10);
}
// Vulnerable: Double close and use
void vulnerable_double_close(int fd) {
    // First close
    close(fd);

    // ... some code ...

    // Vulnerable: fd might be reassigned between closes
    close(fd);  // May close a different file!
}

void vulnerable_process_file(const char *path) {
    int fd = open(path, O_RDONLY);
    char buffer[1024];

    read(fd, buffer, sizeof(buffer));

    // Close in error handling
    if (validate_data(buffer) < 0) {
        close(fd);
        // Forgot to return - continues to use fd
    }

    // Vulnerable: fd may be closed but code continues
    read(fd, buffer, sizeof(buffer));  // Uses expired descriptor
    close(fd);  // Double close if error occurred
}
// Vulnerable: Shared descriptor in multi-threaded code
typedef struct {
    int log_fd;
    pthread_mutex_t lock;
} Logger;

Logger logger = { .log_fd = -1 };

void vulnerable_log(const char *message) {
    pthread_mutex_lock(&logger.lock);

    if (logger.log_fd >= 0) {
        write(logger.log_fd, message, strlen(message));
    }

    pthread_mutex_unlock(&logger.lock);
}

void vulnerable_close_log() {
    // Vulnerable: No lock protection
    close(logger.log_fd);

    // Other thread may use expired fd between close and assignment
    logger.log_fd = -1;
}
# Vulnerable: Using file after close
def vulnerable_process_file(filename):
    f = open(filename, 'r')
    data = f.read()
    f.close()

    # Some processing
    if needs_more_data(data):
        # Vulnerable: File is closed
        more_data = f.read()  # ValueError: I/O operation on closed file
// Vulnerable: Stream used after close
public class VulnerableStreamHandler {

    private OutputStream output;

    public void process(byte[] data) throws IOException {
        output.write(data);  // May be closed

        if (shouldClose()) {
            output.close();
        }

        // Vulnerable: output may be closed
        output.flush();  // IOException if closed
    }
}
// Vulnerable: File descriptor leak with potential reuse
int vulnerable_copy_file(const char *src, const char *dst) {
    int src_fd = open(src, O_RDONLY);
    int dst_fd = open(dst, O_WRONLY | O_CREAT, 0644);

    if (src_fd < 0) {
        return -1;  // Leak: dst_fd not closed
    }

    if (dst_fd < 0) {
        close(src_fd);
        return -1;
    }

    char buffer[4096];
    ssize_t bytes;

    while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
        if (write(dst_fd, buffer, bytes) < 0) {
            close(src_fd);
            // Vulnerable: dst_fd not closed, may be reused
            return -1;
        }
    }

    close(src_fd);
    close(dst_fd);
    return 0;
}

Fixed Code

// Fixed: Track descriptor state and clear after close
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void fixed_file_operation() {
    int fd = open("/tmp/data.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        return;
    }

    write(fd, "Initial data\n", 13);

    // Close and invalidate descriptor
    close(fd);
    fd = -1;  // Fixed: Mark as invalid

    // ... other code ...

    // Fixed: Check before use
    if (fd >= 0) {
        write(fd, "More data\n", 10);  // Won't execute
    }
}

// Helper function for safe close
int safe_close(int *fd) {
    if (fd == NULL || *fd < 0) {
        return 0;  // Already closed or invalid
    }

    int result = close(*fd);
    *fd = -1;  // Mark as closed
    return result;
}
// Fixed: Proper error handling with single close point
int fixed_process_file(const char *path) {
    int fd = -1;
    int result = -1;
    char buffer[1024];

    fd = open(path, O_RDONLY);
    if (fd < 0) {
        goto cleanup;
    }

    if (read(fd, buffer, sizeof(buffer)) < 0) {
        goto cleanup;
    }

    if (validate_data(buffer) < 0) {
        goto cleanup;
    }

    if (read(fd, buffer, sizeof(buffer)) < 0) {
        goto cleanup;
    }

    result = 0;  // Success

cleanup:
    // Fixed: Single close point, always check descriptor
    if (fd >= 0) {
        close(fd);
    }
    return result;
}
// Fixed: Thread-safe logger with proper synchronization
typedef struct {
    int log_fd;
    pthread_mutex_t lock;
} Logger;

Logger logger = { .log_fd = -1, .lock = PTHREAD_MUTEX_INITIALIZER };

void fixed_log(const char *message) {
    pthread_mutex_lock(&logger.lock);

    if (logger.log_fd >= 0) {
        write(logger.log_fd, message, strlen(message));
    }

    pthread_mutex_unlock(&logger.lock);
}

void fixed_close_log() {
    pthread_mutex_lock(&logger.lock);

    // Fixed: Close and invalidate under lock protection
    if (logger.log_fd >= 0) {
        close(logger.log_fd);
        logger.log_fd = -1;
    }

    pthread_mutex_unlock(&logger.lock);
}
# Fixed: Use context manager for automatic cleanup
def fixed_process_file(filename):
    # Fixed: Context manager ensures proper close
    with open(filename, 'r') as f:
        data = f.read()

        if needs_more_data(data):
            # File still open within context
            more_data = f.read()

    # File automatically closed here

# Alternative: Explicit state tracking
class SafeFileReader:
    def __init__(self, filename):
        self.filename = filename
        self.file = None

    def open(self):
        if self.file is None:
            self.file = open(self.filename, 'r')

    def read(self):
        if self.file is None:
            raise RuntimeError("File not open")
        return self.file.read()

    def close(self):
        if self.file is not None:
            self.file.close()
            self.file = None

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, *args):
        self.close()
// Fixed: Try-with-resources for automatic cleanup
public class FixedStreamHandler {

    public void process(String filename, byte[] data) {
        // Fixed: Try-with-resources ensures close
        try (OutputStream output = new FileOutputStream(filename)) {
            output.write(data);
            output.flush();
        } catch (IOException e) {
            handleError(e);
        }
        // Stream automatically closed
    }
}

// Alternative: Explicit state tracking
public class SafeOutputStream {
    private OutputStream output;
    private boolean closed = false;

    public SafeOutputStream(OutputStream output) {
        this.output = output;
    }

    public synchronized void write(byte[] data) throws IOException {
        if (closed) {
            throw new IllegalStateException("Stream is closed");
        }
        output.write(data);
    }

    public synchronized void close() throws IOException {
        if (!closed) {
            output.close();
            closed = true;
        }
    }
}
// Fixed: Proper resource cleanup in copy function
int fixed_copy_file(const char *src, const char *dst) {
    int src_fd = -1;
    int dst_fd = -1;
    int result = -1;

    src_fd = open(src, O_RDONLY);
    if (src_fd < 0) {
        goto cleanup;
    }

    dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dst_fd < 0) {
        goto cleanup;
    }

    char buffer[4096];
    ssize_t bytes;

    while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) {
        if (write(dst_fd, buffer, bytes) != bytes) {
            goto cleanup;
        }
    }

    if (bytes < 0) {
        goto cleanup;  // Read error
    }

    result = 0;  // Success

cleanup:
    // Fixed: Always close all opened descriptors
    if (src_fd >= 0) {
        close(src_fd);
    }
    if (dst_fd >= 0) {
        close(dst_fd);
    }

    return result;
}

  • CWE-672: Operation on a Resource after Expiration or Release (parent)
  • CWE-416: Use After Free (related - memory analog)
  • CWE-775: Missing Release of File Descriptor or Handle after Effective Lifetime (related)
  • CWE-399: Resource Management Errors (category)

References

  1. MITRE Corporation. "CWE-910: Use of Expired File Descriptor." https://cwe.mitre.org/data/definitions/910.html
  2. CERT C Secure Coding Standard. "FIO46-C. Do not access a closed file."
  3. CERT C Secure Coding Standard. "FIO42-C. Close files when they are no longer needed."