Skip to content

fix(fd): tighten FD/CLOEXEC lifetime across accept/pipe/compression#20

Open
andypost wants to merge 1 commit into
masterfrom
claude/audit-pr-e-fd-cloexec
Open

fix(fd): tighten FD/CLOEXEC lifetime across accept/pipe/compression#20
andypost wants to merge 1 commit into
masterfrom
claude/audit-pr-e-fd-cloexec

Conversation

@andypost
Copy link
Copy Markdown
Owner

@andypost andypost commented May 8, 2026

Summary

Audit-driven FD/CLOEXEC lifetime pass (PR-E from security-audit.md
/ PR #10). Seven findings; small, no protocol or config-surface
changes.

Findings addressed

  • V14 [High] Accept sockets missing CLOEXEC (conn_accept.c, epoll_engine.c, kqueue_engine.c)
  • V14 [High] Pipe FD leak on nonblocking-set failure (nxt_file.c)
  • V14 [Medium] Pipe FDs lack CLOEXEC (nxt_file.c, auto/files)
  • V14 [Medium] nxt_file_redirect close transient (skipped — see commit body)
  • V14 [Medium] accept4() ENOSYS detection narrowed (nxt_epoll_engine.c)
  • V14 [Low] socketpair SO_PASSCRED logging (skipped — already adequate)
  • V11 [High] Compression mmap FD leak (nxt_http_compression.c)

Test plan

  • ./configure --openssl succeeds; NXT_HAVE_PIPE2 lands in build/include/nxt_auto_config.h alongside NXT_HAVE_ACCEPT4.
  • make -j unitd clean (build sandbox blocked compile in this run; reviewer to confirm locally).
  • pytest-3 test/test_static.py test/test_http_compression.py (compression mmap path).
  • pytest-3 test/test_routing.py (accept path).
  • Spawn an app worker, inspect /proc/<pid>/fd/ for any leaked listener/client/pipe descriptors.

Upstream

Same fixes apply to freeunitorg/freeunit; will forward after merge.

Generated by Claude Code

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements measures to prevent file descriptor leaks by ensuring the CLOEXEC flag is set on sockets and pipes across various paths, including the addition of pipe2() support and SOCK_CLOEXEC for accept4(). It also addresses resource leaks in the static file compression path by closing file descriptors during mmap failures. Feedback identifies that the leak protection in the compression logic is incomplete, as several other error paths still leak the source file descriptor, and the practice of nullifying the file pointer on failure introduces an inconsistency that could affect the caller's error handling.

Comment on lines +284 to +285
nxt_file_close(task, *f);
*f = NULL;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fix for the source file descriptor leak (V11) is incomplete and introduces an API inconsistency.

  1. Incomplete Leak Fix: While *f is now closed on mmap failures, it is still leaked on other error paths in this function, such as ftruncate failures (lines 273 and 328) and compression failure (line 313). Since the function takes ownership of the file descriptor (as evidenced by it being closed on success at line 336), it should be closed on all error paths to prevent resource exhaustion.
  2. Inconsistent Pointer Handling: Setting *f = NULL on failure (lines 285 and 296) is inconsistent with the success path where the pointer *f remains valid while the underlying struct is updated (**f = tfile). This inconsistency may lead to null pointer dereferences in the caller if it attempts to access the file object after a failure, especially if it expects to fall back to uncompressed serving or perform its own cleanup.

Audit-driven pass for V11/V14 in security-audit.md; seven findings,
no protocol or config-surface changes.

V14 [High] Accepted sockets missing CLOEXEC.
nxt_conn_accept.c, nxt_kqueue_engine.c: set FD_CLOEXEC on the fd
returned by plain accept() before handing it to the connection layer
so it cannot leak into spawned application processes.
nxt_epoll_engine.c: pass SOCK_CLOEXEC alongside SOCK_NONBLOCK to
accept4().

V14 [High] Pipe FD leak on nxt_fd_nonblocking() failure.
nxt_file.c: nxt_pipe_create() now closes both ends via
nxt_pipe_close() if either nxt_fd_nonblocking() call fails, instead
of leaking the other end.

V14 [Medium] Pipe FDs lack CLOEXEC.
nxt_file.c: prefer pipe2(pp, O_CLOEXEC) where the kernel supports
it, with a fcntl(F_SETFD, FD_CLOEXEC) fallback on the legacy pipe()
path.  auto/files adds an NXT_HAVE_PIPE2 capability probe modeled on
the existing NXT_HAVE_ACCEPT4 detect.

V14 [Medium] accept4() ENOSYS detection narrowed.
nxt_epoll_engine.c: the capability probe falls back only when the
kernel actually returns ENOSYS; any other errno (e.g. EBADF, the
expected outcome on a -1 fd) is treated as "supported".  Behaviour
unchanged on Linux; the comment now explains why.

V14 [Medium] nxt_file_redirect close transient.  Skipped:
nxt_file.c:638 already alerts and returns NXT_ERROR on close()
failure after a successful dup2(), and the caller still owns the
descriptor's lifetime; no safe behaviour change is available.

V14 [Low] socketpair SO_PASSCRED logging.  Skipped:
nxt_socketpair.c already emits a dedicated "failed to set
SO_PASSCRED" alert before falling through to the cleanup-close
logging, so operators can already distinguish credential-passing
failures from generic socket teardown.

V11 [High] Compression mmap FD leak.
nxt_http_compression.c: on either mmap-failure return path,
nxt_http_comp_compress_static_response() now closes the source file
via *f and sets *f = NULL so the caller's fail-label cleanup does
not double-close.  Also adds nxt_alert() lines for the failures so
operators have visibility into FD/mem pressure.

CHANGES: add a single 1.35.4 bullet covering the seven findings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@andypost andypost force-pushed the claude/audit-pr-e-fd-cloexec branch from 0972892 to d3cdb07 Compare May 11, 2026 23:24
@andypost
Copy link
Copy Markdown
Owner Author

Gemini's [high] finding addressed in d3cdb079: the source-file fd is now closed (and *f set to NULL) on every error path in nxt_http_comp_compress_static_response — the two ftruncate failures (lines 273, 328) and the compression failure (line 313) join the two mmap-failure paths I added in the first round. The *f = NULL pattern is kept because the caller's fail handler in nxt_http_static.c:693 already NULL-guards before its own close; dropping the NULL would cause a double-close from that path. Force-pushed; build clean.

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