From eb7d0ad0fed520ecf66ef39075f404e1c7e37a51 Mon Sep 17 00:00:00 2001 From: Karel Zak Date: Thu, 17 Aug 2017 16:09:52 +0200 Subject: su: add PTY support Signed-off-by: Karel Zak --- login-utils/Makemodule.am | 3 + login-utils/su-common.c | 471 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 429 insertions(+), 45 deletions(-) (limited to 'login-utils') diff --git a/login-utils/Makemodule.am b/login-utils/Makemodule.am index 67a0c98ec..3fdabe468 100644 --- a/login-utils/Makemodule.am +++ b/login-utils/Makemodule.am @@ -146,6 +146,9 @@ su_LDADD = $(LDADD) libcommon.la -lpam if HAVE_LINUXPAM su_LDADD += -lpam_misc endif +if HAVE_UTIL +su_LDADD += -lutil +endif endif # BUILD_SU diff --git a/login-utils/su-common.c b/login-utils/su-common.c index 265f5fd62..7725d1558 100644 --- a/login-utils/su-common.c +++ b/login-utils/su-common.c @@ -3,7 +3,7 @@ * * Copyright (C) 1992-2006 Free Software Foundation, Inc. * Copyright (C) 2012 SUSE Linux Products GmbH, Nuernberg - * Copyright (C) 2016 Karel Zak + * Copyright (C) 2016-2017 Karel Zak * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free @@ -37,6 +37,14 @@ #include #include +#if defined(HAVE_LIBUTIL) && defined(HAVE_PTY_H) && defined(HAVE_SYS_SIGNALFD_H) +# include +# include +# include +# include "all-io.h" +# define USE_PTY +#endif + #include "err.h" #include @@ -66,6 +74,7 @@ UL_DEBUG_DEFINE_MASKNAMES(su) = UL_DEBUG_EMPTY_MASKNAMES; #define SU_DEBUG_LOG (1 << 5) #define SU_DEBUG_MISC (1 << 6) #define SU_DEBUG_SIG (1 << 7) +#define SU_DEBUG_PTY (1 << 8) #define SU_DEBUG_ALL 0xFFFF #define DBG(m, x) __UL_DBG(su, SU_DEBUG_, m, x) @@ -126,7 +135,16 @@ struct su_context { pid_t child; /* fork() baby */ struct sigaction oldact[SIGNALS_IDX_COUNT]; /* original sigactions indexed by SIG*_IDX */ - + sigset_t oldsig; /* original signal mask */ + +#ifdef USE_PTY + struct termios stdin_attrs; /* stdin and slave terminal runtime attributes */ + int pty_master; + int pty_slave; + int pty_sigfd; /* signalfd() */ + int poll_timeout; + struct winsize win; /* terminal window size */ +#endif unsigned int runuser :1, /* flase=su, true=runuser */ runuser_uopt :1, /* runuser -u specified */ isterm :1, /* is stdin terminal? */ @@ -164,6 +182,383 @@ static void init_tty(struct su_context *su) get_terminal_name(NULL, &su->tty_name, &su->tty_number); } +static int wait_for_child(struct su_context *su) +{ + pid_t pid = (pid_t) -1;; + int status = 0; + + if (su->child != (pid_t) -1) { + DBG(SIG, ul_debug("waiting for child [%d]...", su->child)); + for (;;) { + pid = waitpid(su->child, &status, WUNTRACED); + + if (pid != (pid_t) - 1 && WIFSTOPPED(status)) { + kill(getpid(), SIGSTOP); + /* once we get here, we must have resumed */ + kill(pid, SIGCONT); + } else + break; + } + } + if (pid != (pid_t) -1) { + if (WIFSIGNALED(status)) { + fprintf(stderr, "%s%s\n", + strsignal(WTERMSIG(status)), + WCOREDUMP(status) ? _(" (core dumped)") + : ""); + status = WTERMSIG(status) + 128; + } else + status = WEXITSTATUS(status); + + DBG(SIG, ul_debug("child %d is dead", su->child)); + su->child = (pid_t) -1; /* Don't use the PID anymore! */ + } else if (caught_signal) + status = caught_signal + 128; + else + status = 1; + + DBG(SIG, ul_debug("status=%d", status)); + return status; +} + + +#ifdef USE_PTY +static void pty_init_slave(struct su_context *su) +{ + DBG(PTY, ul_debug("initialize slave")); + + ioctl(su->pty_slave, TIOCSCTTY, 0); + close(su->pty_master); + + dup2(su->pty_slave, STDIN_FILENO); + dup2(su->pty_slave, STDOUT_FILENO); + dup2(su->pty_slave, STDERR_FILENO); + + close(su->pty_slave); + close(su->pty_sigfd); + + su->pty_slave = -1; + su->pty_master = -1; + su->pty_sigfd = -1; + + sigprocmask(SIG_SETMASK, &su->oldsig, NULL); + + DBG(PTY, ul_debug("... initialize slave done")); +} + +static void pty_create(struct su_context *su) +{ + struct termios slave_attrs; + int rc; + + if (su->isterm) { + DBG(PTY, ul_debug("create for terminal")); + struct winsize win; + + /* original setting of the current terminal */ + if (tcgetattr(STDIN_FILENO, &su->stdin_attrs) != 0) + err(EXIT_FAILURE, _("failed to get terminal attributes")); + + /* reuse the current terminal setting for slave */ + slave_attrs = su->stdin_attrs; + cfmakeraw(&slave_attrs); + + ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&su->win); + + /* create master+slave */ + rc = openpty(&su->pty_master, &su->pty_slave, NULL, &slave_attrs, &win); + + } else { + DBG(PTY, ul_debug("create for non-terminal")); + rc = openpty(&su->pty_master, &su->pty_slave, NULL, NULL, NULL); + + /* set slave attributes */ + if (rc < 0) { + tcgetattr(su->pty_slave, &slave_attrs); + cfmakeraw(&slave_attrs); + tcsetattr(su->pty_slave, TCSANOW, &slave_attrs); + } + } + + if (rc < 0) + err(EXIT_FAILURE, _("failed to create pseudo-terminal")); + + DBG(PTY, ul_debug("pty setup done [master=%d, slave=%d]", su->pty_master, su->pty_slave)); +} + +static void pty_cleanup(struct su_context *su) +{ + if (su->pty_master == -1) + return; + + DBG(PTY, ul_debug("cleanup")); + if (su->isterm) + tcsetattr(STDIN_FILENO, TCSADRAIN, &su->stdin_attrs); +} + +static int write_output(char *obuf, ssize_t bytes) +{ + DBG(PTY, ul_debug(" writing output")); + + if (write_all(STDOUT_FILENO, obuf, bytes)) { + DBG(PTY, ul_debug(" writing output *failed*")); + warn(_("write failed")); + return -errno; + } + + return 0; +} + +static int write_to_child(struct su_context *su, + char *buf, size_t bufsz) +{ + return write_all(su->pty_master, buf, bufsz); +} + +/* + * The su(1) is usually faster than shell, so it's a good idea to wait until + * the previous message has been already read by shell from slave before we + * write to master. This is necessary especially for EOF situation when we can + * send EOF to master before shell is fully initialized, to workaround this + * problem we wait until slave is empty. For example: + * + * echo "date" | su + * + * Unfortunately, the child (usually shell) can ignore stdin at all, so we + * don't wait forever to avoid dead locks... + * + * Note that su --pty is primarily designed for interactive sessions as it + * maintains master+slave tty stuff within the session. Use pipe to write to + * su(1) and assume non-interactive (tee-like) behavior is NOT well + * supported. + */ +static void write_eof_to_child(struct su_context *su) +{ + unsigned int tries = 0; + struct pollfd fds[] = { + { .fd = su->pty_slave, .events = POLLIN } + }; + char c = DEF_EOF; + + DBG(PTY, ul_debug(" waiting for empty slave")); + while (poll(fds, 1, 10) == 1 && tries < 8) { + DBG(PTY, ul_debug(" slave is not empty")); + xusleep(250000); + tries++; + } + if (tries < 8) + DBG(PTY, ul_debug(" slave is empty now")); + + DBG(PTY, ul_debug(" sending EOF to master")); + write_to_child(su, &c, sizeof(char)); +} + +static int pty_handle_io(struct su_context *su, int fd, int *eof) +{ + char buf[BUFSIZ]; + ssize_t bytes; + + DBG(PTY, ul_debug("%d FD active", fd)); + *eof = 0; + + /* read from active FD */ + bytes = read(fd, buf, sizeof(buf)); + if (bytes < 0) { + if (errno == EAGAIN || errno == EINTR) + return 0; + return -errno; + } + + if (bytes == 0) { + *eof = 1; + return 0; + } + + /* from stdin (user) to command */ + if (fd == STDIN_FILENO) { + DBG(PTY, ul_debug(" stdin --> master %zd bytes", bytes)); + + if (write_to_child(su, buf, bytes)) { + warn(_("write failed")); + return -errno; + } + /* without sync write_output() will write both input & + * shell output that looks like double echoing */ + fdatasync(su->pty_master); + + /* from command (master) to stdout */ + } else if (fd == su->pty_master) { + DBG(PTY, ul_debug(" master --> stdout %zd bytes", bytes)); + write_output(buf, bytes); + } + + return 0; +} + +static int pty_handle_signal(struct su_context *su, int fd) +{ + struct signalfd_siginfo info; + ssize_t bytes; + + DBG(SIG, ul_debug("signal FD %d active", fd)); + + bytes = read(fd, &info, sizeof(info)); + if (bytes != sizeof(info)) { + if (bytes < 0 && (errno == EAGAIN || errno == EINTR)) + return 0; + return -errno; + } + + switch (info.ssi_signo) { + case SIGCHLD: + DBG(SIG, ul_debug(" get signal SIGCHLD")); + wait_for_child(su); + su->poll_timeout = 10; + return 0; + case SIGWINCH: + DBG(SIG, ul_debug(" get signal SIGWINCH")); + if (su->isterm) { + ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&su->win); + ioctl(su->pty_slave, TIOCSWINSZ, (char *)&su->win); + } + break; + case SIGTERM: + /* fallthrough */ + case SIGINT: + /* fallthrough */ + case SIGQUIT: + DBG(SIG, ul_debug(" get signal SIG{TERM,INT,QUIT}")); + caught_signal = info.ssi_signo; + /* Child termination is going to generate SIGCHILD (see above) */ + kill(su->child, SIGTERM); + break; + default: + abort(); + } + + return 0; +} + +static void pty_proxy_master(struct su_context *su) +{ + sigset_t ourset; + int rc = 0, ret, ignore_stdin = 0, eof = 0; + enum { + POLLFD_SIGNAL = 0, + POLLFD_MASTER, + POLLFD_STDIN /* optional; keep it last, see ignore_stdin */ + + }; + struct pollfd pfd[] = { + [POLLFD_SIGNAL] = { .fd = -1, .events = POLLIN | POLLERR | POLLHUP }, + [POLLFD_MASTER] = { .fd = su->pty_master, .events = POLLIN | POLLERR | POLLHUP }, + [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP } + }; + + /* for PTY mode we use signalfd + * + * TODO: script(1) initializes this FD before fork, good or bad idea? + */ + sigfillset(&ourset); + if (sigprocmask(SIG_BLOCK, &ourset, &su->oldsig)) { + warn(_("cannot block signals")); + caught_signal = true; + return; + } + + sigemptyset(&ourset); + sigaddset(&ourset, SIGCHLD); + sigaddset(&ourset, SIGWINCH); + sigaddset(&ourset, SIGALRM); + sigaddset(&ourset, SIGTERM); + sigaddset(&ourset, SIGINT); + sigaddset(&ourset, SIGQUIT); + + if ((su->pty_sigfd = signalfd(-1, &ourset, SFD_CLOEXEC)) < 0) { + warn(("cannot create signal file descriptor")); + caught_signal = true; + return; + } + + pfd[POLLFD_SIGNAL].fd = su->pty_sigfd; + su->poll_timeout = -1; + + while (!caught_signal) { + size_t i; + int errsv; + + DBG(PTY, ul_debug("calling poll()")); + + /* wait for input or signal */ + ret = poll(pfd, ARRAY_SIZE(pfd) - ignore_stdin, su->poll_timeout); + errsv = errno; + DBG(PTY, ul_debug("poll() rc=%d", ret)); + + if (ret < 0) { + if (errsv == EAGAIN) + continue; + warn(_("poll failed")); + break; + } + if (ret == 0) { + DBG(PTY, ul_debug("leaving poll() loop [timeout=%d]", su->poll_timeout)); + break; + } + + for (i = 0; i < ARRAY_SIZE(pfd) - ignore_stdin; i++) { + rc = 0; + + if (pfd[i].revents == 0) + continue; + + DBG(PTY, ul_debug(" active pfd[%s].fd=%d %s %s %s", + i == POLLFD_STDIN ? "stdin" : + i == POLLFD_MASTER ? "master" : + i == POLLFD_SIGNAL ? "signal" : "???", + pfd[i].fd, + pfd[i].revents & POLLIN ? "POLLIN" : "", + pfd[i].revents & POLLHUP ? "POLLHUP" : "", + pfd[i].revents & POLLERR ? "POLLERR" : "")); + switch (i) { + case POLLFD_STDIN: + case POLLFD_MASTER: + /* data */ + if (pfd[i].revents & POLLIN) + rc = pty_handle_io(su, pfd[i].fd, &eof); + /* EOF maybe detected by two ways: + * A) poll() return POLLHUP event after close() + * B) read() returns 0 (no data) */ + if ((pfd[i].revents & POLLHUP) || eof) { + DBG(PTY, ul_debug(" ignore FD")); + pfd[i].fd = -1; + /* according to man poll() set FD to -1 can't be used to ignore + * STDIN, so let's remove the FD from pool at all */ + if (i == POLLFD_STDIN) { + ignore_stdin = 1; + write_eof_to_child(su); + DBG(PTY, ul_debug(" ignore STDIN")); + } + } + continue; + case POLLFD_SIGNAL: + rc = pty_handle_signal(su, pfd[i].fd); + break; + } + if (rc) + break; + } + } + + DBG(PTY, ul_debug("poll() done [signal=%d, rc=%d]", caught_signal, rc)); +} +#else +# define pty_create +# define pty_cleanup +# define pty_init_slave +# define pty_proxy_master +#endif /* USE_PTY */ + + /* Log the fact that someone has run su to the user given by PW; if SUCCESSFUL is true, they gave the correct password, etc. */ @@ -333,45 +728,6 @@ static void supam_open_session(struct su_context *su) su->pam_has_session = 1; } -static int wait_for_child(struct su_context *su) -{ - pid_t pid = (pid_t) -1;; - int status = 0; - - if (su->child != (pid_t) -1) { - DBG(SIG, ul_debug("waiting for child [%d]...", su->child)); - for (;;) { - pid = waitpid(su->child, &status, WUNTRACED); - - if (pid != (pid_t) - 1 && WIFSTOPPED(status)) { - kill(getpid(), SIGSTOP); - /* once we get here, we must have resumed */ - kill(pid, SIGCONT); - } else - break; - } - } - if (pid != (pid_t) -1) { - if (WIFSIGNALED(status)) { - fprintf(stderr, "%s%s\n", - strsignal(WTERMSIG(status)), - WCOREDUMP(status) ? _(" (core dumped)") - : ""); - status = WTERMSIG(status) + 128; - } else - status = WEXITSTATUS(status); - - DBG(SIG, ul_debug("child %d is dead", su->child)); - su->child = (pid_t) -1; /* Don't use the PID anymore! */ - } else if (caught_signal) - status = caught_signal + 128; - else - status = 1; - - DBG(SIG, ul_debug("status=%d", status)); - return status; -} - static void parent_setup_signals(struct su_context *su) { sigset_t ourset; @@ -384,7 +740,7 @@ static void parent_setup_signals(struct su_context *su) DBG(SIG, ul_debug("initialize signals")); sigfillset(&ourset); - if (sigprocmask(SIG_BLOCK, &ourset, NULL)) { + if (sigprocmask(SIG_BLOCK, &ourset, &su->oldsig)) { warn(_("cannot block signals")); caught_signal = true; } @@ -449,9 +805,17 @@ static void create_watching_parent(struct su_context *su) DBG(MISC, ul_debug("forking...")); + if (su->pty) + /* create master and slave terminals */ + pty_create(su); + + fflush(stdout); /* ??? */ + switch ((int) (su->child = fork())) { case -1: /* error */ supam_cleanup(su, PAM_ABORT); + if (su->pty) + pty_cleanup(su); err(EXIT_FAILURE, _("cannot create child process")); break; @@ -471,7 +835,10 @@ static void create_watching_parent(struct su_context *su) if (chdir("/") != 0) warn(_("cannot change directory to %s"), "/"); - parent_setup_signals(su); + if (su->pty) + pty_proxy_master(su); + else + parent_setup_signals(su); /* * Wait for child @@ -523,6 +890,8 @@ static void create_watching_parent(struct su_context *su) kill(getpid(), caught_signal); } + if (su->pty) + pty_cleanup(su); DBG(MISC, ul_debug("exiting [rc=%d]", status)); exit(status); } @@ -809,7 +1178,12 @@ int su_main(int argc, char **argv, int mode) .conv = { supam_conv, NULL }, .runuser = (mode == RUNUSER_MODE ? 1 : 0), .change_environment = 1, - .new_user = DEFAULT_USER + .new_user = DEFAULT_USER, +#ifdef USE_PTY + .pty_master = -1, + .pty_slave = -1, + .pty_sigfd = -1, +#endif }, *su = &_su; int optc; @@ -884,7 +1258,11 @@ int su_main(int argc, char **argv, int mode) break; case 'P': +#ifdef USE_PTY su->pty = 1; +#else + err(EXIT_FAILURE, _("--pty is not implemented")); +#endif break; case 's': @@ -1008,9 +1386,12 @@ int su_main(int argc, char **argv, int mode) /* Now we're in the child. */ change_identity(su->pwd); - if (!su->same_session) + if (!su->same_session || su->pty) setsid(); + if (su->pty) + pty_init_slave(su); + /* Set environment after pam_open_session, which may put KRB5CCNAME into the pam_env, etc. */ -- cgit v1.2.3-55-g7522