>> Regarding the suggestion of EBADF, it is my understanding that the kernel
>> must keep file descriptor 0 open (valid) even after a disconnect to
prevent
>> immediate FD reuse.
> either it's closed, or it's not.
> ncurses doesn't reopen it.
> Since ncurses doesn't reopen it, I don't see any reason why the dangling
> symlink would be relevant.

To clarify my earlier point regarding EBADF:

If the kernel were to simply close file descriptor 0 upon a disconnect, the
very next file opened by the process (or a library) would be assigned FD 0,
as it is the lowest available slot.
This would cause catastrophic confusion, as code expecting stdin would
suddenly be reading from a random file or socket.

Instead, the descriptor remains allocated to the process but points to a
defunct terminal (often following a SIGHUP).
This is why read() returns 0 (EOF) and errno remains 0, rather than
triggering EBADF.

We can detect this specific "ghost" state using isatty(STDIN_FILENO).
Upon disconnect, isatty will return 0 (false) because the underlying
terminal device is no longer associated with the descriptor, even though
the descriptor itself hasn't been "closed."
This approach doesn't work with a piped file though.

My mention of the dangling symlink was simply to illustrate this state: the
process holds a valid descriptor to a path/device that no longer exists in
the active filesystem.


On Sun, Mar 29, 2026 at 6:38 PM Klaas van Aarsen <[email protected]>
wrote:

> > If you're able to reproduce the problem, and modify the program to report
> > the errno value, then we can discuss whether the errno value matches the
> > expected behavior.
>
> I have run the requested tests to capture errno during both a pipe EOF and
> a forced SSH disconnect.
>
> Environment
> -----------
> Ubuntu 22.04.5 LTS in WSL2
> ncurses 6.3.20211021
>
> Test Program
> ------------
> #include <ncurses.h>
> #include <errno.h>
> #include <stdio.h>
>
> int main() {
>     FILE *f = fopen("trace.log", "w");
>     initscr();
>     halfdelay(5);
>     for (int i = 0; i < 100; ++i) {
>         errno = 0;
>         int ch = getch();
>         int err = errno;
>         fprintf(f, "getch: %d, errno: %d\n", ch, err);
>         fflush(f);
>     }
>     endwin();
>     fclose(f);
> }
>
>
> Scenario 1: Pipe EOF
> --------------------
> $ echo "hi" | ./a.out ; cat trace.log
>
> Results show getch: 104, 105, 10 followed by consistent getch: -1, errno:
> 0.
>
>
> Scenario 2: SSH Disconnect
> --------------------------
> 1. SSH into host.
> 2. Run ./a.out.
> 3. Hard-close the terminal window (sending no logout/exit).
> 4. Inspect log after 30 seconds.
>
> Trace Log Result:
> getch: 72, errno: 0   (H)
> getch: 105, errno: 0  (i)
> getch: -1, errno: 0
> getch: -1, errno: 0
> ... [continues until loop end] ...
> getch: -1, errno: 0
>
>
> Scenario 3: Manually closing stdin (FD 0)
> -----------------------------------------
> To test if getch() would report EBADF, I added close(0) inside the loop.
>
> Result: The loop completed instantly (ignoring the halfdelay timeout).
> getch() returned -1 for every iteration, but errno remained 0.
>
> This demonstrates that even when the file descriptor is explicitly invalid
> (EBADF condition), getch() does not pass that error information through to
> the caller.
>
>
> Conclusion
> ----------
> In all cases, ncurses reports ERR but the system does not report EBADF or
> any other error via errno.
> This suggests that the terminal disconnect is being treated as a
> persistent EOF (0) rather than a file descriptor error.
>
> On Sun, Mar 29, 2026 at 5:32 PM Thomas Dickey <[email protected]>
> wrote:
>
>> On Sun, Mar 29, 2026 at 12:15:45PM +0200, Klaas van Aarsen wrote:
>> > > man read(2)
>> > >       EBADF  fd is not a valid file descriptor or is not open for
>> reading.
>> > > Klaas is describing a condition which isn't in the manpage - ymmv.
>> >
>> > Regarding the suggestion of EBADF, it is my understanding that the
>> kernel
>> > must keep file descriptor 0 open (valid) even after a disconnect to
>> prevent
>> > immediate FD reuse.
>>
>> either it's closed, or it's not.
>>
>> ncurses doesn't reopen it.
>>
>> Since ncurses doesn't reopen it, I don't see any reason why the dangling
>> symlink would be relevant.
>>
>> Checking errno after the failed call would be more useful than making
>> a signal handler :-)
>>
>> > If read() were to return EBADF while the descriptor is still allocated
>> to
>> > the process, it would technically contradict the POSIX definition of
>> that
>> > error.
>>
>> If you're able to reproduce the problem, and modify the program to report
>> the errno value, then we can discuss whether the errno value matches the
>> expected behavior.
>>
>> > While the kernel could conceivably return an error like EIO or ENXIO to
>> > signal a "dead" device, treating a terminal disconnect as an "end of
>> > stream" (returning 0) appears to be the standard behavior for
>> > pseudo-terminals across most modern Unix-like systems.
>> > This ensures the stream is closed gracefully without releasing the
>> > descriptor slot prematurely.
>>
>> ncurses is using the read directly (stream usually refers to things
>> opened via fopen).
>>
>> initscr (used in this program) calls newterm
>>
>>         if (newterm(name, stdout, stdin) == NULL) {
>>
>> newterm copies the file descriptor:
>>
>>             SP_PARM->_ifd = fileno(_ifp);
>>
>> and after that, it ignores any buffering that the caller may have on that
>> file descriptor.  ncurses never closes that file descriptor.  If you got
>> EBADF, then that would indicate that the connection was closed (either
>> end of a connection can close it).
>>
>> > I appreciate the technical dialogue on this.
>> >
>> >
>> > On Sun, Mar 29, 2026 at 11:59 AM Thomas Dickey <
>> [email protected]>
>> > wrote:
>> >
>> > > On Sun, Mar 29, 2026 at 11:40:56AM +0200, Stian Skjelstad wrote:
>> > > > > Actually what you're telling us is that there's a defect in
>> (presumably
>> > > > Linux)
>> > > > > which should be documented in read(2).  Once you've gotten it
>> > > documented
>> > > > in
>> > > > >the proper place (Linux manpages), then others can use the
>> > > documentation.
>> > > >
>> > > > It is normal for read() to return 0, without an error when EOF is
>> > > reached.
>> > > > This is how you detect EOF/closed on sockets and files. Should TTY
>> behave
>> > > > differently?
>> > >
>> > > man read(2)
>> > >
>> > >        EBADF  fd is not a valid file descriptor or is not open for
>> reading.
>> > >
>> > > Klaas is describing a condition which isn't in the manpage - ymmv.
>> > >
>> > > --
>> > > Thomas E. Dickey <[email protected]>
>> > > https://invisible-island.net
>> > >
>>
>> --
>> Thomas E. Dickey <[email protected]>
>> https://invisible-island.net
>>
>

Reply via email to