Subj : Audit POSIX close-on-exec (FD_CLOEXEC/O_CLOEXEC): fork/exec children i To : GitLab note in main/sbbs From : Rob Swindell Date : Sat Aug 08 2026 18:08:52 https://gitlab.synchro.net/main/sbbs/-/issues/1174#note_10090 Closing. Every fork/exec path in the servers now closes what the child has no business with, and the descriptors that survive are stdio plus, for a native socket-door, the one socket it is handed by number. ### What landed | commit | | |---|---| | 0db85afb4c (social-29-army, 2026-08-07) | listen sockets close-on-exec on POSIX | | b96da4eb92 (forget-6-zone, 2026-08-07) | use the creation flag where the platform has one, closing the window between socket() and fcntl() | | f7c74bcbbf (boost-4-denial, 2026-08-08) | accepted client sockets, via accept4() | | b16969edb8 (rats-4-fitting, 2026-08-08) | close everything else in the external() child | | 7384ba6569 (centers-11-chains, 2026-08-08) | do that with closefrom() rather than close_range() | | 0791f3e3bf (spec-12-defeat, 2026-08-08) | the same for the CGI child in websrvr.cpp | The Windows half was already done in 6816ce611a (carries-19-baby, 2026-06-24) under #1151. ### The one deliberate exemption `main.cpp` accepts the node passthru socket with plain `accept()`, not `xp_accept()`, and `external()` exempts it from the close. A native socket-door is handed that descriptor by number through the `%H` command-line specifier, so it is the one descriptor that has to survive exec. Both sites say so, because each otherwise reads as an oversight. ### Not done, and why it is no longer a hole Marking the long-lived `open()` call sites `O_CLOEXEC` was the fourth item under the suggested approach. It was not done and is not needed for correctness. The child-side close covers every descriptor regardless of who opened it, including the ones libraries open where there is no call site to mark. What `O_CLOEXEC` would still add is coverage for an exec occurring inside a library, where there is no child of ours in which to close anything. That is a narrow case and is better raised on its own if it ever bites. Worth recording for whoever meets that case: per-site marking cannot be completed by audit. This tree has roughly 1700 `open()`/`fopen()` sites, and the process holds descriptors it never opened - four sockets to the MQTT broker, created inside libmosquitto, were the example that settled the question. ### Verification Each change was checked against a scratch BBS rather than by inspection, using an external program, a socket-door, and a CGI over HTTP that each dumped the descriptor table they were handed. Before: a door started with the node.dab handles, the node log, client sockets and a pipe. After: stdio, and the socket door additionally reported its `%H` descriptor open and usable. For the CGI, a listen socket was deliberately left inheritable to stand in for a library-opened descriptor; the stock child inherited it and the fixed one did not. The outage that prompted this is written up in the note above, from 2026-08-07. *Authored by Claude (Claude Code), on behalf of @rswindell* --- SBBSecho 3.37-Linux * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705) .