Skip to content

Fix file descriptor leaks in enclave_proc_command_send_all - #754

Open
torbensen wants to merge 1 commit into
aws:mainfrom
torbensen:fix/enclave-proc-command-send-all-fd-leak
Open

Fix file descriptor leaks in enclave_proc_command_send_all#754
torbensen wants to merge 1 commit into
aws:mainfrom
torbensen:fix/enclave-proc-command-send-all-fd-leak

Conversation

@torbensen

@torbensen torbensen commented Jun 9, 2026

Copy link
Copy Markdown

Issue #, if available:

N/A

Description of changes:

Summary

enclave_proc_command_send_all() in src/enclave_proc_comm.rs leaks file descriptors on every invocation. For a one-shot CLI command this is harmless, but any long-running process that links this crate and calls the function on an interval (for example, a service that periodically issues a Describe to check enclave state) steadily accumulates descriptors until it reaches its RLIMIT_NOFILE. After that, every subsequent socket/accept syscall in the process fails with EMFILE ("No file descriptors available").

Root cause

There are two distinct leaks in the function:

  1. epoll instance (leaks on every call). The function obtains an epoll fd via nix::sys::epoll::epoll_create() and never closes it:

    let epoll_fd = epoll::epoll_create().map_err(...)?;

    With nix 0.26, epoll_create returns a bare RawFd that is not closed when it goes out of scope, so exactly one descriptor is leaked per call regardless of outcome.

  2. Connection sockets (leaks on the epoll_wait timeout path). Each connection is detached from its UnixStream with into_raw_fd() and registered with epoll. A descriptor is only turned back into a UnixStream (and thus eventually closed) when epoll_wait reports an event for it. When a peer does not reply within ENCLAVE_PROC_WAIT_TIMEOUT_MSEC, the loop hits if num_events == 0 { continue; } and that connection's fd is never reclaimed, never removed from epoll, and never closed — one leaked descriptor per unanswered connection. The early ? returns inside the loop have the same problem.

The first leak is deterministic and constant-rate, so a periodic caller exhausts its fd table after a fixed, predictable runtime.

Fix

Introduce a small RAII guard, local to the function, that owns the epoll fd and the set of detached connection fds, and closes all of them in Drop. This guarantees cleanup on every return path — the normal return, the early ? propagations during setup and in the wait loop, and the epoll_wait timeout branch. Descriptors that are successfully reclaimed into UnixStreams are released from the guard first, so there is no double close.

The change is confined to enclave_proc_command_send_all; the function's signature and observable behaviour are unchanged.

Testing

  • cargo check passes (the crate sets #![deny(warnings)], so the build also confirms the change is warning-free).
  • Reviewed all exit paths to confirm each fd is closed exactly once: reclaimed-and-returned sockets are closed by the caller; reclaimed-but-unconfirmed sockets are closed when their UnixStream drops; timed-out and registration-failed sockets plus the epoll fd are closed by the guard.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

enclave_proc_command_send_all() creates an epoll instance via
nix::sys::epoll::epoll_create() but never closes it. With nix 0.26 the
returned RawFd is not closed when it goes out of scope, so every call
leaks one descriptor. A long-running process that polls enclave state on
an interval (for example a periodic Describe-based health check)
therefore accumulates descriptors until it hits its RLIMIT_NOFILE and
subsequent syscalls fail with EMFILE ("No file descriptors available").

A second, related leak occurs on the epoll_wait timeout path: each
connection is detached from its UnixStream via into_raw_fd() and handed
to epoll, but a socket that does not reply before
ENCLAVE_PROC_WAIT_TIMEOUT_MSEC is never turned back into a UnixStream and
is neither removed from epoll nor closed, leaking one descriptor per
unanswered connection.

Wrap the epoll fd and the detached connection fds in a small RAII guard
so that every descriptor is closed on all return paths, including early
`?` propagations and epoll_wait timeouts. Descriptors that are reclaimed
into UnixStreams are released from the guard to avoid a double close.

Signed-off-by: torbensen <torbenmagne@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant