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
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Program may read data from wrong file if descriptor was reassigned. |
| Integrity | Scope: Integrity Modify Application Data - Writes to closed descriptor may corrupt unintended files or send data to wrong recipients. |
| Availability | Scope: 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;
}
Related CWEs
- 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
- MITRE Corporation. "CWE-910: Use of Expired File Descriptor." https://cwe.mitre.org/data/definitions/910.html
- CERT C Secure Coding Standard. "FIO46-C. Do not access a closed file."
- CERT C Secure Coding Standard. "FIO42-C. Close files when they are no longer needed."