]> ruderich.org/simon Gitweb - wall-notify/wall-notify.git/blobdiff - src/wall-notify.c
set O_CLOEXEC on TTY slave/master file descriptors
[wall-notify/wall-notify.git] / src / wall-notify.c
index c502bce0862e1c4a2379116b2cf3a6150be6361e..57ec37f00a60cef695e28b536055551716c4f391 100644 (file)
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/select.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/types.h>
+#include <sys/wait.h>
 #include <unistd.h>
 
 #ifdef USE_UTEMPTER
 #ifdef USE_UTMPX
 # include <utmpx.h>
 #endif
+#ifndef DONT_USE_X11
+# include <pthread.h>
+# include <X11/Xlib.h>
+#endif
 
 
 static void sig_handler(int signal) {
@@ -59,12 +65,20 @@ static void setup_signals(void) {
     sigaction(SIGQUIT, &action, NULL);
     sigaction(SIGUSR1, &action, NULL);
     sigaction(SIGUSR2, &action, NULL);
+}
 
-    /* Collect zombies automatically without having to call waitpid(2). */
-    signal(SIGCHLD, SIG_IGN);
+static int set_cloexec(int fd) {
+    int flags = fcntl(fd, F_GETFD);
+    if (flags == -1) {
+        return 0;
+    }
+    if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
+        return 0;
+    }
+    return 1;
 }
 
