pty: childs don't always react on close(2)
From: Dirk Gouders
Date: Fri Dec 05 2025 - 16:38:18 EST
Hi,
chances are high, that I am doing a stupid mistake, because experienced
programmers already would have noticed problems with ptys.
But if you have the time, perhaps you could help me undestand the
following problem:
I am working on a program (a terminal pager [1]) that works with
ptys to allow users to have several manual pages open and reload them
when terminals are resized.
I now noticed, that a close on a pty file descriptor doesn't cause the
child process to exit and a waitpid(2) hangs forever or gets interrupted
(when used without WNOHANG) -- it seems the child doesn't get noticed
about the close(2) on the other end.
Curious is that I notice this only if more than one ptys are in use --
with a single one everything is OK (see attached sample program and test
it with only one pty).
I would be very glad if you could tell me if I am correct to expect that
if I close(2) a pty file descriptor the other end should notice that and
I could assume a waitpid(2) to succeed.
And perhaps, you could tell me which mistake I am doing.
I prepared a small test-case that reproduces the problem, at least here.
If that program is modified to work with just one child, everything
works as expected, when the second child is used, the waitpid() hangs...
Apologies, if this is not the right place and if I should have asked
somewhere else.
Regards,
Dirk
[1] https://github.com/dgouders/lsp
/*
* Test closing file descriptors opened via forkpty() when not all data has been
* read. A following waitpid() blocks, when we opened two childs and try to
* close the file descriptor and then waitpid() for that child...
*/
#include <stdlib.h>
#include <stdio.h>
#include <pty.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdint.h>
#define READ_SIZE 4096
/*
* Set PAGER variables and start a man(1) process.
*/
void do_child(void);
void do_child()
{
char *e_argv[3] = {"man", "groff_ms", NULL};
putenv("PAGER=cat");
putenv("MANPAGER=cat");
execvp("man", e_argv);
}
int main()
{
unsigned char read_buffer[READ_SIZE];
int pty_fd;
int pty_fd1;
int wstatus;
pid_t child_pid;
pid_t child_pid1;
pid_t ret_pid;
ssize_t ret;
/*
* Start a child to send us a manual page.
*/
child_pid = forkpty(&pty_fd, NULL, NULL, NULL);
if (child_pid == -1) {
fprintf(stderr, "forkpty(): %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (child_pid == 0)
do_child();
printf("child_pid = %jd\n", (intmax_t) child_pid);
memset(read_buffer, '\0', READ_SIZE);
ret = read(pty_fd, read_buffer, READ_SIZE);
if (ret == -1) {
fprintf(stderr, "read(): %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("%s\n", read_buffer);
printf("%ld bytes read.\n", ret);
/*
* Start another child to send us a manual page.
*/
child_pid1 = forkpty(&pty_fd1, NULL, NULL, NULL);
if (child_pid1 == -1) {
fprintf(stderr, "forkpty(): %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
if (child_pid1 == 0)
do_child();
printf("child_pid1 = %jd\n", (intmax_t) child_pid1);
memset(read_buffer, '\0', READ_SIZE);
ret = read(pty_fd1, read_buffer, READ_SIZE);
if (ret == -1) {
fprintf(stderr, "read(): %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("%s\n", read_buffer);
printf("%ld bytes read.\n", ret);
close(pty_fd);
ret_pid = waitpid(child_pid, &wstatus, 0);
printf("ret_pid = %jd\n", (intmax_t) ret_pid);
exit(EXIT_SUCCESS);
}