Skip to content

Security: accept errors retried in tight loop — CPU/log-flood DoS via descriptor exhaustion #326

Description

@coderabbitai

Summary

Severity: High
Affected commit: cbf5791
Raised via: PR #323, requested by @leynos

Accept errors in the main TCP server loop are now swallowed and retried immediately. For persistent errors such as EMFILE/ENFILE caused by file-descriptor exhaustion, listener.accept() can return an error immediately and repeatedly. Because there is no connection limit, no backoff, no reserve-fd recovery path, and no shutdown/break on repeated fatal accept errors, a remote unauthenticated attacker can open and hold enough TCP connections to exhaust process file descriptors, after which the daemon spins, burns CPU, and floods logs.

The prior code propagated the accept error from main rather than entering a retry loop, so the tight retry/logging behaviour is introduced by this commit.


Evidence

src/main.rs (lines 120–134)

res = listener.accept() => {
    match res {
        Ok((socket, peer)) => {
            let pool = pool.clone();
            let mut rx = shutdown_rx.clone();
            join_set.spawn(async move {
                if let Err(e) = handle_client(socket, peer, pool, &mut rx).await {
                    eprintln!("connection error from {}: {}", peer, e);
                }
            });
        }
        Err(e) => {
            eprintln!("accept error: {}", e);
        }
    }
  • The Err branch logs and immediately returns to the accept loop with no backoff, fatal-error classification, or rate limiting.
  • Each accepted socket spawns a task without a connection cap, making descriptor exhaustion by many unauthenticated idle connections practical.

Attack Path

  1. Unauthenticated TCP client opens and holds many TCP connections to mxd on 0.0.0.0:5500 (default).
  2. listener.accept() succeeds and spawns one unbounded JoinSet task per socket.
  3. Idle clients keep their descriptors open; handle_client writes a banner then waits for input or shutdown signal.
  4. Process file descriptors are exhausted; subsequent accept() calls return EMFILE/ENFILE.
  5. The error branch logs and immediately retries — no delay, no recovery, no shutdown.
  6. CPU burns and logs flood; legitimate clients cannot connect.

Impact

Dimension Assessment
Likelihood High — unauthenticated, reachable over the primary TCP service, no application-level connection cap or idle timeout.
Impact High — availability-only: CPU/log flood and service unavailability. No RCE, authentication bypass, or data disclosure.
Severity High

Existing Controls

  • No authentication is required to consume a connection slot/file descriptor.
  • No global connection limit is present in repository code.
  • No per-IP rate limiting is present in repository code.
  • No unauthenticated idle timeout is present in repository code.
  • No accept-error backoff or log rate-limiting is present in repository code.
  • Shutdown signalling exists but does not mitigate attacker-driven descriptor exhaustion.

Suggested Remediation Direction

  • Introduce an accept-error backoff (e.g. exponential with a ceiling) for persistent/resource errors.
  • Classify errors: treat EMFILE/ENFILE as fatal or apply a sleep before retrying.
  • Add a global connection semaphore or cap to prevent unbounded task spawning.
  • Add an unauthenticated-idle timeout so clients that do not progress through authentication release descriptors promptly.
  • Optionally, rate-limit accept-error log emission to prevent log flooding.

Assumptions / Blindspots

  • No repository IaC, Kubernetes, systemd, or load-balancer manifests were present to confirm real deployment exposure or external connection limits.
  • Static review cannot confirm production ulimit values or host-level mitigations.
  • The actual mxd binary could not be built in the validation environment due to dependency/network restrictions; however, the source path and a dependency-free harness strongly support reachability.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions