• sbbsctrl/server processes leak listen-socket handles to spawned childr

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Fri May 29 11:42:43 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1151

    ## Summary

    On Windows, every server listen socket created by Synchronet is inheritable by default, and `CreateProcess` is called with `bInheritHandles = TRUE` in the paths that spawn timed events, external programs, and CGI scripts. As a result, every child process (jsexec for a timed event, an external program from the Terminal Server, a CGI from the Web Server) inherits open handles to **all** server listen sockets — Terminal Server, Mail, FTP, Web, NNTP, BBS, finger, gopher, IRC, etc.

    When the parent (sbbsctrl/sbbscon/sbbsNTsvcs) later exits — cleanly or otherwise — any still-running child keeps those listen sockets alive in the kernel. The next instance of the parent can still bind via `SO_REUSEADDR`, but both the orphan and the new parent now race for incoming connections, and accepts handed to the orphan are silently dropped (the orphan has no Synchronet runtime state to service them). From the user's perspective, *"sbbsctrl is running but won't answer incoming connections of any protocol."*

    ## Observed (this morning, vert / Windows 11)

    1. `chat_llm_irc.js` was started via a timed event under sbbsctrl PID **77564** at 2026-05-29 00:09 — jsexec.exe PID **73240**.
    2. PID 77564 later exited.
    3. A new sbbsctrl (PID **56988**) started, ran for a while, then also exited. 4. A third sbbsctrl (PID **85988**) was launched at 11:20.
    5. `netstat -ano | findstr LISTENING` showed **both** 85988 and 56988 as owners of every Synchronet service port (23/25/80/21/110/119/143/443/587/1123/8466/8467/11235/24554/24555/…). Neither was visible in `Get-Process`/Task Manager except 85988.
    6. Connections to the BBS hung or were dropped.
    7. Killing 85988 left PID 56988 still owning all 48 of its Synchronet listening sockets in the kernel — but no process named 56988 existed (`Get-CimInstance Win32_Process` returned nothing).
    8. The actual holder was jsexec PID 73240 (`chat_llm_irc.js`), which had inherited the listen-socket handles back when PID 77564 spawned it. `Stop-Process -Id 73240` would release the sockets.

    ## Source

    `bInheritHandles=TRUE` is passed at every `CreateProcess` call site that runs inside a process holding listen sockets:

    - `src/sbbs3/xtrn.cpp:641` — `native && !(mode & EX_OFFLINE)` (TRUE for the timed-event / native online path that spawned `chat_llm_irc.js`)
    - `src/sbbs3/websrvr.cpp:5376` — literal `true` (CGI)
    - `src/sbbs3/ctrl/MainFormUnit.cpp:1497`, `:2949`, `:3833`, `:3887` — sbbsctrl spawning scfg/echocfg/etc.

    And `grep -rE 'SetHandleInformation|HANDLE_FLAG_INHERIT|WSA_FLAG_NO_HANDLE_INHERIT' src/` returns **zero** matches across the entire tree. Listen sockets created via `socket()`/`WSASocket()` without `WSA_FLAG_NO_HANDLE_INHERIT` are inheritable by default on Windows.

    ## Suggested fix

    Two complementary approaches; either alone would close the leak, doing both is belt-and-suspenders:

    1. **Mark listen sockets non-inheritable at creation.** In the common socket-creation helper (or in each server's listen-socket setup), on `_WIN32` call `SetHandleInformation((HANDLE)sock, HANDLE_FLAG_INHERIT, 0)` immediately after creating the listening socket. Alternative: switch to `WSASocketW(..., WSA_FLAG_NO_HANDLE_INHERIT)` directly.

    2. **Use `bInheritHandles=FALSE` in `CreateProcess`** and, for the paths that actually need the child to inherit specific handles (stdio for CGI/I/O-redirected externals), use `STARTUPINFOEX` + `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` to inherit *only* those handles. This is the documented MS pattern for selective inheritance.

    Of the two, **(1)** is the smaller and more targeted change and would resolve the symptom directly. **(2)** is more invasive but is the textbook fix and would also cover any other accidentally-inheritable handles (file handles, mutexes, pipes) that sbbsctrl/server processes may be creating today or in the future.

    ## Workaround

    Stop the leaking child process before restarting sbbsctrl. After identifying the orphan via `Get-NetTCPConnection -State Listen | Where-Object OwningProcess -eq <ghost-pid>` and tracing back to a long-running jsexec child, `Stop-Process -Id <pid> -Force` releases the inherited sockets.

    ## Platform

    Windows-only. POSIX `fork`/`exec` on Linux/macOS has its own close-on-exec story (`O_CLOEXEC` / `FD_CLOEXEC`) that should be audited separately, but the symptom report here is specifically Windows.

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