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)