-static int open_tty(void) {
+static int open_tty(int public) {
     int ptm;
     const char *name;
 
@@ -75,15 +89,20 @@ static int open_tty(void) {
     if (grantpt(ptm) != 0) {
         return -1;
     }
+    if (!set_cloexec(ptm)) {
+        return -1;
+    }
 
     /* Prevent write access for other users so they can't use wall to send
      * messages to this program. */
-    name = ptsname(ptm);
-    if (!name) {
-        return -1;
-    }
-    if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
-        return -1;
+    if (!public) {
+        name = ptsname(ptm);
+        if (!name) {
+            return -1;
+        }
+        if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
+            return -1;
+        }
     }
 
     if (unlockpt(ptm) != 0) {
@@ -144,7 +163,16 @@ static int set_utmpx(short type, int ptm) {
 #endif
 static int login(int ptm) {
 #if defined(USE_UTEMPTER)
-    return utempter_add_record(ptm, NULL);
+    int result = utempter_add_record(ptm, NULL);
+    /* Exit value of utempter_*() is not correct on all systems, e.g.
+     * FreeBSD() always returns 0. Checking the utmpx database manually is
+     * difficult because we don't know the exact values for ut_id and ut_line,
+     * therefore we only check the return value on systems known to return a
+     * useful value. */
+# ifndef __linux
+    result = 1;
+# endif
+    return result;
 #elif defined(USE_UTMPX)
     return set_utmpx(USER_PROCESS, ptm);
 #else
@@ -153,7 +181,12 @@ static int login(int ptm) {
 }
 static int logout(int ptm) {
 #if defined(USE_UTEMPTER)
-    return utempter_remove_record(ptm);
+    int result = utempter_remove_record(ptm);
+    /* See above. */
+# ifndef __linux
+    result = 1;
+# endif
+    return result;
 #elif defined(USE_UTMPX)
     return set_utmpx(DEAD_PROCESS, ptm);
 #else
@@ -161,15 +194,75 @@ static int logout(int ptm) {
 #endif
 }
 
+static int wait_for_write(int fd, int timeout) {
+    fd_set rfds;
+    struct timeval tv;
+    int result;
+
+    FD_ZERO(&rfds);
+    FD_SET(fd, &rfds);
+
+    tv.tv_sec  = timeout;
+    tv.tv_usec = 0;
+
+    result = select(fd + 1, &rfds, NULL, NULL, &tv);
+    if (result < 0) {
+        perror("select");
+        return 0;
+    }
+    if (result == 0) {
+        /* Timeout. */
+        return 0;
+    }
+
+    /* Got more data to read. */
+    return 1;
+}
+
+#ifdef USE_UTMPX
+static void drop_privileges(void) {
+    uid_t uid, ruid, euid, suid;
+    gid_t gid, rgid, egid, sgid;
+
+    uid = getuid();
+    gid = getgid();
+
+    /* Drop all privileges. */
+    if (setresgid(gid, gid, gid) != 0) {
+        perror("setresgid");
+        exit(EXIT_FAILURE);
+    }
+    if (setresuid(uid, uid, uid) != 0) {
+        perror("setresuid");
+        exit(EXIT_FAILURE);
+    }
+
+    /* Verify all privileges were dropped. */
+    if (getresuid(&ruid, &euid, &suid) != 0) {
+        perror("getresuid");
+        exit(EXIT_FAILURE);
+    }
+    if (getresgid(&rgid, &egid, &sgid) != 0) {
+        perror("getresgid");
+        exit(EXIT_FAILURE);
+    }
+    if (uid == ruid && uid == euid && uid == suid
+            && gid == rgid && gid == egid && gid == sgid) {
+        /* Everything fine. */
+        return;
+    }
+
+    fprintf(stderr, "failed to drop privileges, aborting\n");
+    exit(EXIT_FAILURE);
+}
+#endif
+
 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
     int fds[2];
     FILE *fh;
 
     pid_t pid;
 
-    /* Skip argv[0]. */
-    argv++;
-
     if (pipe(fds) != 0) {
         perror("pipe");
         return;
@@ -190,6 +283,10 @@ static void pass_buffer_to_program(const char *buffer, size_t length, char **arg
     } else if (pid == 0) {
         /* child */
 
+#ifdef USE_UTMPX
+        drop_privileges();
+#endif
+
         close(fds[1]); /* write side */
 
         /* Pass read side as stdin to the program. */
@@ -199,9 +296,21 @@ static void pass_buffer_to_program(const char *buffer, size_t length, char **arg
         }
         close(fds[0]);
 
-        execvp(argv[0], argv);
-        perror("execvp");
-        exit(EXIT_FAILURE);
+        /* Double fork so `wall-notify` doesn't have to wait for the
+         * notification process to terminate. We can't just use
+         * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
+         * doesn't work if SIGCHLD is ignored. */
+        pid = fork();
+        if (pid < 0) {
+            perror("fork");
+            exit(EXIT_FAILURE);
+        } else if (pid == 0) {
+            execvp(argv[0], argv);
+            perror("execvp");
+            exit(EXIT_FAILURE);
+        }
+
+        exit(EXIT_SUCCESS);
     }
     /* father */
 
@@ -213,28 +322,124 @@ static void pass_buffer_to_program(const char *buffer, size_t length, char **arg
 out:
     close(fds[0]); /* read side */
     fclose(fh);
+
+    if (waitpid(pid, NULL, 0) < 0) {
+        perror("waitpid");
+    }
 }
 static void handle_wall(int fd, char **argv) {
     char buffer[4096];
     ssize_t r;
 
+    assert(SSIZE_MAX <= SIZE_MAX);
     while ((r = read(fd, buffer, sizeof(buffer))) > 0) {
-        assert(SSIZE_MAX <= SIZE_MAX);
+        size_t space;
+        ssize_t r2;
+
+        /* To prevent partial messages (sometimes it takes multiple reads to
+         * get the complete message) wait for a short time to get the rest of
+         * the message. */
+        space = sizeof(buffer) - (size_t)r;
+        while (space > 0 && wait_for_write(fd, 1 /* second */)) {
+            r2 = read(fd, buffer + r, space);
+            if (r2 <= 0) {
+                break;
+            }
+            r += r2;
+            space -= (size_t)r2;
+        }
+
         pass_buffer_to_program(buffer, (size_t)r, argv);
     }
 }
+#ifndef DONT_USE_X11
+static void *x11_event_loop_thread(void *unused) {
+    Display *display;
+    XEvent event;
+
+    (void)unused;
+
+    pthread_detach(pthread_self());
+
+    display = XOpenDisplay(NULL);
+    if (!display) {
+        fprintf(stderr, "failed to connect to X server\n");
+        exit(EXIT_FAILURE);
+    }
+
+    /* Do nothing. We just want to die if the X11 session is closed. */
+    while (1) {
+        XNextEvent(display, &event);
+    }
+}
+#endif
+
+static void usage(const char *argv0) {
+    fprintf(stderr, "usage: %s [-X] [-m] [-v] <cmd args..>\n", argv0);
+    fprintf(stderr, "Pass wall messages to <cmd args..>.\n");
+    fprintf(stderr, "\n");
+    fprintf(stderr, "-X quit when the current X session terminates\n");
+    fprintf(stderr, "-m allow messages from normal users (like `mesg y`)\n");
+    fprintf(stderr, "-v display version\n");
+#ifdef DONT_USE_X11
+    fprintf(stderr, "\n");
+    fprintf(stderr, "compiled without X11 support, -X disabled\n");
+#endif
+    exit(EXIT_FAILURE);
+}
 
 
 int main(int argc, char **argv) {
+    int option, enable_x11, mesg_yes;
     int ptm, pts;
     char *name;
 
-    if (argc < 2) {
-        fprintf(stderr, "usage: %s <cmd args..>\n", argv[0]);
-        exit(EXIT_FAILURE);
+    /* Don't display error messages for unknown options. */
+    opterr = 0;
+
+    enable_x11 = 0;
+    mesg_yes = 0;
+
+    /*
+     * Glibc violates POSIX by default and skips over non-option arguments and
+     * parses later arguments which look like options as well. But we want to
+     * pass everything except the options unmodified to execvp(). Prefixing
+     * the optstring with "+" fixes this behaver. This is not POSIX
+     * compatible, but the option should be ignored on other systems.
+     */
+    while ((option = getopt(argc, argv, "+Xmhv")) != -1) {
+        switch (option) {
+            case 'X':
+                enable_x11 = 1;
+                break;
+            case 'm':
+                mesg_yes = 1;
+                break;
+            case 'h':
+                usage(argv[0]);
+                break;
+            case 'v':
+                printf("%s%s\n",
+                       PACKAGE_STRING,
+                       strlen(GIT_VERSION) ? " (Git " GIT_VERSION ")" : "");
+                exit(0);
+                break;
+            default:
+                fprintf(stderr, "%s: unknown option '%s'!\n\n",
+                                argv[0], argv[optind - 1]);
+                usage(argv[0]);
+                break;
+        }
+    }
+
+    /* No arguments remaining, abort. */
+    if (!argv[optind]) {
+        usage(argv[0]);
     }
+    /* Arguments for notification program. */
+    argv += optind;
 
-    ptm = open_tty();
+    ptm = open_tty(mesg_yes);
     if (ptm < 0) {
         perror("open_tty");
         exit(EXIT_FAILURE);
@@ -251,12 +456,28 @@ int main(int argc, char **argv) {
 
     /* We need to open the slave or reading from the master yields EOF after
      * the first wall write to it. */
-    pts = open(name, O_RDWR);
+    pts = open(name, O_RDWR | O_CLOEXEC);
     if (pts < 0) {
         perror(name);
         exit(EXIT_FAILURE);
     }
 
+    /* Start a thread which connects to X11. This way we die if the user logs
+     * out of its X11 session. */
+    if (enable_x11) {
+#ifdef DONT_USE_X11
+        fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
+        usage(argv0);
+#else
+        pthread_t tid;
+
+        if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
+            perror("pthread_create");
+            exit(EXIT_FAILURE);
+        }
+#endif
+    }
+
     /* Cleanup on signals. Necessary before login(). */
     setup_signals();