Skip to content

Commit

Permalink
tty: handle corner cases on stdin and stdout
Browse files Browse the repository at this point in the history
epoll_ctl would generally return EPERM after redirecting standard
streams to /dev/null or closing them. It turns out that /dev/null and
/dev/full, among others, are not supported by epoll -- in particular
/dev/null is always write-ready, and /dev/full is always read-ready.

Confusingly, closing stdin, stdout, or stderr causes glibc to reopen
/dev/full for stdin and /dev/null for stdout and stderr.

Another thing to consider is that epoll does not support regular files
either, because linux consider those to always be read-write-ready
(independent of whether or not the disk will actually end up blocking).

We now handle these corner cases by using an eventfd to emulate these
unsupported file descriptors.
  • Loading branch information
Snaipe committed Jun 14, 2021
1 parent 2d859c4 commit f111c51
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 5 deletions.
11 changes: 10 additions & 1 deletion test/tty.t
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@

Allocate a PTY for the spacetime
$ bst --tty --mount devpts,/dev/pts,devpts,mode=620,ptmxmode=666 tty
/dev/pts/0
/dev/pts/0

Check that redirections still work

$ echo hello | bst --tty --mount devpts,/dev/pts,devpts,mode=620,ptmxmode=666 cat
hello
hello

$ bst --tty --mount devpts,/dev/pts,devpts,mode=620,ptmxmode=666 echo hello | cat
hello
53 changes: 49 additions & 4 deletions tty.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/file.h>
#include <sys/signalfd.h>
#include <sys/socket.h>
Expand Down Expand Up @@ -276,7 +277,8 @@ static int tty_handle_io(int epollfd, const struct epoll_event *ev, int fd, pid_
}

if ((inbound_handler.ready & (READ_READY | WRITE_READY)) == (READ_READY | WRITE_READY)) {
int read_fd = inbound_handler.fd;
/* inbound_handler.fd might contain our eventfd workaround */
int read_fd = STDIN_FILENO;
int write_fd = inbound_handler.peer_fd;

ssize_t copied = io_copy(write_fd, read_fd, &inbound_buffer);
Expand All @@ -302,7 +304,8 @@ static int tty_handle_io(int epollfd, const struct epoll_event *ev, int fd, pid_

if (outbound_handler.ready == (READ_READY | WRITE_READY)) {
int read_fd = outbound_handler.peer_fd;
int write_fd = outbound_handler.fd;
/* outbound_handler.fd might contain our eventfd workaround */
int write_fd = STDOUT_FILENO;

if (io_copy(write_fd, read_fd, &outbound_buffer) == -1) {
err(1, "copy tty -> stdout");
Expand Down Expand Up @@ -424,14 +427,56 @@ void tty_parent_setup(int epollfd, int socket)
event.data.ptr = &inbound_handler;

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
err(1, "epoll_ctl_add stdin");
if (errno != EPERM && errno != EBADF) {
err(1, "epoll_ctl_add stdin");
}
/* EPERM means the file descriptor does not support epoll. This can
happen if our caller closed stdin on us, which causes the libc to
open /dev/full O_WRONLY in its stead.
EBADF usually does not happen for the reason above, but I can
imagine that not all libcs might open /dev/full for us.
Devices and regular files never block and are always read-ready
(well, rather, it's not possible to know whether an IO operation
will wait on disk, and select() already reports regular files
as being always read-ready). Emulate that behaviour with an eventfd. */

int fd = eventfd(1, EFD_CLOEXEC);
if (fd == -1) {
err(1, "eventfd");
}

inbound_handler.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1) {
err(1, "epoll_ctl_add stdout eventfd fallback");
}
}

event.events = EPOLLOUT | EPOLLONESHOT;
event.data.ptr = &outbound_handler;

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, STDOUT_FILENO, &event) == -1) {
err(1, "epoll_ctl_add stdout");
if (errno != EPERM && errno != EBADF) {
err(1, "epoll_ctl_add stdout");
}
/* We ignore EPERM for the same reasons as for stdin. The libc opens
/dev/null if our caller closed stdout.
EBADF has the same treatment as stdin, too.
Devices and regular files never block, and are always write-ready.
Emulate that behaviour with an eventfd. */

int fd = eventfd(1, EFD_CLOEXEC);
if (fd == -1) {
err(1, "eventfd");
}

outbound_handler.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1) {
err(1, "epoll_ctl_add stdout eventfd fallback");
}
}

if (info.stdinIsatty) {
Expand Down

0 comments on commit f111c51

Please sign in to comment.