• Audit POSIX close-on-exec (FD_CLOEXEC/O_CLOEXEC): fork/exec children i

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Wed Jun 24 01:12:56 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1174

    ## Summary

    Follow-up to #1151 (the Windows listen-socket inheritance leak, fixed in commit 6816ce611). That fix addressed Windows handle inheritance; the equivalent **POSIX `fork`/`exec` close-on-exec story should be audited and hardened separately**, as flagged in the original report.

    On POSIX, a child created by `fork()` inherits **every** open file descriptor of the parent. Unless those fds are marked close-on-exec (`FD_CLOEXEC` / `O_CLOEXEC` / `SOCK_CLOEXEC`) or explicitly closed in the child before `exec`, the exec'd program inherits them — including all server listen sockets, the active client socket, message-base fds, SpiderMonkey/cryptlib fds, etc. This is the direct analog of the Windows leak: a long-lived external (a timed-event door, a CGI) could keep server listen sockets bound after the parent exits.

    ## Current state

    - **Zero** uses of `O_CLOEXEC`, `FD_CLOEXEC`, or `SOCK_CLOEXEC` across the entire `src/` tree (`git grep` returns nothing).
    - `sbbs3/xtrn.cpp` `external()` POSIX path: after `fork()` (xtrn.cpp:1021) the child sets up stdio redirection via `dup2()` and redirects unused stdio to `/dev/null`, then `execvp()`s (xtrn.cpp:1862) **without closing any other inherited descriptors** — no `closefrom()`, no fd-range close loop. So the door inherits all of the parent's other fds.
    - `sbbs3/js_global.cpp:52` `execv("/proc/self/exe", ...)` (self-restart) has the same exposure.
    - Listen sockets are created in the shared `xpdev/multisock.c` path (`socket()` at `xpms_add`) with no `SOCK_CLOEXEC`.

    ## Suggested approach

    Mirror the Windows fix's "close at the source" strategy, which is more robust than per-exec cleanup:

    1. **Create long-lived fds close-on-exec.** Set `SOCK_CLOEXEC` when creating listen/accept sockets in `multisock.c` (and `accept4(..., SOCK_CLOEXEC)` where available), and `O_CLOEXEC` on long-lived `open()`s. Where the platform lacks the atomic flag, fall back to `fcntl(fd, F_SETFD, FD_CLOEXEC)` immediately after creation.
    2. **Belt-and-suspenders in the child**, for fds we don't control: after `fork()` and after the intended `dup2()` redirections, `closefrom(3)` (or a portable fd-range close) before `exec`, keeping only 0/1/2 and any fd the door is explicitly meant to receive (e.g. a socket-handle door passed its descriptor).

    Care is needed not to break doors that are *intended* to inherit a specific descriptor (the POSIX equivalent of the Windows passthru/`client_socket_dup` socket-handle door) — those fds must be exempted from the close.

    ## Platform

    POSIX (Linux/macOS/*BSD). The Windows half is resolved by #1151 / 6816ce611.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)