Allocation of File Descriptors or Handles Without Limits or Throttling
Description
Allocation of File Descriptors or Handles Without Limits or Throttling is a resource management vulnerability where software allocates file descriptors or handles without enforcing restrictions on the quantity allocated. When applications open files, sockets, pipes, or other system resources without limit checking, attackers can trigger excessive allocation to exhaust available descriptors. This causes denial of service as the application and potentially other system processes can no longer perform file operations, establish network connections, or access other descriptor-based resources.
Risk
File descriptors are a finite system resource, typically limited per-process and system-wide. When applications don't limit descriptor allocation, attackers can cause resource exhaustion through various means: opening many connections to a server, triggering code paths that repeatedly open files, or exploiting functionality that creates descriptors. Once exhausted, the application cannot open new files, accept network connections, or perform I/O operations requiring descriptors. This denial of service may cascade to other processes sharing the same descriptor limits. Servers handling untrusted input are particularly vulnerable.
Solution
Implement limits on file descriptor allocation at both application and OS levels. Use setrlimit() and getrlimit() on POSIX systems to set per-process limits. Track active descriptors and refuse new allocations when approaching limits. Implement connection throttling for network servers. Use resource pools with bounded sizes. Set timeout values to automatically close idle connections. Monitor descriptor usage and implement alerts. Reserve some descriptors for critical operations. Implement proper cleanup to ensure descriptors are released promptly. Consider using epoll/kqueue for handling many connections efficiently with fewer descriptor management issues.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Resource Consumption - Unlimited descriptor allocation allows attackers to exhaust available file descriptors. |
| Availability | Scope: Availability DoS: System Impact - File descriptor exhaustion may affect other processes on the system. |
Example Code
Vulnerable Code
// Vulnerable: No limit on concurrent connections
#include <sys/socket.h>
#include <netinet/in.h>
void vulnerable_server(int port) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
// Vulnerable: No limit on accepted connections
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd >= 0) {
// Creates new handler, but no limit checking
handleClient(client_fd); // May create thread or store fd
}
}
}
// Vulnerable: File opens without limit
void vulnerable_process_files(const char** filenames, int count) {
// Vulnerable: No check if count is reasonable
int* fds = malloc(count * sizeof(int));
for (int i = 0; i < count; i++) {
// Vulnerable: Opens all files without limit
fds[i] = open(filenames[i], O_RDONLY);
// Attacker could provide thousands of files
}
// Process files...
for (int i = 0; i < count; i++) {
if (fds[i] >= 0) close(fds[i]);
}
free(fds);
}
# Vulnerable: No limit on file handles
class VulnerableFileCache:
def __init__(self):
self.open_files = {}
def get_file(self, filename):
if filename not in self.open_files:
# Vulnerable: No limit on cache size
self.open_files[filename] = open(filename, 'r')
return self.open_files[filename]
# Attacker can cache unlimited files
// Vulnerable: No limit on database connections
public class VulnerableConnectionPool {
private List<Connection> connections = new ArrayList<>();
// Vulnerable: Creates connections without limit
public Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(dbUrl);
connections.add(conn);
return conn;
}
// No maximum pool size, no throttling
}
Fixed Code
// Fixed: Limit concurrent connections
#include <sys/socket.h>
#include <sys/resource.h>
#define MAX_CONNECTIONS 1000
static int current_connections = 0;
static pthread_mutex_t conn_mutex = PTHREAD_MUTEX_INITIALIZER;
void fixed_server(int port) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// Set resource limits
struct rlimit rl;
rl.rlim_cur = MAX_CONNECTIONS + 10; // +10 for server overhead
rl.rlim_max = MAX_CONNECTIONS + 10;
setrlimit(RLIMIT_NOFILE, &rl);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
while (1) {
// Fixed: Check limit before accepting
pthread_mutex_lock(&conn_mutex);
int can_accept = (current_connections < MAX_CONNECTIONS);
pthread_mutex_unlock(&conn_mutex);
if (!can_accept) {
sleep(1); // Wait for connections to free up
continue;
}
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd >= 0) {
pthread_mutex_lock(&conn_mutex);
current_connections++;
pthread_mutex_unlock(&conn_mutex);
handleClient(client_fd);
}
}
}
void connection_closed() {
pthread_mutex_lock(&conn_mutex);
current_connections--;
pthread_mutex_unlock(&conn_mutex);
}
// Fixed: Limit file opens with validation
#define MAX_CONCURRENT_FILES 100
int fixed_process_files(const char** filenames, int count) {
// Fixed: Validate count
if (count > MAX_CONCURRENT_FILES) {
fprintf(stderr, "Too many files requested: %d (max %d)\n",
count, MAX_CONCURRENT_FILES);
return -1;
}
// Check current descriptor availability
struct rlimit rl;
getrlimit(RLIMIT_NOFILE, &rl);
if (count > (int)(rl.rlim_cur - 10)) { // Reserve 10 for other ops
fprintf(stderr, "Not enough file descriptors available\n");
return -1;
}
int* fds = malloc(count * sizeof(int));
int opened = 0;
for (int i = 0; i < count; i++) {
fds[i] = open(filenames[i], O_RDONLY);
if (fds[i] >= 0) opened++;
}
// Process files...
for (int i = 0; i < count; i++) {
if (fds[i] >= 0) close(fds[i]);
}
free(fds);
return 0;
}
# Fixed: Bounded file cache with LRU eviction
from collections import OrderedDict
class FixedFileCache:
def __init__(self, max_files=100):
self.max_files = max_files
self.open_files = OrderedDict()
def get_file(self, filename):
if filename in self.open_files:
# Move to end (most recently used)
self.open_files.move_to_end(filename)
return self.open_files[filename]
# Fixed: Evict oldest if at limit
while len(self.open_files) >= self.max_files:
oldest_name, oldest_file = self.open_files.popitem(last=False)
oldest_file.close()
self.open_files[filename] = open(filename, 'r')
return self.open_files[filename]
def close_all(self):
for f in self.open_files.values():
f.close()
self.open_files.clear()
// Fixed: Bounded connection pool
public class FixedConnectionPool {
private final int maxConnections;
private final BlockingQueue<Connection> availableConnections;
private final Set<Connection> allConnections;
private final Semaphore connectionSemaphore;
public FixedConnectionPool(int maxConnections) {
this.maxConnections = maxConnections;
this.availableConnections = new LinkedBlockingQueue<>();
this.allConnections = Collections.synchronizedSet(new HashSet<>());
this.connectionSemaphore = new Semaphore(maxConnections);
}
public Connection getConnection(long timeout, TimeUnit unit)
throws SQLException, InterruptedException {
// Fixed: Block if pool exhausted
if (!connectionSemaphore.tryAcquire(timeout, unit)) {
throw new SQLException("Connection pool exhausted");
}
Connection conn = availableConnections.poll();
if (conn == null || conn.isClosed()) {
conn = createNewConnection();
}
return conn;
}
public void releaseConnection(Connection conn) {
if (conn != null && !conn.isClosed()) {
availableConnections.offer(conn);
}
connectionSemaphore.release();
}
private Connection createNewConnection() throws SQLException {
Connection conn = DriverManager.getConnection(dbUrl);
allConnections.add(conn);
return conn;
}
}
Detection Methods
- Static Analysis: SAST tools can identify unbounded allocation loops and missing limit checks.
- Runtime Monitoring: Track file descriptor usage with lsof, /proc/pid/fd, or custom instrumentation.
- Load Testing: Stress test applications to identify descriptor exhaustion points.
References
- MITRE Corporation. "CWE-774: Allocation of File Descriptors or Handles Without Limits or Throttling." https://cwe.mitre.org/data/definitions/774.html
- CERT C Coding Standard. "FIO42-C. Close files when they are no longer needed."
- Linux Manual. "getrlimit, setrlimit - get/set resource limits."