lib/pty: Put master PTY into non-blocking mode and buffer its output to avoid deadlock

If we filled the script->child buffer before the child had a chance to read any
input, we'd sleep forever in write_all(pty->master), and the child would sleep
forever in write(1<pty->slave>)

By putting the master PTY in non-blocking mode, we can poll(pty->master,
POLLOUT) and keep supplying more data as the child reads from the buffer

Fixes Debian bug #1003095

Signed-off-by: Karel Zak <kzak@redhat.com>
This commit is contained in:
наб 2022-04-12 16:25:14 +02:00 committed by su-fang
parent 6d8fbabbbb
commit f358fe6105
2 changed files with 110 additions and 23 deletions

View File

@ -81,6 +81,13 @@ struct ul_pty {
struct timeval next_callback_time; struct timeval next_callback_time;
struct ul_pty_child_buffer {
struct ul_pty_child_buffer *next;
char buf[BUFSIZ];
size_t size, cursor;
unsigned int final_input:1; /* drain child before writing */
} *child_buffer_head, *child_buffer_tail, *free_buffers;
unsigned int isterm:1, /* is stdin terminal? */ unsigned int isterm:1, /* is stdin terminal? */
slave_echo:1; /* keep ECHO on pty slave */ slave_echo:1; /* keep ECHO on pty slave */
}; };

View File

