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

ImpactDetails
AvailabilityScope: Availability

DoS: Resource Consumption - Unlimited descriptor allocation allows attackers to exhaust available file descriptors.
AvailabilityScope: 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

  1. MITRE Corporation. "CWE-774: Allocation of File Descriptors or Handles Without Limits or Throttling." https://cwe.mitre.org/data/definitions/774.html
  2. CERT C Coding Standard. "FIO42-C. Close files when they are no longer needed."
  3. Linux Manual. "getrlimit, setrlimit - get/set resource limits."