Skip to content

Mark file descriptors close-on-exec at creation (fixes #887) - #888

Open
wharris623 wants to merge 6 commits into
nzbgetcom:developfrom
wharris623:fix/close-on-exec-fds
Open

Mark file descriptors close-on-exec at creation (fixes #887)#888
wharris623 wants to merge 6 commits into
nzbgetcom:developfrom
wharris623:fix/close-on-exec-fds

Conversation

@wharris623

@wharris623 wharris623 commented Aug 11, 2026

Copy link
Copy Markdown

Description

The original solution I worked through with Claude was to create a loop that closed every inherited file handle right before execvp(). The maintainer wanted something that treated the root cause, not the symptom. We agree — the root problem is that the descriptors are created in the inheritable state in the first place, so the fix is to make them non-inheritable at creation instead.

In Connection.cpp, this exact problem was already accounted for on Windows (SetHandleInformation(..., HANDLE_FLAG_INHERIT, 0)) — we just needed to extend the same treatment to POSIX. nzbget.cpp's lock-file open() already used O_CLOEXEC too. So this was already being addressed in a couple of places, just not consistently. We engineered a small helper (SetCloseOnExec) and applied it to every raw socket()/accept() call in Connection.cpp, and a second helper for the 4 fopen() calls in FileSystem.cpp, most importantly DiskFile::Open(). In ScriptController.cpp, we replaced the old close-everything loop with fcntl(fd, F_SETFD, FD_CLOEXEC) called right after each of the two pipe() calls. We scoped this to POSIX only — no Windows changes.

Addresses #887.

Util::SetCloseOnExec() replaces file-local helpers

Sockets handled in InitSocketOpts() (including the listening socket via Bind())

Two regression tests: flag check + fork/exec file-deletion behavior

AI assistance

  • This PR involved AI assistance

Claude did the code changes and testing. I reviewed and directed it — Claude has the technical skill, I have the people guidance, judgment on what makes sense, and troubleshooting direction.

Lib changes

N/A — no vendored libraries changed.

Testing

We did full compiles of the source both with and without our changes, and both passed all 10 test groups. We then added a new test case (within the existing UtilTest group) that checks FD_CLOEXEC directly via fcntl(fd, F_GETFD) on a file opened through DiskFile::Open(). For live verification, we built and ran the actual compiled patched and baseline binaries as real daemons, then used gdb to query the real listening socket's FD_CLOEXEC flag on each running process directly.

Follow-up to nzbgetcom#886 (closed) / addresses nzbgetcom#887, per maintainer feedback:
closing every inherited fd in a loop right before execvp() treats the
symptom, not the cause, and is fragile (misses anything opened after
the loop is written) and costly (has to iterate up to the process's fd
ceiling on every single fork). The root problem is that descriptors
are created inheritable in the first place.

This instead marks each descriptor non-inheritable (FD_CLOEXEC) at the
point it's created, matching the pattern the codebase already uses in
a couple of places (the lock-file open() already sets O_CLOEXEC; the
Windows side of Connection.cpp already clears HANDLE_FLAG_INHERIT on
some sockets) - just applied consistently everywhere a descriptor is
created on POSIX:

- ScriptController::StartProcess(): both pipe() calls. The ends that
  become the child's stdin/stdout/stderr are unaffected, since dup2()
  never carries over FD_CLOEXEC from its source fd - this only stops
  the original pipe fd numbers from leaking into any *other* child
  forked while they're still open.
- Connection.cpp: all 6 raw socket() calls and the accept() call, via
  a small SetCloseOnExec(SOCKET) helper mirroring the existing
  SetHandleInformation(..., HANDLE_FLAG_INHERIT, 0) calls already used
  for Windows in two of these spots. This covers both the WebUI/
  control-port listening socket (explains "port still busy after
  restart" from the issue) and outbound/accepted connection sockets.
- FileSystem.cpp: all 4 fopen() calls, via a SetCloseOnExec(FILE*)
  helper, most importantly DiskFile::Open() - the general-purpose file
  I/O class used throughout the app for downloads, par2, and unpacking,
  and the most likely actual source of the original EBUSY reports.
Directly checks the FD_CLOEXEC flag via fcntl(fd, F_GETFD) on a file
opened through DiskFile::Open() - the general-purpose file I/O class
used throughout the app, and the most likely source of the original
EBUSY reports in nzbgetcom#887.
@dnzbk
dnzbk self-requested a review August 12, 2026 05:40
@dnzbk dnzbk linked an issue Aug 12, 2026 that may be closed by this pull request
1 task
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.

Daemon descriptors leak into script children

2 participants