• 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)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Wed Jun 24 01:09:42 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1151
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Wed Jun 24 01:10:02 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1151#note_9449

    Fixed in commit 6816ce611 (`carries-19-baby`).

    Two complementary changes:

    1. **`xpdev/multisock.c`** — every listen socket, and every accepted client socket, is now marked non-inheritable on Windows via `SetHandleInformation(HANDLE_FLAG_INHERIT, 0)` immediately after creation. Since this is the shared listen-socket path for all servers (Terminal/Mail/FTP/Web/Services), a spawned child can no longer inherit a listen socket regardless of any `CreateProcess` inheritance flag. This closes the leak at the source.

    2. **`sbbs3/xtrn.cpp` `external()`** — `bInheritHandles` is no longer passed `TRUE` unconditionally for the native/online path. It is now `TRUE` only when the child actually needs to inherit a handle we're sharing:
    - the redirected stdio pipes (`use_pipes`), or
    - the duplicated passthru/client socket that a native socket-door talks over (`native && passthru_thread_running && client_socket_dup != INVALID_SOCKET`).

    A timed event running `jsexec` (the case in this report) shares neither, so it now gets `bInheritHandles=FALSE`. DOS doors communicate via named mailslots/events and never needed inheritance.

    Approach #1 alone resolves the reported symptom; #2 is additional hygiene so we don't hand the duplicated client socket (or any future inheritable handle) to children that don't use it.

    Tested locally on Windows. The POSIX `O_CLOEXEC`/`FD_CLOEXEC` audit noted in the report remains a separate follow-up.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Wed Jun 24 01:13:06 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1151#note_9455

    The POSIX `FD_CLOEXEC`/`O_CLOEXEC` audit follow-up is now tracked as #1174.

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