]> git.armaanb.net Git - opendoas.git/blobdiff - persist_timestamp.c
libopenbsd/closefrom: correctly handle snprintf truncation
[opendoas.git] / persist_timestamp.c
index c8f4bb5ddd70f4242e3c3b5d86c86163cc4b775c..99b53785b775df4fcf8639af214631e558026d41 100644 (file)
@@ -1,42 +1,79 @@
+/*
+ * 1) Timestamp files and directories
+ *
+ * Timestamp files MUST NOT be accessible to users other than root,
+ * this includes the name, metadata and the content of timestamp files
+ * and directories.
+ *
+ * Symlinks can be used to create, manipulate or delete wrong files
+ * and directories. The Implementation MUST reject any symlinks for
+ * timestamp files or directories.
+ *
+ * To avoid race conditions the implementation MUST use the same
+ * file descriptor for permission checks and do read or write
+ * write operations after the permission checks.
+ *
+ * The timestamp files MUST be opened with openat(2) using the
+ * timestamp directory file descriptor. Permissions of the directory
+ * MUST be checked before opening the timestamp file descriptor.
+ *
+ * 2) Clock sources for timestamps
+ *
+ * Timestamp files MUST NOT rely on only one clock source, using the
+ * wall clock would allow to reset the clock to an earlier point in
+ * time to reuse a timestamp.
+ *
+ * The timestamp MUST consist of multiple clocks and MUST reject the
+ * timestamp if there is a change to any clock because there is no way
+ * to differentiate between malicious and legitimate clock changes.
+ *
+ * 3) Timestamp lifetime
+ *
+ * The implementation MUST NOT use the user controlled stdin, stdout
+ * and stderr file descriptors to determine the controlling terminal.
+ * On linux the /proc/$pid/stat file MUST be used to get the terminal
+ * number.
+ *
+ * There is no reliable way to determine the lifetime of a tty/pty.
+ * The start time of the session leader MUST be used as part of the
+ * timestamp to determine if the tty is still the same.
+ * If the start time of the session leader changed the timestamp MUST
+ * be rejected.
+ *
+ */
+
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/vfs.h>
+
+#if !defined(timespecisset) || \
+    !defined(timespeccmp) || \
+    !defined(timespecadd)
+#      include "sys-time.h"
+#endif
+
 #include <ctype.h>
 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <libgen.h>
+#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <time.h>
 #include <unistd.h>
-#include <limits.h>
-#include <string.h>
-
-#include <sys/stat.h>
-#include <sys/vfs.h>
 
 #include "includes.h"
 
 #ifndef TIMESTAMP_DIR
-# define TIMESTAMP_DIR "/tmp/doas"
+#      define TIMESTAMP_DIR "/tmp/doas"
 #endif
