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
- Unauthenticated TCP client opens and holds many TCP connections to
mxd on 0.0.0.0:5500 (default).
listener.accept() succeeds and spawns one unbounded JoinSet task per socket.
- Idle clients keep their descriptors open;
handle_client writes a banner then waits for input or shutdown signal.
- Process file descriptors are exhausted; subsequent
accept() calls return EMFILE/ENFILE.
- The error branch logs and immediately retries — no delay, no recovery, no shutdown.
- 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.
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/ENFILEcaused 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
mainrather than entering a retry loop, so the tight retry/logging behaviour is introduced by this commit.Evidence
src/main.rs(lines 120–134)Errbranch logs and immediately returns to the accept loop with no backoff, fatal-error classification, or rate limiting.Attack Path
mxdon0.0.0.0:5500(default).listener.accept()succeeds and spawns one unboundedJoinSettask per socket.handle_clientwrites a banner then waits for input or shutdown signal.accept()calls returnEMFILE/ENFILE.Impact
Existing Controls
Suggested Remediation Direction
EMFILE/ENFILEas fatal or apply a sleep before retrying.Assumptions / Blindspots
ulimitvalues or host-level mitigations.mxdbinary 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.