]> ruderich.org/simon Gitweb - wall-notify/wall-notify.git/blob - src/wall-notify.c
fix race condition in handle_wall()
[wall-notify/wall-notify.git] / src / wall-notify.c
1 /*
2  * Receive wall messages and pass them to a notification program via stdin.
3  *
4  * Copyright (C) 2014  Simon Ruderich
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20
21 #include <config.h>
22
23 #include <assert.h>
24 #include <fcntl.h>
25 #include <limits.h>
26 #include <pwd.h>
27 #include <signal.h>
28 #include <stdint.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <sys/select.h>
33 #include <sys/stat.h>
34 #include <sys/time.h>
35 #include <sys/types.h>
36 #include <sys/wait.h>
37 #include <unistd.h>
38
39 #ifdef USE_UTEMPTER
40 # include <utempter.h>
41 #endif
42 #ifdef USE_UTMPX
43 # include <utmpx.h>
44 #endif
45 #ifndef DONT_USE_X11
46 # include <pthread.h>
47 # include <X11/Xlib.h>
48 #endif
49
50
51 static sig_atomic_t signaled = 0;
52
53
54 static void sig_handler(int signal) {
55     (void)signal;
56
57     signaled = 1;
58 }
59 static void setup_signals(void) {
60     struct sigaction action;
61
62     memset(&action, 0, sizeof(action));
63     sigemptyset(&action.sa_mask);
64     action.sa_handler = sig_handler;
65
66     /* Handle all important signals which might be sent to us so we break out
67      * of the read()-loop below and can perform our cleanup. */
68     sigaction(SIGHUP,  &action, NULL);
69     sigaction(SIGINT,  &action, NULL);
70     sigaction(SIGQUIT, &action, NULL);
71     sigaction(SIGUSR1, &action, NULL);
72     sigaction(SIGUSR2, &action, NULL);
73 }
74
75 static int set_cloexec(int fd) {
76     int flags = fcntl(fd, F_GETFD);
77     if (flags == -1) {
78         return 0;
79     }
80     if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
81         return 0;
82     }
83     return 1;
84 }
85
86 static int open_tty(int public) {
87     int ptm;
88     const char *name;
89
90     ptm = posix_openpt(O_RDWR);
91     if (ptm < 0) {
92         return -1;
93     }
94     if (grantpt(ptm) != 0) {
95         return -1;
96     }
97     if (!set_cloexec(ptm)) {
98         return -1;
99     }
100
101     /* Prevent write access for other users so they can't use wall to send
102      * messages to this program. */
103     if (!public) {
104         name = ptsname(ptm);
105         if (!name) {
106             return -1;
107         }
108         if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
109             return -1;
110         }
111     }
112
113     if (unlockpt(ptm) != 0) {
114         return -1;
115     }
116
117     return ptm;
118 }
119
120 #ifdef USE_UTMPX
121 static const char *skip_prefix(const char *string, const char *prefix) {
122     size_t length = strlen(prefix);
123
124     if (!strncmp(string, prefix, length)) {
125         return string + length;
126     } else {
127         return string;
128     }
129 }
130 static int set_utmpx(short type, int ptm) {
131     struct utmpx entry;
132
133     const char *tty, *user, *id, *line;
134     struct timeval now;
135
136     user = getpwuid(getuid())->pw_name;
137     gettimeofday(&now, NULL);
138
139     tty = ptsname(ptm);
140     if (!tty) {
141         return 0;
142     }
143
144     id   = skip_prefix(tty, "/dev/pts/");
145     line = skip_prefix(tty, "/dev/");
146
147     /* Create utmp entry for the given terminal. */
148     memset(&entry, 0, sizeof(entry));
149
150     snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
151     snprintf(entry.ut_id,   sizeof(entry.ut_id),   "%s", id);
152     snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
153
154     entry.ut_pid  = getpid();
155     entry.ut_type = type;
156     entry.ut_tv.tv_sec  = now.tv_sec;
157     entry.ut_tv.tv_usec = now.tv_usec;
158
159     /* Write the entry to the utmp file. */
160     setutxent();
161     if (!pututxline(&entry)) {
162         return 0;
163     }
164     endutxent();
165
166     return 1;
167 }
168 #endif
169 static int login(int ptm) {
170 #if defined(USE_UTEMPTER)
171     int result = utempter_add_record(ptm, NULL);
172     /* Exit value of utempter_*() is not correct on all systems, e.g.
173      * FreeBSD() always returns 0. Checking the utmpx database manually is
174      * difficult because we don't know the exact values for ut_id and ut_line,
175      * therefore we only check the return value on systems known to return a
176      * useful value. */
177 # ifndef __linux
178     result = 1;
179 # endif
180     return result;
181 #elif defined(USE_UTMPX)
182     return set_utmpx(USER_PROCESS, ptm);
183 #else
184 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
185 #endif
186 }
187 static int logout(int ptm) {
188 #if defined(USE_UTEMPTER)
189     int result = utempter_remove_record(ptm);
190     /* See above. */
191 # ifndef __linux
192     result = 1;
193 # endif
194     return result;
195 #elif defined(USE_UTMPX)
196     return set_utmpx(DEAD_PROCESS, ptm);
197 #else
198 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
199 #endif
200 }
201
202 static int wait_for_write(int fd, int timeout) {
203     fd_set rfds;
204     struct timeval tv;
205     int result;
206
207     FD_ZERO(&rfds);
208     FD_SET(fd, &rfds);
209
210     tv.tv_sec  = timeout;
211     tv.tv_usec = 0;
212
213     result = select(fd + 1, &rfds, NULL, NULL, &tv);
214     if (result < 0) {
215         perror("select");
216         return 0;
217     }
218     if (result == 0) {
219         /* Timeout. */
220         return 0;
221     }
222
223     /* Got more data to read. */
224     return 1;
225 }
226
227 #ifdef USE_UTMPX
228 static void drop_privileges(void) {
229     uid_t uid, ruid, euid, suid;
230     gid_t gid, rgid, egid, sgid;
231
232     uid = getuid();
233     gid = getgid();
234
235     /* Drop all privileges. */
236     if (setresgid(gid, gid, gid) != 0) {
237         perror("setresgid");
238         exit(EXIT_FAILURE);
239     }
240     if (setresuid(uid, uid, uid) != 0) {
241         perror("setresuid");
242         exit(EXIT_FAILURE);
243     }
244
245     /* Verify all privileges were dropped. */
246     if (getresuid(&ruid, &euid, &suid) != 0) {
247         perror("getresuid");
248         exit(EXIT_FAILURE);
249     }
250     if (getresgid(&rgid, &egid, &sgid) != 0) {
251         perror("getresgid");
252         exit(EXIT_FAILURE);
253     }
254     if (uid == ruid && uid == euid && uid == suid
255             && gid == rgid && gid == egid && gid == sgid) {
256         /* Everything fine. */
257         return;
258     }
259
260     fprintf(stderr, "failed to drop privileges, aborting\n");
261     exit(EXIT_FAILURE);
262 }
263 #endif
264
265 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
266     int fds[2];
267     FILE *fh;
268
269     pid_t pid;
270
271     if (pipe(fds) != 0) {
272         perror("pipe");
273         return;
274     }
275
276     fh = fdopen(fds[1] /* write side */, "w");
277     if (!fh) {
278         perror("fdopen");
279         close(fds[0]);
280         close(fds[1]);
281         return;
282     }
283
284     pid = fork();
285     if (pid < 0) {
286         perror("fork");
287         goto out;
288     } else if (pid == 0) {
289         /* child */
290
291 #ifdef USE_UTMPX
292         drop_privileges();
293 #endif
294
295         close(fds[1]); /* write side */
296
297         /* Pass read side as stdin to the program. */
298         if (dup2(fds[0], STDIN_FILENO) < 0) {
299             perror("dup2");
300             exit(EXIT_FAILURE);
301         }
302         close(fds[0]);
303
304         /* Double fork so `wall-notify` doesn't have to wait for the
305          * notification process to terminate. We can't just use
306          * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
307          * doesn't work if SIGCHLD is ignored. */
308         pid = fork();
309         if (pid < 0) {
310             perror("fork");
311             exit(EXIT_FAILURE);
312         } else if (pid == 0) {
313             execvp(argv[0], argv);
314             perror("execvp");
315             exit(EXIT_FAILURE);
316         }
317
318         exit(EXIT_SUCCESS);
319     }
320     /* father */
321
322     if (fwrite(buffer, 1, length, fh) != length) {
323         perror("fwrite");
324         /* continue to perform cleanup */
325     }
326
327 out:
328     close(fds[0]); /* read side */
329     fclose(fh);
330
331     if (waitpid(pid, NULL, 0) < 0) {
332         perror("waitpid");
333     }
334 }
335 static void handle_wall(int fd, char **argv) {
336     char buffer[4096];
337     ssize_t r;
338
339     assert(SSIZE_MAX <= SIZE_MAX);
340     while (!signaled && (r = read(fd, buffer, sizeof(buffer))) > 0) {
341         size_t space;
342         ssize_t r2;
343
344         /* To prevent partial messages (sometimes it takes multiple reads to
345          * get the complete message) wait for a short time to get the rest of
346          * the message. */
347         space = sizeof(buffer) - (size_t)r;
348         while (space > 0 && wait_for_write(fd, 1 /* second */)) {
349             r2 = read(fd, buffer + r, space);
350             if (r2 <= 0) {
351                 break;
352             }
353             r += r2;
354             space -= (size_t)r2;
355         }
356
357         pass_buffer_to_program(buffer, (size_t)r, argv);
358     }
359 }
360 #ifndef DONT_USE_X11
361 static void *x11_event_loop_thread(void *unused) {
362     Display *display;
363     XEvent event;
364
365     (void)unused;
366
367     pthread_detach(pthread_self());
368
369     display = XOpenDisplay(NULL);
370     if (!display) {
371         fprintf(stderr, "failed to connect to X server\n");
372         exit(EXIT_FAILURE);
373     }
374
375     /* Do nothing. We just want to die if the X11 session is closed. */
376     while (1) {
377         XNextEvent(display, &event);
378     }
379 }
380 #endif
381
382 static void usage(const char *argv0) {
383     fprintf(stderr, "usage: %s [-X] [-m] [-v] <cmd args..>\n", argv0);
384     fprintf(stderr, "Pass wall messages to <cmd args..>.\n");
385     fprintf(stderr, "\n");
386     fprintf(stderr, "-X quit when the current X session terminates\n");
387     fprintf(stderr, "-m allow messages from normal users (like `mesg y`)\n");
388     fprintf(stderr, "-v display version\n");
389 #ifdef DONT_USE_X11
390     fprintf(stderr, "\n");
391     fprintf(stderr, "compiled without X11 support, -X disabled\n");
392 #endif
393     exit(EXIT_FAILURE);
394 }
395
396
397 int main(int argc, char **argv) {
398     int option, enable_x11, mesg_yes;
399     const char *argv0;
400     int ptm, pts;
401     char *name;
402
403     /* Don't display error messages for unknown options. */
404     opterr = 0;
405
406     enable_x11 = 0;
407     mesg_yes = 0;
408
409     argv0 = argv[0];
410
411     /*
412      * Glibc violates POSIX by default and skips over non-option arguments and
413      * parses later arguments which look like options as well. But we want to
414      * pass everything except the options unmodified to execvp(). Prefixing
415      * the optstring with "+" fixes this behaver. This is not POSIX
416      * compatible, but the option should be ignored on other systems.
417      */
418     while ((option = getopt(argc, argv, "+Xmhv")) != -1) {
419         switch (option) {
420             case 'X':
421                 enable_x11 = 1;
422                 break;
423             case 'm':
424                 mesg_yes = 1;
425                 break;
426             case 'h':
427                 usage(argv0);
428                 break;
429             case 'v':
430                 printf("%s%s\n",
431                        PACKAGE_STRING,
432                        strlen(GIT_VERSION) ? " (Git " GIT_VERSION ")" : "");
433                 exit(EXIT_SUCCESS);
434                 break;
435             default:
436                 fprintf(stderr, "%s: unknown option '%s'!\n\n",
437                                 argv0, argv[optind - 1]);
438                 usage(argv0);
439                 break;
440         }
441     }
442
443     /* No arguments remaining, abort. */
444     if (!argv[optind]) {
445         usage(argv0);
446     }
447     /* Arguments for notification program. */
448     argv += optind;
449
450     ptm = open_tty(mesg_yes);
451     if (ptm < 0) {
452         perror("open_tty");
453         exit(EXIT_FAILURE);
454     }
455     name = ptsname(ptm);
456     if (!name) {
457         perror("ptsname");
458         exit(EXIT_FAILURE);
459     }
460
461 #ifdef DEBUG
462     printf("%s\n", name);
463 #endif
464
465     /* We need to open the slave or reading from the master yields EOF after
466      * the first wall write to it. */
467     pts = open(name, O_RDWR | O_CLOEXEC);
468     if (pts < 0) {
469         perror(name);
470         exit(EXIT_FAILURE);
471     }
472
473     /* Start a thread which connects to X11. This way we die if the user logs
474      * out of its X11 session. */
475     if (enable_x11) {
476 #ifdef DONT_USE_X11
477         fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
478         usage(argv0);
479 #else
480         pthread_t tid;
481
482         if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
483             perror("pthread_create");
484             exit(EXIT_FAILURE);
485         }
486 #endif
487     }
488
489     /* Cleanup on signals. Necessary before login(). */
490     setup_signals();
491
492     if (!login(ptm)) {
493         perror("login");
494         exit(EXIT_FAILURE);
495     }
496
497     /* Main loop. Handle all wall messages sent to our PTY. */
498     handle_wall(ptm, argv);
499
500     if (!logout(ptm)) {
501         perror("logout");
502         exit(EXIT_FAILURE);
503     }
504
505     close(ptm);
506     close(pts);
507
508     return EXIT_SUCCESS;
509 }