-#ifndef TMPFS_MAGIC
-# define TMPFS_MAGIC 0x01021994
-#endif
-
-#define        timespecisset(tsp)              ((tsp)->tv_sec || (tsp)->tv_nsec)
-#define        timespeccmp(tsp, usp, cmp)                                      \
-       (((tsp)->tv_sec == (usp)->tv_sec) ?                             \
-           ((tsp)->tv_nsec cmp (usp)->tv_nsec) :               \
-           ((tsp)->tv_sec cmp (usp)->tv_sec))
-#define        timespecadd(tsp, usp, vsp) do {                                         \
-               (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec;          \
-               (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec;       \
-               if ((vsp)->tv_nsec >= 1000000000L) {                            \
-                       (vsp)->tv_sec++;                                                                \
-                       (vsp)->tv_nsec -= 1000000000L;                                  \
-               }                                                                                                       \
-       } while (0)
 
+#if defined(TIMESTAMP_TMPFS) && defined(__linux__)
+#      ifndef TMPFS_MAGIC
+#              define TMPFS_MAGIC 0x01021994
+#      endif
+#endif
 
 #ifdef __linux__
 /* Use tty_nr from /proc/self/stat instead of using
  * See https://www.sudo.ws/alerts/tty_tickets.html
  */
 static int
-ttynr()
+proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
 {
+       char path[128];
        char buf[1024];
-       char *p, *p1, *saveptr;
+       char *p, *saveptr, *ep;
        const char *errstr;
        int fd, n;
 
        p = buf;
 
-       if ((fd = open("/proc/self/stat", O_RDONLY)) == -1)
+       if (snprintf(path, sizeof path, "/proc/%d/stat", pid) == -1)
+               return -1;
+
+       if ((fd = open(path, O_RDONLY)) == -1)
                return -1;
 
        while ((n = read(fd, p, buf + sizeof buf - p)) != 0) {
                if (n == -1) {
                        if (errno == EAGAIN || errno == EINTR)
                                continue;
-                       else
-                               break;
+                       break;
                }
                p += n;
                if (p >= buf + sizeof buf)
                        break;
        }
        close(fd);
+
        /* error if it contains NULL bytes */
        if (n != 0 || memchr(buf, '\0', p - buf))
                return -1;
 
        /* Get the 7th field, 5 fields after the last ')',
-        * because the 5th field 'comm' can include spaces
-        * and closing paranthesis too.
+        * (2th field) because the 5th field 'comm' can include
+        * spaces and closing paranthesis too.
         * See https://www.sudo.ws/alerts/linux_tty.html
         */
        if ((p = strrchr(buf, ')')) == NULL)
                return -1;
-       for ((p1 = strtok_r(p, " ", &saveptr)), n = 0; p1;
-           (p1 = strtok_r(NULL, " ", &saveptr)), n++)
-               if (n == 5)
-                       break;
-       if (p1 == NULL || n != 5)
-               return -1;
 
-       n = strtonum(p1, INT_MIN, INT_MAX, &errstr);
-       if (errstr)
-               return -1;
+       n = 2;
+       for ((p = strtok_r(p, " ", &saveptr)); p;
+           (p = strtok_r(NULL, " ", &saveptr))) {
+               switch (n++) {
+               case 7:
+                       *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
+                       if (errstr)
+                               return -1;
+                       break;
+               case 22:
+                       errno = 0;
+                       *starttime = strtoull(p, &ep, 10);
+                       if (p == ep ||
+                          (errno == ERANGE && *starttime == ULLONG_MAX))
+                               return -1;
+                       break;
+               }
+               if (n == 23)
+                       break;
+       }
 
-       return n;
+       return 0;
 }
 #else
-#error "ttynr not implemented"
+#error "proc_info not implemented"
 #endif
 
-static char pathbuf[PATH_MAX];
+static const char *
+tsname()
+{
+       static char buf[128];
+       int tty, fd;
+       unsigned long long starttime;
+       pid_t ppid, sid, leader;
+       if ((fd = open("/dev/tty", O_RDONLY)) == -1)
+               err(1, "open: /dev/tty");
+       if (ioctl(fd, TIOCGSID, &leader) == -1)
+               err(1, "ioctl: failed to get session leader");
+       close(fd);
+       if (proc_info(leader, &tty, &starttime) == -1)
+               errx(1, "failed to get tty number");
+       ppid = getppid();
+       if ((sid = getsid(0)) == -1)
+               err(1, "getsid");
+       if (snprintf(buf, sizeof buf, ".%d_%d_%llu_%d_%d",
+           tty, leader, starttime, ppid, sid) == -1)
+               return NULL;
+       return buf;
+}
 
 static int
-tspath(const char **path)
+checktsdir(int fd)
 {
-       int tty;
-       pid_t ppid;
-       if (*pathbuf == '\0') {
-               if ((tty = ttynr()) == -1)
-                       errx(1, "failed to get tty number");
-               ppid = getppid();
-               if (snprintf(pathbuf, sizeof pathbuf, "%s/.%d_%d",
-                   TIMESTAMP_DIR, tty, ppid) == -1)
-                       return -1;
-       }
-       *path = pathbuf;
+       struct stat st;
+
+       if (fstat(fd, &st) == -1)
+               err(1, "fstatat");
+
+       if ((st.st_mode & S_IFMT) != S_IFDIR)
+               errx(0, "timestamp directory is not a directory");
+       if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
+               errx(1, "timestamp directory permissions wrong");
+       if (st.st_uid != 0 || st.st_gid != 0)
+               errx(1, "timestamp directory is not owned by root");
+
+#if defined(TIMESTAMP_TMPFS) && defined(__linux__)
+       struct statfs sf;
+       if (fstatfs(fd, &sf) == -1)
+               err(1, "statfs");
+
+       if (sf.f_type != TMPFS_MAGIC)
+               errx(1, "timestamp directory not on tmpfs");
+#endif
+
        return 0;
 }
 
 static int
-checktsdir(const char *path)
+opentsdir()
 {
-       char *dir, *buf;
-       struct stat st;
-       struct statfs sf;
        gid_t gid;
+       int fd;
 
-       if (!(buf = strdup(path)))
-               err(1, "strdup");
-       dir = dirname(buf);
-
-check:
-       if (lstat(dir, &st) == -1) {
+reopen:
+       if ((fd = open(TIMESTAMP_DIR, O_RDONLY | O_DIRECTORY)) == -1) {
                if (errno == ENOENT) {
                        gid = getegid();
                        if (setegid(0) != 0)
                                err(1, "setegid");
-                       if (mkdir(dir, (S_IRUSR|S_IWUSR|S_IXUSR)) != 0)
+                       if (mkdir(TIMESTAMP_DIR, (S_IRUSR|S_IWUSR|S_IXUSR)) != 0)
                                err(1, "mkdir");
                        if (setegid(gid) != 0)
                                err(1, "setegid");
-                       goto check;
+                       goto reopen;
                } else {
-                       err(1, "stat");
+                       err(1, "failed to open timestamp directory: %s", TIMESTAMP_DIR);
                }
        }
 
-       if ((st.st_mode & S_IFMT) != S_IFDIR)
-               errx(1, "timestamp directory is not a directory");
+       if (checktsdir(fd) != 0)
+               return -1;
+
+       return fd;
+}
+
+static int
+checktsfile(int fd, size_t *tssize)
+{
+       struct stat st;
+       gid_t gid;
+
+       if (fstat(fd, &st) == -1)
+               err(1, "stat");
+       if ((st.st_mode & S_IFMT) != S_IFREG)
+               errx(1, "timestamp is not a regular file");
        if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
-               errx(1, "timestamp directory permissions wrong");
-       if (st.st_uid != 0 || st.st_gid != 0)
-               errx(1, "timestamp directory is not owned by root");
-       if (statfs(dir, &sf) == -1)
-               err(1, "statfs");
-       if (sf.f_type != TMPFS_MAGIC)
-               errx(1, "timestamp directory not on tmpfs");
+               errx(1, "timestamp has wrong permissions");
+
+       gid = getegid();
+       if (st.st_uid != 0 || st.st_gid != gid)
+               errx(1, "timestamp has wrong owner");
+
+       *tssize = st.st_size;
 
-       free(buf);
        return 0;
 }
 
-int
-persist_check(int fd, int secs)
+static int
+validts(int fd, int secs)
 {
        struct timespec mono, real, ts_mono, ts_real, timeout;
 
        if (read(fd, &ts_mono, sizeof ts_mono) != sizeof ts_mono ||
-           read(fd, &ts_real, sizeof ts_real) != sizeof ts_mono)
+           read(fd, &ts_real, sizeof ts_real) != sizeof ts_real)
                err(1, "read");
        if (!timespecisset(&ts_mono) || !timespecisset(&ts_real))
                errx(1, "timespecisset");
@@ -217,58 +307,56 @@ persist_set(int fd, int secs)
 int
 persist_open(int *valid, int secs)
 {
-       struct stat st;
-       int fd;
-       gid_t gid;
-       const char *path;
+       int dirfd, fd;
+       const char *name;
+
+       *valid = 0;
 
-       if (tspath(&path) == -1)
-               errx(1, "failed to get timestamp path");
-       if (checktsdir(path))
-               errx(1, "checktsdir");
+       if ((name = tsname()) == NULL)
+               errx(1, "failed to get timestamp name");
+       if ((dirfd = opentsdir()) == -1)
+               errx(1, "opentsdir");
 
-       if ((fd = open(path, (O_RDWR), (S_IRUSR|S_IWUSR))) == -1)
+       if ((fd = openat(dirfd, name, (O_RDWR), (S_IRUSR|S_IWUSR))) == -1)
                if (errno != ENOENT)
-                       err(1, "open: %s", path);
+                       err(1, "open timestamp file");
 
        if (fd == -1) {
-               if ((fd = open(path, (O_RDWR|O_CREAT|O_EXCL), (S_IRUSR|S_IWUSR))) == -1)
-                       err(1, "open: %s", path);
-               *valid = 0;
-               return fd;
+               if ((fd = openat(dirfd, name, (O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW),
+                   (S_IRUSR|S_IWUSR))) == -1)
+                       err(1, "open timestamp file");
        }
 
-       if (fstat(fd, &st) == -1)
-               err(1, "stat");
-       if ((st.st_mode & S_IFMT) != S_IFREG)
-               errx(1, "timestamp is not a file");
-       if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
-               errx(1, "timestamp permissions wrong");
-
-       gid = getegid();
-       if (st.st_uid != 0 || st.st_gid != gid)
-               errx(1, "timestamp has wrong owner");
-
-       if (st.st_size == 0) {
-               *valid = 0;
-               return fd;
-       }
+       size_t tssize;
+       if (checktsfile(fd, &tssize) == -1)
+               err(1, "checktsfile");
 
-       if (st.st_size != sizeof(struct timespec) * 2)
+       /* The timestamp size is 0 if its a new file or a
+        * timestamp that was never set, its not valid but
+        * can be used to write the new timestamp.
+        * If the size does not match the expected size it
+        * is incomplete and should never be used
+        */
+       if (tssize == sizeof(struct timespec) * 2)
+               *valid = validts(fd, secs) == 0;
+       else if (tssize != 0)
                errx(1, "corrupt timestamp file");
 
-       *valid = persist_check(fd, secs) == 0;
-
+       close(dirfd);
        return fd;
 }
 
 int
 persist_clear()
 {
-       const char *path;
-       if (tspath(&path) == -1)
-               errx(1, "failed to get timestamp path");
-       if (unlink(path) == -1 && errno != ENOENT)
+       const char *name;
+       int dirfd;
+       if ((name = tsname()) == NULL)
+               errx(1, "failed to get timestamp name");
+       if ((dirfd = opentsdir()) == -1)
+               errx(1, "opentsdir");
+       if (unlinkat(dirfd, name, 0) == -1 && errno != ENOENT)
                return -1;
+       close(dirfd);
        return 0;
 }