2 * Run the login shell or command as the given user in a new pty to prevent
3 * terminal injection attacks.
5 * Copyright (C) 2016-2021 Simon Ruderich
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
36 #include <sys/ioctl.h>
37 #include <sys/types.h>
42 /* Default PATH for new process.*/
43 #ifndef PTYAS_DEFAULT_PATH
44 /* Default user PATH from Debian's /etc/profile, change as needed. */
45 # define PTYAS_DEFAULT_PATH "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
49 static void die(const char *s) {
53 static void die_fmt(const char *fmt, ...) {
57 vfprintf(stderr, fmt, ap);
63 static void open_pty_or_die(int *pty_master, int *pty_slave, uid_t uid) {
66 *pty_master = posix_openpt(O_RDWR | O_NOCTTY);
67 if (*pty_master == -1) {
70 slave_path = ptsname(*pty_master);
74 if (grantpt(*pty_master) != 0) {
77 if (unlockpt(*pty_master) != 0) {
81 *pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
82 if (*pty_slave == -1) {
83 die("open slave tty");
86 * The user must be able to write to the new TTY. Normally grantpt() would
87 * do this for us, but we don't trust the user and thus don't want to pass
88 * the pty_master to a process running under that uid.
90 if (chown(slave_path, uid, (gid_t)-1) != 0) {
91 die("chown slave tty");
95 static void close_or_die(int fd) {
100 static void dup2_or_die(int oldfd, int newfd) {
101 if (dup2(oldfd, newfd) != newfd) {
105 static int snprintf_or_assert(char *str, size_t size, const char *format, ...) {
109 va_start(ap, format);
110 ret = vsnprintf(str, size, format, ap);
111 assert(size <= (size_t)INT_MAX);
112 assert(ret < (int)size); /* assert output fit into buffer */
118 static void drop_privileges_or_die(uid_t uid, gid_t gid) {
119 /* Drop all supplementary group IDs. */
122 /* FreeBSD uses the first gid to set the egid of the process. */
124 if (setgroups(1, &egid) != 0) {
127 if (getgroups(1, &egid) != 1) {
128 die_fmt("failed to drop all supplementary groups\n");
131 die_fmt("failed to drop all supplementary groups (egid): %d %d\n",
136 if (setgroups(0, NULL) != 0) {
139 if (getgroups(0, NULL) != 0) {
140 die_fmt("failed to drop all supplementary groups\n");
144 /* Dropping groups may require privileges, do that first. */
145 if (setresgid(gid, gid, gid) != 0) {
148 if (setresuid(uid, uid, uid) != 0) {
152 /* Ensure we dropped all privileges. */
154 uid_t ruid, euid, suid;
155 gid_t rgid, egid, sgid;
157 if (getresuid(&ruid, &euid, &suid) != 0) {
160 if (getresgid(&rgid, &egid, &sgid) != 0) {
163 if ( uid != ruid || uid != euid || uid != suid
164 || gid != rgid || gid != egid || gid != sgid) {
165 die_fmt("failed to drop privileges\n");
168 /* Just to be safe. */
169 if (setuid(0) != -1) {
170 die_fmt("failed to drop privileges (setuid)\n");
174 static void quit_with_matching_code(int status) {
175 if (WIFEXITED(status)) {
176 exit(WEXITSTATUS(status));
177 } else if (WIFSIGNALED(status)) {
178 kill(getpid(), WTERMSIG(status));
179 /* Fall-through, should not happen. */
181 abort(); /* Should never happen, die painfully. */
184 static bool read_from_write_to(int from, int to) {
187 ssize_t r = read(from, buf, sizeof(buf));
192 size_t left = (size_t)r;
196 ssize_t w = write(to, data, left);
198 if (errno == EINTR) {
210 static void proxy_input_between_ttys(int pty_master, int ctty, volatile pid_t *pid_to_wait_for) {
211 struct pollfd fds[] = {
222 sigset_t sigset, sigset_old;
223 sigemptyset(&sigset);
224 sigaddset(&sigset, SIGCHLD);
225 if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
226 die("sigprocmask block sigchld proxy");
229 /* Proxy data until our child has terminated. */
230 while (*pid_to_wait_for != 0) {
232 * If a signal happens here _and_ the child hasn't closed pty_slave,
233 * we would hang in poll(); therefore ppoll() is necessary.
235 nfds_t nfds = sizeof(fds)/sizeof(*fds);
236 if (ppoll(fds, nfds, NULL /* no timeout */, &sigset_old) == -1) {
237 if (errno == EAGAIN || errno == EINTR) {
245 * Handle errors first. (Data available before the error occurred
246 * might be dropped, but shouldn't matter here.)
248 if (fds[0].revents & (POLLERR | POLLNVAL)) {
249 fprintf(stderr, "poll: error on master: %d\n", fds[0].revents);
252 if (fds[1].revents & (POLLERR | POLLNVAL)) {
253 fprintf(stderr, "poll: error on ctty: %d\n", fds[1].revents);
257 /* Read data if available. */
258 if (fds[0].revents & POLLIN) {
259 if (!read_from_write_to(pty_master, ctty)) {
260 perror("read from master write to ctty");
264 if (fds[1].revents & POLLIN) {
265 if (!read_from_write_to(ctty, pty_master)) {
266 perror("read from ctty write to master");
271 /* Finally we are done if either side of the pty has disconnected. */
272 if ((fds[0].revents & POLLHUP) || (fds[1].revents & POLLHUP)) {
277 if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
278 die("sigprocmask setmask proxy");
284 * Not sig_atomic_t (as required by POSIX) but I don't know how to do that any
287 static volatile pid_t pid_to_wait_for;
288 static int pid_to_wait_for_status;
290 static void sigchld_handler(int signal) {
296 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
297 if (pid == pid_to_wait_for) {
298 /* Mark that our child has died and we should exit as well. */
300 /* We must exit like our child, save status. */
301 pid_to_wait_for_status = status;
307 * SIGWINCH handler to handle resizes of the outer terminal.
309 * Errors are ignored without message because printing in signal handlers is
310 * problematic (no FILE * usable due to locks) and there's not much we can do
313 static int sigwinch_ctty = -1;
314 static int sigwinch_slave = -1;
316 static void sigwinch_handler(int signal) {
320 if (ioctl(sigwinch_ctty, TIOCGWINSZ, &size) == -1) {
323 if (ioctl(sigwinch_slave, TIOCSWINSZ, &size) == -1) {
329 int main(int argc, char **argv) {
330 char *exec_argv_shell[] = { NULL, NULL }; /* filled below */
331 char **exec_argv = NULL;
334 /* exec_argv set below */
335 } else if (argc > 2) {
336 exec_argv = argv + 2;
338 die_fmt("%s <user> [<cmd>...]\n", argv[0]);
341 const char *user = argv[1];
343 struct passwd *passwd = getpwnam(user);
345 die_fmt("unknown user name '%s'\n", user);
348 uid_t uid = passwd->pw_uid;
349 gid_t gid = passwd->pw_gid;
353 exec_argv_shell[0] = passwd->pw_shell;
354 exec_argv = exec_argv_shell;
357 int pty_master, pty_slave;
359 open_pty_or_die(&pty_master, &pty_slave, uid);
361 int ctty = open("/dev/tty", O_RDWR | O_NOCTTY); /* controlling TTY */
363 die("open /dev/tty");
366 sigset_t sigset, sigset_old;
367 sigemptyset(&sigset);
368 sigaddset(&sigset, SIGCHLD);
369 if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
370 die("sigprocmask block sigchld");
376 } else if (pid == 0) {
377 /* child, will become a session leader */
379 if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
380 die("sigprocmask setmask child");
384 if (ioctl(ctty, TIOCGWINSZ, &size) == -1) {
385 die("ioctl TIOCGWINSZ");
388 close_or_die(pty_master);
391 /* Start a new session and attach controlling TTY. */
392 if (setsid() == -1) {
395 if (ioctl(pty_slave, TIOCSCTTY, 0) == -1) {
396 die("ioctl TIOCSCTTY");
399 if (ioctl(pty_slave, TIOCSWINSZ, &size) == -1) {
400 die("ioctl TIOCSWINSZ");
406 } else if (pid == 0) {
408 * Drop the privileges just now so that the other user doesn't get
409 * access to the master TTY or the session leader (which might
410 * have additional privileges).
412 drop_privileges_or_die(uid, gid);
414 dup2_or_die(pty_slave, STDIN_FILENO);
415 dup2_or_die(pty_slave, STDOUT_FILENO);
416 dup2_or_die(pty_slave, STDERR_FILENO);
417 close_or_die(pty_slave);
419 const char *term_orig = getenv("TERM");
420 const char *term = term_orig;
422 term = ""; /* for strlen() below */
424 const char *home = passwd->pw_dir;
427 * Ignore errors here as we don't want to die on non-existent home
428 * directories to allow running as any user (think "/nonexistent"
429 * as home) and an error message will be annoying to ignore when
430 * running this command in scripts.
434 char envp_user[strlen("USER=") + strlen(user) + 1];
435 char envp_home[strlen("HOME=") + strlen(home) + 1];
436 char envp_term[strlen("TERM=") + strlen(term) + 1];
437 snprintf_or_assert(envp_user, sizeof(envp_user), "USER=%s", user);
438 snprintf_or_assert(envp_home, sizeof(envp_home), "HOME=%s", home);
439 snprintf_or_assert(envp_term, sizeof(envp_term), "TERM=%s", term);
441 char *exec_envp[] = {
442 "PATH=" PTYAS_DEFAULT_PATH,
445 term_orig ? envp_term : NULL,
449 execve(exec_argv[0], exec_argv, exec_envp);
452 close_or_die(pty_slave);
453 close_or_die(STDIN_FILENO);
454 close_or_die(STDOUT_FILENO);
455 close_or_die(STDERR_FILENO);
459 if (waitpid(pid, &status, 0) <= 0) {
460 die("waitpid child");
462 quit_with_matching_code(status);
464 /* Don't close pty_slave here as it's used in sigwinch_handler(). */
466 sigwinch_ctty = ctty;
467 sigwinch_slave = pty_slave;
469 struct sigaction action_sigwinch = {
470 .sa_handler = sigwinch_handler,
472 sigemptyset(&action_sigwinch.sa_mask);
473 if (sigaction(SIGWINCH, &action_sigwinch, NULL) != 0) {
474 die("sigaction SIGWINCH");
477 pid_to_wait_for = pid;
478 struct sigaction action_sigchld = {
479 .sa_handler = sigchld_handler,
481 sigemptyset(&action_sigchld.sa_mask);
482 if (sigaction(SIGCHLD, &action_sigchld, NULL) != 0) {
483 die("sigaction SIGCHLD");
486 if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
487 die("sigprocmask setmask parent");
490 struct termios old_term, term;
492 /* Change terminal to raw mode. */
493 if (tcgetattr(ctty, &old_term) != 0) {
497 /* From man 3 cfmakeraw; cfmakeraw is non-standard so set it manually. */
498 term.c_iflag &= ~(tcflag_t)(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
499 term.c_oflag &= ~(tcflag_t)(OPOST);
500 term.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
501 term.c_cflag &= ~(tcflag_t)(CSIZE | PARENB);
503 if (tcsetattr(ctty, TCSADRAIN, &term) != 0) {
507 proxy_input_between_ttys(pty_master, ctty, &pid_to_wait_for);
509 /* Restore terminal mode. */
510 if (tcsetattr(ctty, TCSADRAIN, &old_term) != 0) {
511 die("tcsetattr restore");
515 * Wait until we got the status code from our child. poll() might already
516 * exit after POLLHUP while we haven't collected the child yet.
518 if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
519 die("sigprocmask block sigchld loop");
521 while (pid_to_wait_for != 0) {
522 sigsuspend(&sigset_old);
523 if (errno != EINTR) {
527 if (sigprocmask(SIG_SETMASK, &sigset, &sigset_old) != 0) {
528 die("sigprocmask setmask sigchld loop");
531 /* Try to exit the same way as the spawned process. */
532 if (pid_to_wait_for == 0) {
533 quit_with_matching_code(pid_to_wait_for_status);