open
https://gitlab.synchro.net/main/sbbs/-/issues/1184
Seen in `data/error.log` (four occurrences: 2025-01-03, 2025-05-11, 2025-08-30, 2026-07-05, all on the Linux host):
```
Sun Jul 5 14:33:15 2026 master/90b6a1343 cvs.synchro.net
term Node 11 !ERROR 9 (Bad file descriptor) on local spy socket 179 receive
```
No spy (`localspy*.sock`) connection was involved. Journal correlation for the 2026-07-05 occurrence: fd 179 was the node's **passthru connect socket** — which for an SSH session *is* the node's `client_socket` — and the error fired one second after normal session teardown ("disconnecting client") closed it:
```
13:30:51 term Node 11 passthru connect socket 179 opened
14:33:13 term Node 11 <...> Executing post external program execution module: postxtrn ...
14:33:14 term Node 11 <...> disconnecting client
14:33:14 term Node 11 passthru connection reset by peer on receive
14:33:15 term Node 11 !ERROR 9 (Bad file descriptor) on local spy socket 179 receive
```
## Root cause
In `input_thread()` (`src/sbbs3/main.cpp`), the socket that was polled/`recv()`ed is identified **after the fact** by re-comparing against the mutable atomic:
```c
if (rd == SOCKET_ERROR)
{
#ifdef __unix__
if (sock == sbbs->client_socket) { // <-- re-reads the atomic, post-recv
...normal client-disconnect handling...
} else { // <-- assumes "not client => spy socket"
errprintf(LOG_ERR, WHERE, "Node %d !ERROR %d (%s) on local spy socket %d receive", ...);
close_socket(uspy_socket[node-1]);
...
```
Normal *nix hangup wakes `input_thread` by closing the client socket from another thread and storing `client_socket = INVALID_SOCKET`. When the wake-up `recv()` fails with `EBADF`, the identity re-check compares the old fd against the now-`INVALID_SOCKET` atomic, fails, and the error is misrouted into the "must be the local spy socket" branch: a routine disconnect gets logged as a LOG_ERR spy-socket failure (with the confusing fd number of the just-closed client/passthru socket) and lands in `error.log`. The spy-branch cleanup then does `close_socket(INVALID_SOCKET)`, which is harmless, so the defect is cosmetic/log-noise — but it reliably sends sysops hunting for spy-socket or passthru problems that don't exist.
The same stale re-comparison pattern exists at three more sites in `input_thread()`:
- the `ssh_mode && sock == sbbs->client_socket` receive-path selection,
- the `rd == 0 && sock == sbbs->client_socket` EOF/"disconnected" check,
- the `sock != sbbs->client_socket` skip-telnet-interpretation check.
Each can misclassify the socket when `client_socket` is cleared between poll-time selection and use.
## Suggested fix
Record which socket was selected at poll time (e.g. a `bool spy_sock` set alongside `sock = ...`) and branch on that instead of re-comparing `sock` against `sbbs->client_socket` at the four sites above. A genuinely-errored spy socket keeps the existing log+close+clear handling; a closed client socket routes to the existing client-disconnect handling (which already breaks quietly when `!sbbs->online`).
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)