2 * Copyright (c) 2020 Duncan Overbruck <mail@duncano.de>
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 * 1) Timestamp files and directories
22 * Timestamp files MUST NOT be accessible to users other than root,
23 * this includes the name, metadata and the content of timestamp files
26 * Symlinks can be used to create, manipulate or delete wrong files
27 * and directories. The Implementation MUST reject any symlinks for
28 * timestamp files or directories.
30 * To avoid race conditions the implementation MUST use the same
31 * file descriptor for permission checks and do read or write
32 * write operations after the permission checks.
34 * The timestamp files MUST be opened with openat(2) using the
35 * timestamp directory file descriptor. Permissions of the directory
36 * MUST be checked before opening the timestamp file descriptor.
38 * 2) Clock sources for timestamps
40 * Timestamp files MUST NOT rely on only one clock source, using the
41 * wall clock would allow to reset the clock to an earlier point in
42 * time to reuse a timestamp.
44 * The timestamp MUST consist of multiple clocks and MUST reject the
45 * timestamp if there is a change to any clock because there is no way
46 * to differentiate between malicious and legitimate clock changes.
48 * 3) Timestamp lifetime
50 * The implementation MUST NOT use the user controlled stdin, stdout
51 * and stderr file descriptors to determine the controlling terminal.
52 * On linux the /proc/$pid/stat file MUST be used to get the terminal
55 * There is no reliable way to determine the lifetime of a tty/pty.
56 * The start time of the session leader MUST be used as part of the
57 * timestamp to determine if the tty is still the same.
58 * If the start time of the session leader changed the timestamp MUST
63 #include <sys/ioctl.h>
67 #if !defined(timespecisset) || \
68 !defined(timespeccmp) || \
70 # include "sys-time.h"
89 # define TIMESTAMP_DIR "/run/doas"
92 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
94 # define TMPFS_MAGIC 0x01021994
99 /* Use tty_nr from /proc/self/stat instead of using
100 * ttyname(3), stdin, stdout and stderr are user
101 * controllable and would allow to reuse timestamps
102 * from another writable terminal.
103 * See https://www.sudo.ws/alerts/tty_tickets.html
106 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
110 char *p, *saveptr, *ep;
116 n = snprintf(path, sizeof path, "/proc/%d/stat", pid);
117 if (n < 0 || n >= (int)sizeof path)
120 if ((fd = open(path, O_RDONLY|O_NOFOLLOW)) == -1) {
121 warn("failed to open: %s", path);
125 while ((n = read(fd, p, buf + (sizeof buf - 1) - p)) != 0) {
127 if (errno == EAGAIN || errno == EINTR)
129 warn("read: %s", path);
134 if (p >= buf + (sizeof buf - 1))
139 /* error if it contains NULL bytes */
140 if (n != 0 || memchr(buf, '\0', p - buf - 1) != NULL) {
141 warn("NUL in: %s", path);
147 /* Get the 7th field, 5 fields after the last ')',
148 * (2th field) because the 5th field 'comm' can include
149 * spaces and closing paranthesis too.
150 * See https://www.sudo.ws/alerts/linux_tty.html
152 if ((p = strrchr(buf, ')')) == NULL)
156 for ((p = strtok_r(p, " ", &saveptr)); p;
157 (p = strtok_r(NULL, " ", &saveptr))) {
160 *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
166 *starttime = strtoull(p, &ep, 10);
168 (errno == ERANGE && *starttime == ULLONG_MAX))
177 #error "proc_info not implemented"
181 timestamp_path(char *buf, size_t len)
184 unsigned long long starttime;
188 if ((sid = getsid(0)) == -1)
190 if (proc_info(ppid, &ttynr, &starttime) == -1)
192 n = snprintf(buf, len, TIMESTAMP_DIR "/%d-%d-%d-%llu-%d",
193 ppid, sid, ttynr, starttime, getuid());
194 if (n < 0 || n >= (int)len)
200 timestamp_set(int fd, int secs)
202 struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
204 if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
205 clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
208 timespecadd(&ts[0], &timeout, &ts[0]);
209 timespecadd(&ts[1], &timeout, &ts[1]);
210 return futimens(fd, ts);
214 * Returns 1 if the timestamp is valid, 0 if its invalid
217 timestamp_check(int fd, int secs)
219 struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
222 if (fstat(fd, &st) == -1)
224 if (st.st_uid != 0 || st.st_gid != getgid() || st.st_mode != (S_IFREG | 0000))
225 errx(1, "timestamp uid, gid or mode wrong");
227 /* this timestamp was created but never set, invalid but no error */
228 if (!timespecisset(&st.st_atim) || !timespecisset(&st.st_mtim))
231 if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
232 clock_gettime(CLOCK_REALTIME, &ts[1]) == -1) {
233 warn("clock_gettime");
237 /* check if timestamp is too old */
238 if (timespeccmp(&st.st_atim, &ts[0], <) ||
239 timespeccmp(&st.st_mtim, &ts[1], <))
242 /* check if timestamp is too far in the future */
243 timespecadd(&ts[0], &timeout, &ts[0]);
244 timespecadd(&ts[1], &timeout, &ts[1]);
245 if (timespeccmp(&st.st_atim, &ts[0], >) ||
246 timespeccmp(&st.st_mtim, &ts[1], >)) {
247 warnx("timestamp too far in the future");
255 timestamp_open(int *valid, int secs)
257 struct timespec ts[2] = {0};
265 if (stat(TIMESTAMP_DIR, &st) == -1) {
268 if (mkdir(TIMESTAMP_DIR, 0700) == -1)
270 } else if (st.st_uid != 0 || st.st_mode != (S_IFDIR | 0700)) {
274 if (timestamp_path(path, sizeof path) == -1)
277 fd = open(path, O_RDONLY|O_NOFOLLOW);
283 err(1, "open: %s", path);
285 n = snprintf(tmp, sizeof tmp, TIMESTAMP_DIR "/.tmp-%d", getpid());
286 if (n < 0 || n >= (int)sizeof tmp)
289 fd = open(tmp, O_RDONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0000);
292 if (futimens(fd, ts) == -1 || rename(tmp, path) == -1) {
300 *valid = timestamp_check(fd, secs);
310 if (timestamp_path(path, sizeof path) == -1)
312 if (unlink(path) == -1 && errno != ENOENT)