@ -69,6 +69,15 @@ struct ul_pty *ul_new_pty(int is_stdin_tty)
void ul_free_pty(struct ul_pty *pty) void ul_free_pty(struct ul_pty *pty)
{ {
struct ul_pty_child_buffer *hd;
while((hd = pty->child_buffer_head)) {
pty->child_buffer_head = hd->next;
free(hd);
}
while((hd = pty->free_buffers)) {
pty->free_buffers = hd->next;
free(hd);
}
free(pty); free(pty);
} }
@ -182,7 +191,7 @@ int ul_pty_setup(struct ul_pty *pty)
cfmakeraw(&attrs); cfmakeraw(&attrs);
tcsetattr(STDIN_FILENO, TCSANOW, &attrs); tcsetattr(STDIN_FILENO, TCSANOW, &attrs);
} else { } else {
DBG(SETUP, ul_debugobj(pty, "create for non-terminal")); DBG(SETUP, ul_debugobj(pty, "create for non-terminal"));
rc = openpty(&pty->master, &pty->slave, NULL, NULL, NULL); rc = openpty(&pty->master, &pty->slave, NULL, NULL, NULL);
if (rc) if (rc)
@ -198,6 +207,8 @@ int ul_pty_setup(struct ul_pty *pty)
tcsetattr(pty->slave, TCSANOW, &attrs); tcsetattr(pty->slave, TCSANOW, &attrs);
} }
fcntl(pty->master, F_SETFL, O_NONBLOCK);
sigfillset(&ourset); sigfillset(&ourset);
if (sigprocmask(SIG_BLOCK, &ourset, NULL)) { if (sigprocmask(SIG_BLOCK, &ourset, NULL)) {
rc = -errno; rc = -errno;
@ -290,9 +301,27 @@ static int write_output(char *obuf, ssize_t bytes)
return 0; return 0;
} }
static int write_to_child(struct ul_pty *pty, char *buf, size_t bufsz) static int schedule_child_write(struct ul_pty *pty, char *buf, size_t bufsz, int final)
{ {
return write_all(pty->master, buf, bufsz); struct ul_pty_child_buffer *stash;
if (pty->free_buffers) {
stash = pty->free_buffers;
pty->free_buffers = stash->next;
memset(stash, 0, sizeof(*stash));
} else
stash = calloc(1, sizeof(*stash));
if (!stash)
return -1;
memcpy(stash->buf, buf, bufsz);
stash->size = bufsz;
stash->final_input = final ? 1 : 0;
if (pty->child_buffer_head)
pty->child_buffer_tail = pty->child_buffer_tail->next = stash;
else
pty->child_buffer_head = pty->child_buffer_tail = stash;
return 0;
} }
/* /*
@ -311,16 +340,13 @@ static int write_to_child(struct ul_pty *pty, char *buf, size_t bufsz)
* maintains master+slave tty stuff within the session. Use pipe to write to * maintains master+slave tty stuff within the session. Use pipe to write to
* pty and assume non-interactive (tee-like) behavior is NOT well supported. * pty and assume non-interactive (tee-like) behavior is NOT well supported.
*/ */
void ul_pty_write_eof_to_child(struct ul_pty *pty) static void drain_child_buffers(struct ul_pty *pty)
{ {
unsigned int tries = 0; unsigned int tries = 0;
struct pollfd fds[] = { struct pollfd fd = { .fd = pty->slave, .events = POLLIN };
{ .fd = pty->slave, .events = POLLIN }
};
char c = DEF_EOF;
DBG(IO, ul_debugobj(pty, " waiting for empty slave")); DBG(IO, ul_debugobj(pty, " waiting for empty slave"));
while (poll(fds, 1, 10) == 1 && tries < 8) { while (poll(&fd, 1, 10) == 1 && tries < 8) {
DBG(IO, ul_debugobj(pty, " slave is not empty")); DBG(IO, ul_debugobj(pty, " slave is not empty"));
xusleep(250000); xusleep(250000);
tries++; tries++;
@ -329,7 +355,53 @@ void ul_pty_write_eof_to_child(struct ul_pty *pty)
DBG(IO, ul_debugobj(pty, " slave is empty now")); DBG(IO, ul_debugobj(pty, " slave is empty now"));
DBG(IO, ul_debugobj(pty, " sending EOF to master")); DBG(IO, ul_debugobj(pty, " sending EOF to master"));
write_to_child(pty, &c, sizeof(char)); }
static int flush_child_buffers(struct ul_pty *pty, int *anything)
{
int ret = 0, any = 0;
while (pty->child_buffer_head) {
struct ul_pty_child_buffer *hd = pty->child_buffer_head;
if(hd->final_input)
drain_child_buffers(pty);
DBG(IO, ul_debugobj(hd, " stdin --> master trying %zu bytes", hd->size - hd->cursor));
ssize_t ret = write(pty->master, hd->buf + hd->cursor, hd->size - hd->cursor);
if (ret == -1) {
DBG(IO, ul_debugobj(hd, " EAGAIN"));
if (!(errno == EINTR || errno == EAGAIN))
ret = -errno;
goto out;
}
DBG(IO, ul_debugobj(hd, " wrote %zd", ret));
any = 1;
hd->cursor += ret;
if (hd->cursor == hd->size) {
pty->child_buffer_head = hd->next;
if(!hd->next)
pty->child_buffer_tail = NULL;
hd->next = pty->free_buffers;
pty->free_buffers = hd;
}
}
out:
/* without sync write_output() will write both input &
* shell output that looks like double echoing */
if (any)
fdatasync(pty->master);
if (anything)
*anything = any;
return ret;
}
void ul_pty_write_eof_to_child(struct ul_pty *pty)
{
char c = DEF_EOF;
schedule_child_write(pty, &c, sizeof(char), 1);
} }
static int mainloop_callback(struct ul_pty *pty) static int mainloop_callback(struct ul_pty *pty)
@ -362,7 +434,7 @@ static int handle_io(struct ul_pty *pty, int fd, int *eof)
/* read from active FD */ /* read from active FD */
bytes = read(fd, buf, sizeof(buf)); bytes = read(fd, buf, sizeof(buf));
sigprocmask(SIG_BLOCK, &set, NULL); sigprocmask(SIG_BLOCK, &set, NULL);
if (bytes < 0) { if (bytes == -1) {
if (errno == EAGAIN || errno == EINTR) if (errno == EAGAIN || errno == EINTR)
return 0; return 0;
return -errno; return -errno;
@ -375,15 +447,11 @@ static int handle_io(struct ul_pty *pty, int fd, int *eof)
/* from stdin (user) to command */ /* from stdin (user) to command */
if (fd == STDIN_FILENO) { if (fd == STDIN_FILENO) {
DBG(IO, ul_debugobj(pty, " stdin --> master %zd bytes", bytes)); DBG(IO, ul_debugobj(pty, " stdin --> master %zd bytes queued", bytes));
if (write_to_child(pty, buf, bytes)) if (schedule_child_write(pty, buf, bytes, 0))
return -errno; return -errno;
/* without sync write_output() will write both input &
* shell output that looks like double echoing */
fdatasync(pty->master);
/* from command (master) to stdout */ /* from command (master) to stdout */
} else if (fd == pty->master) { } else if (fd == pty->master) {
DBG(IO, ul_debugobj(pty, " master --> stdout %zd bytes", bytes)); DBG(IO, ul_debugobj(pty, " master --> stdout %zd bytes", bytes));
@ -567,6 +635,11 @@ int ul_pty_proxy_master(struct ul_pty *pty)
} else } else
timeout = pty->poll_timeout; timeout = pty->poll_timeout;
if (pty->child_buffer_head)
pfd[POLLFD_MASTER].events |= POLLOUT;
else
pfd[POLLFD_MASTER].events &= ~POLLOUT;
/* wait for input, signal or timeout */ /* wait for input, signal or timeout */
DBG(IO, ul_debugobj(pty, "calling poll() [timeout=%dms]", timeout)); DBG(IO, ul_debugobj(pty, "calling poll() [timeout=%dms]", timeout));
ret = poll(pfd, ARRAY_SIZE(pfd), timeout); ret = poll(pfd, ARRAY_SIZE(pfd), timeout);
@ -600,23 +673,30 @@ int ul_pty_proxy_master(struct ul_pty *pty)
if (pfd[i].revents == 0) if (pfd[i].revents == 0)
continue; continue;
DBG(IO, ul_debugobj(pty, " active pfd[%s].fd=%d %s %s %s %s", DBG(IO, ul_debugobj(pty, " active pfd[%s].fd=%d %s %s %s %s %s",
i == POLLFD_STDIN ? "stdin" : i == POLLFD_STDIN ? "stdin" :
i == POLLFD_MASTER ? "master" : i == POLLFD_MASTER ? "master" :
i == POLLFD_SIGNAL ? "signal" : "???", i == POLLFD_SIGNAL ? "signal" : "???",
pfd[i].fd, pfd[i].fd,
pfd[i].revents & POLLIN ? "POLLIN" : "", pfd[i].revents & POLLIN ? "POLLIN" : "",
pfd[i].revents & POLLOUT ? "POLLOUT" : "",
pfd[i].revents & POLLHUP ? "POLLHUP" : "", pfd[i].revents & POLLHUP ? "POLLHUP" : "",
pfd[i].revents & POLLERR ? "POLLERR" : "", pfd[i].revents & POLLERR ? "POLLERR" : "",
pfd[i].revents & POLLNVAL ? "POLLNVAL" : "")); pfd[i].revents & POLLNVAL ? "POLLNVAL" : ""));
if (i == POLLFD_SIGNAL) if (i == POLLFD_SIGNAL)
rc = handle_signal(pty, pfd[i].fd); rc = handle_signal(pty, pfd[i].fd);
else if (pfd[i].revents & POLLIN) else {
rc = handle_io(pty, pfd[i].fd, &eof); /* data */ if (pfd[i].revents & POLLIN)
rc = handle_io(pty, pfd[i].fd, &eof); /* data */
if (pfd[i].revents & POLLOUT) /* i == POLLFD_MASTER */
rc = flush_child_buffers(pty, NULL);
}
if (rc) { if (rc) {
ul_pty_write_eof_to_child(pty); ul_pty_write_eof_to_child(pty);
for (int anything = 1; anything;)
flush_child_buffers(pty, &anything);
break; break;
} }
@ -631,11 +711,11 @@ int ul_pty_proxy_master(struct ul_pty *pty)
*/ */
if ((pfd[i].revents & POLLHUP) || (pfd[i].revents & POLLNVAL) || eof) { if ((pfd[i].revents & POLLHUP) || (pfd[i].revents & POLLNVAL) || eof) {
DBG(IO, ul_debugobj(pty, " ignore FD")); DBG(IO, ul_debugobj(pty, " ignore FD"));
pfd[i].fd = -1;
if (i == POLLFD_STDIN) { if (i == POLLFD_STDIN) {
pfd[i].fd = -1;
ul_pty_write_eof_to_child(pty); ul_pty_write_eof_to_child(pty);
DBG(IO, ul_debugobj(pty, " ignore STDIN")); } else /* i == POLLFD_MASTER */
} pfd[i].revents &= ~POLLIN;
} }
} }
if (rc) if (rc)