]> ruderich.org/simon Gitweb - ptyas/ptyas.git/blob - ptyas.c
Add support for FreeBSD
[ptyas/ptyas.git] / ptyas.c
1 /*
2  * Run the login shell or command as the given user in a new pty to prevent
3  * terminal injection attacks.
4  *
5  * Copyright (C) 2016-2019  Simon Ruderich
6  *
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.
11  *
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.
16  *
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/>.
19  */
20
21 #define _GNU_SOURCE
22
23 #include <assert.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <grp.h>
27 #include <limits.h>
28 #include <poll.h>
29 #include <pwd.h>
30 #include <signal.h>
31 #include <stdarg.h>
32 #include <stdbool.h>
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <sys/ioctl.h>
37 #include <sys/types.h>
38 #include <sys/wait.h>
39 #include <termios.h>
40 #include <unistd.h>
41
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"
46 #endif
47
48
49 static void die(const char *s) {
50     perror(s);
51     exit(EXIT_FAILURE);
52 }
53 static void die_fmt(const char *fmt, ...) {
54     va_list ap;
55
56     va_start(ap, fmt);
57     vfprintf(stderr, fmt, ap);
58     va_end(ap);
59
60     exit(EXIT_FAILURE);
61 }
62
63 static void open_pty_or_die(int *pty_master, int *pty_slave, uid_t uid) {
64     char *slave_path;
65
66     *pty_master = posix_openpt(O_RDWR | O_NOCTTY);
67     if (*pty_master == -1) {
68         die("posix_openpt");
69     }
70     slave_path = ptsname(*pty_master);
71     if (!slave_path) {
72         die("ptsname");
73     }
74     if (grantpt(*pty_master) != 0) {
75         die("grantpt");
76     }
77     if (unlockpt(*pty_master) != 0) {
78         die("unlockpt");
79     }
80
81     *pty_slave = open(slave_path, O_RDWR | O_NOCTTY);
82     if (*pty_slave == -1) {
83         die("open slave tty");
84     }
85     /*
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.
89      */
90     if (chown(slave_path, uid, (gid_t)-1) != 0) {
91         die("chown slave tty");
92     }
93 }
94
95 static void close_or_die(int fd) {
96     if (close(fd) != 0) {
97         die("close");
98     }
99 }
100 static void dup2_or_die(int oldfd, int newfd) {
101     if (dup2(oldfd, newfd) != newfd) {
102         die("dup2");
103     }
104 }
105 static int snprintf_or_assert(char *str, size_t size, const char *format, ...) {
106     int ret;
107     va_list ap;
108
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 */
113     va_end(ap);
114
115     return ret;
116 }
117
118 static void drop_privileges_or_die(uid_t uid, gid_t gid) {
119     /* Drop all supplementary group IDs. */
120 #ifdef __FreeBSD__
121     {
122         /* FreeBSD uses the first gid to set the egid of the process. */
123         gid_t egid = gid;
124         if (setgroups(1, &egid) != 0) {
125             die("setgroups");
126         }
127         if (getgroups(1, &egid) != 1) {
128             die_fmt("failed to drop all supplementary groups\n");
129         }
130         if (egid != gid) {
131             die_fmt("failed to drop all supplementary groups (egid): %d %d\n",
132                     egid, gid);
133         }
134     }
135 #else
136     if (setgroups(0, NULL) != 0) {
137         die("setgroups");
138     }
139     if (getgroups(0, NULL) != 0) {
140         die_fmt("failed to drop all supplementary groups\n");
141     }
142 #endif
143
144     /* Dropping groups may require privileges, do that first. */
145     if (setresgid(gid, gid, gid) != 0) {
146         die("setresgid");
147     }
148     if (setresuid(uid, uid, uid) != 0) {
149         die("setresuid");
150     }
151
152     /* Ensure we dropped all privileges. */
153     {
154         uid_t ruid, euid, suid;
155         gid_t rgid, egid, sgid;
156
157         if (getresuid(&ruid, &euid, &suid) != 0) {
158             die("getresuid");
159         }
160         if (getresgid(&rgid, &egid, &sgid) != 0) {
161             die("getresgid");
162         }
163         if (       uid != ruid || uid != euid || uid != suid
164                 || gid != rgid || gid != egid || gid != sgid) {
165             die_fmt("failed to drop privileges\n");
166         }
167     }
168     /* Just to be safe. */
169     if (setuid(0) != -1) {
170         die_fmt("failed to drop privileges (setuid)\n");
171     }
172 }
173
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. */
180     }
181     abort(); /* Should never happen, die painfully. */
182 }
183
184 static bool read_from_write_to(int from, int to) {
185     char buf[4096];
186
187     ssize_t r = read(from, buf, sizeof(buf));
188     if (r < 0) {
189         return false;
190     }
191
192     size_t left = (size_t)r;
193     char *data = buf;
194
195     while (left > 0) {
196         ssize_t w = write(to, data, left);
197         if (w < 0) {
198             if (errno == EINTR) {
199                 continue;
200             }
201             return false;
202         }
203         left -= (size_t)w;
204         data += (size_t)w;
205     }
206
207     return true;
208 }
209
210 static void proxy_input_between_ttys(int pty_master, int ctty, volatile pid_t *pid_to_wait_for) {
211     struct pollfd fds[] = {
212         { /* 0 */
213             .fd = pty_master,
214             .events = POLLIN,
215         },
216         { /* 1 */
217             .fd = ctty,
218             .events = POLLIN,
219         },
220     };
221
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");
227     }
228
229     /* Proxy data until our child has terminated. */
230     while (*pid_to_wait_for != 0) {
231         /*
232          * If a signal happens here _and_ the child hasn't closed pty_slave,
233          * we would hang in poll(); therefore ppoll() is necessary.
234          */
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) {
238                 continue;
239             }
240             perror("poll");
241             break;
242         }
243
244         /*
245          * Handle errors first. (Data available before the error occurred
246          * might be dropped, but shouldn't matter here.)
247          */
248         if (fds[0].revents & (POLLERR | POLLNVAL)) {
249             fprintf(stderr, "poll: error on master: %d\n", fds[0].revents);
250             break;
251         }
252         if (fds[1].revents & (POLLERR | POLLNVAL)) {
253             fprintf(stderr, "poll: error on ctty: %d\n", fds[1].revents);
254             break;
255         }
256
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");
261                 break;
262             }
263         }
264         if (fds[1].revents & POLLIN) {
265             if (!read_from_write_to(ctty, pty_master)) {
266                 perror("read from ctty write to master");
267                 break;
268             }
269         }
270
271         /* Finally we are done if either side of the pty has disconnected. */
272         if ((fds[0].revents & POLLHUP) || (fds[1].revents & POLLHUP)) {
273             break;
274         }
275     }
276
277     if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
278         die("sigprocmask setmask proxy");
279     }
280 }
281
282
283 /*
284  * Not sig_atomic_t (as required by POSIX) but I don't know how to do that any
285  * other way.
286  */
287 static volatile pid_t pid_to_wait_for;
288 static int pid_to_wait_for_status;
289
290 static void sigchld_handler(int signal) {
291     int status;
292     pid_t pid;
293
294     (void)signal;
295
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. */
299             pid_to_wait_for = 0;
300             /* We must exit like our child, save status. */
301             pid_to_wait_for_status = status;
302         }
303     }
304 }
305
306 /*
307  * SIGWINCH handler to handle resizes of the outer terminal.
308  *
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
311  * at this point.
312  */
313 static int sigwinch_ctty = -1;
314 static int sigwinch_slave = -1;
315
316 static void sigwinch_handler(int signal) {
317     (void)signal;
318
319     struct winsize size;
320     if (ioctl(sigwinch_ctty, TIOCGWINSZ, &size) == -1) {
321         return;
322     }
323     if (ioctl(sigwinch_slave, TIOCSWINSZ, &size) == -1) {
324         return;
325     }
326 }
327
328
329 int main(int argc, char **argv) {
330     char *exec_argv_shell[] = { NULL, NULL }; /* filled below */
331     char **exec_argv = NULL;
332
333     if (argc == 2) {
334         /* exec_argv set below */
335     } else if (argc > 2) {
336         exec_argv = argv + 2;
337     } else {
338         die_fmt("%s <user> [<cmd>...]\n", argv[0]);
339     }
340
341     const char *user = argv[1];
342
343     struct passwd *passwd = getpwnam(user);
344     if (!passwd) {
345         die_fmt("unknown user name '%s'\n", user);
346     }
347
348     uid_t uid = passwd->pw_uid;
349     gid_t gid = passwd->pw_gid;
350
351     if (!exec_argv) {
352         assert(argc == 2);
353         exec_argv_shell[0] = passwd->pw_shell;
354         exec_argv = exec_argv_shell;
355     }
356
357     int pty_master, pty_slave;
358
359     open_pty_or_die(&pty_master, &pty_slave, uid);
360
361     int ctty = open("/dev/tty", O_RDWR | O_NOCTTY); /* controlling TTY */
362     if (ctty == -1) {
363         die("open /dev/tty");
364     }
365
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");
371     }
372
373     pid_t pid = fork();
374     if (pid == -1) {
375         die("fork parent");
376     } else if (pid == 0) {
377         /* child, will become a session leader */
378
379         if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
380             die("sigprocmask setmask child");
381         }
382
383         struct winsize size;
384         if (ioctl(ctty, TIOCGWINSZ, &size) == -1) {
385             die("ioctl TIOCGWINSZ");
386         }
387
388         close_or_die(pty_master);
389         close_or_die(ctty);
390
391         /* Start a new session and attach controlling TTY. */
392         if (setsid() == -1) {
393             die("setsid");
394         }
395         if (ioctl(pty_slave, TIOCSCTTY, 0) == -1) {
396             die("ioctl TIOCSCTTY");
397         }
398
399         if (ioctl(pty_slave, TIOCSWINSZ, &size) == -1) {
400             die("ioctl TIOCSWINSZ");
401         }
402
403         pid_t pid = fork();
404         if (pid == -1) {
405             die("fork child");
406         } else if (pid == 0) {
407             /*
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).
411              */
412             drop_privileges_or_die(uid, gid);
413
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);
418
419             const char *term_orig = getenv("TERM");
420             const char *term = term_orig;
421             if (!term) {
422                 term = ""; /* for strlen() below */
423             }
424             const char *home = passwd->pw_dir;
425
426             /*
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.
431              */
432             chdir(home);
433
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);
440
441             char *exec_envp[] = {
442                 "PATH=" PTYAS_DEFAULT_PATH,
443                 envp_user,
444                 envp_home,
445                 term_orig ? envp_term : NULL,
446                 NULL,
447             };
448
449             execve(exec_argv[0], exec_argv, exec_envp);
450             die("execve");
451         }
452         close_or_die(pty_slave);
453         close_or_die(STDIN_FILENO);
454         close_or_die(STDOUT_FILENO);
455         close_or_die(STDERR_FILENO);
456
457         /* TODO: EINTR? */
458         int status;
459         if (waitpid(pid, &status, 0) <= 0) {
460             die("waitpid child");
461         }
462         quit_with_matching_code(status);
463     }
464     /* Don't close pty_slave here as it's used in sigwinch_handler(). */
465
466     sigwinch_ctty = ctty;
467     sigwinch_slave = pty_slave;
468
469     struct sigaction action_sigwinch = {
470         .sa_handler = sigwinch_handler,
471     };
472     sigemptyset(&action_sigwinch.sa_mask);
473     if (sigaction(SIGWINCH, &action_sigwinch, NULL) != 0) {
474         die("sigaction SIGWINCH");
475     }
476
477     pid_to_wait_for = pid;
478     struct sigaction action_sigchld = {
479         .sa_handler = sigchld_handler,
480     };
481     sigemptyset(&action_sigchld.sa_mask);
482     if (sigaction(SIGCHLD, &action_sigchld, NULL) != 0) {
483         die("sigaction SIGCHLD");
484     }
485
486     if (sigprocmask(SIG_SETMASK, &sigset_old, NULL) != 0) {
487         die("sigprocmask setmask parent");
488     }
489
490     struct termios old_term, term;
491
492     /* Change terminal to raw mode. */
493     if (tcgetattr(ctty, &old_term) != 0) {
494         die("tcgetattr");
495     }
496     term = old_term;
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);
502     term.c_cflag |= CS8;
503     if (tcsetattr(ctty, TCSADRAIN, &term) != 0) {
504         die("tcsetattr");
505     }
506
507     proxy_input_between_ttys(pty_master, ctty, &pid_to_wait_for);
508
509     /* Restore terminal mode. */
510     if (tcsetattr(ctty, TCSADRAIN, &old_term) != 0) {
511         die("tcsetattr restore");
512     }
513
514     /*
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.
517      */
518     if (sigprocmask(SIG_BLOCK, &sigset, &sigset_old) != 0) {
519         die("sigprocmask block sigchld loop");
520     }
521     while (pid_to_wait_for != 0) {
522         sigsuspend(&sigset_old);
523         if (errno != EINTR) {
524             die("sigsuspend");
525         }
526     }
527     if (sigprocmask(SIG_SETMASK, &sigset, &sigset_old) != 0) {
528         die("sigprocmask setmask sigchld loop");
529     }
530
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);
534     }
535     return EXIT_FAILURE;
536 }