https://gitlab.synchro.net/main/sbbs/-/issues/1174#note_10056
This is no longer hypothetical. It caused a production outage today (2026-08-07) on the development host, in exactly the shape predicted above:
"a long-lived external could keep server listen sockets bound after the parent exits."
### What happened
A `systemctl reload` of `sbbs.service` (SIGHUP, recycle-all) was issued at about 14:58. The Terminal Server recycled cleanly, but the Mail, FTP and Services servers could not rebind and looped on their retry counters:
```
mail 0009 !ERROR 98 binding SMTP Transfer Agent socket to port 25: Address already in use
srvc 0023 !ERROR 98 binding NNTP socket to port 119: Address already in use
ftp 0014 !ERROR 98 binding FTP Server socket to port 21: Address already in use
systemd[1]: sbbs.service: Reload operation timed out. Killing reload process. ```
SMTP and NNTP stayed down for roughly 25 minutes.
### What was holding the ports
A `/bin/bash -i`, running as the `sbbs` user and parented to the daemon, started at 14:26:39, i.e. about half an hour before the recycle. Its environment identifies it as an external program on a node rather than
anything administrative:
```
SBBSNNUM=11 SBBSNODE=/sbbs/node11/ SBBSCTRL=/sbbs/ctrl/ TERM=pcansi
```
It held 166 open descriptors. Matching the listening-socket inodes from `/proc/net/tcp` against that process's `/proc/<pid>/fd` confirmed it held
every one of the six affected listen sockets (three ports, two interfaces each):
| port | inodes held by the door |
|---|---|
| 21 | 553856839, 553860416 |
| 25 | 553856845, 553856847 |
| 119 | 553861367, 553861369 |
The rest was mostly dozens of duplicate handles on `ctrl/node.dab`, which is
a second consequence of the same inheritance and worth keeping in mind for
the lock-manager behavior.
Terminating that one process released the ports.
### The symptom is worse than "bind failed"
From a client's point of view this does not look like a down server, and that is what makes it nasty to diagnose. The parent still had its own listen
sockets open, so the kernel completed TCP handshakes normally: connections
were accepted into the backlog and simply never processed, because the server threads that would `accept()` them had exited. `ss` showed a Recv-Q of 10 on the FTP listener. Connecting to port 25 gave a successful TCP session and then silence, with no `220` banner at all. It presents as a hung server rather than a bind error, and only the server log names the real cause.
### Which of the two suggested remedies would have prevented it
Only the first one. The door was spawned roughly 30 minutes before the
recycle, so no cleanup performed at recycle time could have helped; the descriptors were already duplicated into a process the server does not
control and cannot reach. Creating the listen sockets close-on-exec in `multisock.c` is what closes this, because it acts at the moment the door is exec'd. The `closefrom()` belt-and-suspenders in `external()` would also have covered this particular case, but it only protects children the BBS spawns itself, so the socket-creation fix is the one that generalizes.
One practical note in favor of doing the socket half first: it is a small, self-contained change, and it carries none of the risk of the child-side
close, which has to preserve whatever descriptor a socket-handle door is
meant to inherit.
Line numbers have drifted since this issue was filed. On master today the `fork()` in `external()` is at `sbbs3/xtrn.cpp:1047` and the `execvp()` at `sbbs3/xtrn.cpp:1905`, and between them the child still touches only fds 0,
1, 2 and its pipes. `git grep` for `O_CLOEXEC`, `FD_CLOEXEC` and
`SOCK_CLOEXEC` across `src/` still returns nothing.
*Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)