Subj : xp_getch() ignores EOF and returns an uninitialized byte, spinning smb To : GitLab issue in main/sbbs From : Rob Swindell Date : Fri Sep 25 2026 13:30:59 open https://gitlab.synchro.net/main/sbbs/-/work_items/1256 ## Repro ``` smbutil r1 /path/to/sub.shd < /dev/null ``` `smbutil` prints the first message, then spins at full CPU re-printing its prompt without ever blocking or exiting: ``` Reading /sbbs/data/subs/aiplayground (?=Menu): #3 (3) Subj : .... Reading /sbbs/data/subs/aiplayground (?=Menu): #3 (3) .... ``` Observed over 64 MB of output in roughly 45 seconds before the capture was cut off. The same happens for any redirected/closed stdin, e.g. running `smbutil r` from a script, a cron job, or any harness without a tty. ## Root cause Not in smbutil. `xp_getch()` (src/xpdev/conwrap.c:135) checks only for a read *error* and ignores EOF: ```c int xp_getch(void) { char c; if (!beensetup) xp_termios_setup(); /* get a char out of stdin */ if (read(STDIN_FILENO, &c, 1) == -1) return 0; return c; } ``` `read()` returns **0** at EOF, not -1. That path falls through to `return c;` with `c` never written, so the function returns an **uninitialized stack byte**. Two defects in three lines: 1. EOF is indistinguishable from a keypress, and there is no value a caller can test for it. 2. The returned value on EOF is indeterminate. `-Wall` does not flag it here because the compiler cannot see that the `read()` result constrains whether `c` was written. `0` is also already the documented-by-implementation return for a read error, so it is overloaded before EOF is even considered. The consequence at src/sbbs3/smbutil.c:1886 is that `switch (toupper(xp_getch()))` receives garbage, hits `default`, and loops forever. ## Scope sbbscon is **not** affected: it checks `isatty()` (sbbscon.c:1937, :1951) and gates its read behind `xp_kbhit()` (sbbscon.c:1967), so it never reaches `xp_getch()` on a dead stdin. The exposed callers are the utilities that call `xp_getch()` unguarded: * src/sbbs3/smbutil.c:1886 (the read loop above) * src/sbbs3/chksmb.c:227, :1265 * src/sbbs3/sbbsecho.c:3335 Only smbutil's is inside a loop, so only it spins; the others return a garbage byte once and act on it, which in a pause-for-keypress context is harmless but is still an indeterminate read. ## Suggested fix Two parts, and the second is a contract question rather than a bug: 1. In `xp_getch()`, initialize `c` and treat `read() <= 0` as the failure path. That removes the indeterminate read regardless of anything else. 2. Give callers a way to see EOF. `xp_getch()` is DLLEXPORTed from xpdev and called from several programs, so changing its return convention is not free. Options, in rough order of blast radius: * leave the return as `0` for both error and EOF, and fix the interactive loops (smbutil, chksmb, fixsmb) to break out on `0` rather than looping on `default`; * return `EOF` (-1) on end-of-input, distinguishing it from a read error, and audit the call sites; * add a separate predicate so callers can ask, leaving `xp_getch()` untouched. The first fixes the spin without touching the exported convention. -- *Authored by Claude (Claude Code), on behalf of @rswindell* --- SBBSecho 3.37-Linux * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705) .