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"
88 # define TIMESTAMP_DIR "/run/doas"
91 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
93 # define TMPFS_MAGIC 0x01021994
98 /* Use tty_nr from /proc/self/stat instead of using
99 * ttyname(3), stdin, stdout and stderr are user
100 * controllable and would allow to reuse timestamps
101 * from another writable terminal.
102 * See https://www.sudo.ws/alerts/tty_tickets.html
105 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
109 char *p, *saveptr, *ep;
115 n = snprintf(path, sizeof path, "/proc/%d/stat", pid);
116 if (n < 0 || n >= (int)sizeof path)
119 if ((fd = open(path, O_RDONLY|O_NOFOLLOW)) == -1) {
120 warn("failed to open: %s", path);
124 while ((n = read(fd, p, buf + (sizeof buf - 1) - p)) != 0) {
126 if (errno == EAGAIN || errno == EINTR)
128 warn("read: %s", path);
133 if (p >= buf + (sizeof buf - 1))
138 /* error if it contains NULL bytes */
139 if (n != 0 || memchr(buf, '\0', p - buf - 1) != NULL) {
140 warn("NUL in: %s", path);
146 /* Get the 7th field, 5 fields after the last ')',
147 * (2th field) because the 5th field 'comm' can include
148 * spaces and closing paranthesis too.
149 * See https://www.sudo.ws/alerts/linux_tty.html
151 if ((p = strrchr(buf, ')')) == NULL)
155 for ((p = strtok_r(p, " ", &saveptr)); p;
156 (p = strtok_r(NULL, " ", &saveptr))) {
159 *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
165 *starttime = strtoull(p, &ep, 10);
167 (errno == ERANGE && *starttime == ULLONG_MAX))
176 #error "proc_info not implemented"
180 timestamp_path(char *buf, size_t len)
183 unsigned long long starttime;
187 if ((sid = getsid(0)) == -1)
189 if (proc_info(ppid, &ttynr, &starttime) == -1)
191 n = snprintf(buf, len, TIMESTAMP_DIR "/%d-%d-%d-%llu-%d",
192 ppid, sid, ttynr, starttime, getuid());
193 if (n < 0 || n >= (int)len)
199 timestamp_set(int fd, int secs)
201 struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
203 if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
204 clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
207 timespecadd(&ts[0], &timeout, &ts[0]);
208 timespecadd(&ts[1], &timeout, &ts[1]);
209 return futimens(fd, ts);
213 * Returns 1 if the timestamp is valid, 0 if its invalid
216 timestamp_check(int fd, int secs)
218 struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
221 if (fstat(fd, &st) == -1)
223 if (st.st_uid != 0 || st.st_gid != getgid() || st.st_mode != (S_IFREG | 0000))
224 errx(1, "timestamp uid, gid or mode wrong");
226 /* this timestamp was created but never set, invalid but no error */
227 if (!timespecisset(&st.st_atim) || !timespecisset(&st.st_mtim))
230 if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
231 clock_gettime(CLOCK_REALTIME, &ts[1]) == -1) {
232 warn("clock_gettime");
236 /* check if timestamp is too old */
237 if (timespeccmp(&st.st_atim, &ts[0], <) ||
238 timespeccmp(&st.st_mtim, &ts[1], <))
241 /* check if timestamp is too far in the future */
242 timespecadd(&ts[0], &timeout, &ts[0]);
243 timespecadd(&ts[1], &timeout, &ts[1]);
244 if (timespeccmp(&st.st_atim, &ts[0], >) ||
245 timespeccmp(&st.st_mtim, &ts[1], >)) {
246 warnx("timestamp too far in the future");
254 timestamp_open(int *valid, int secs)
256 struct timespec ts[2] = {0};
264 if (stat(TIMESTAMP_DIR, &st) == -1) {
267 if (mkdir(TIMESTAMP_DIR, 0700) == -1)
269 } else if (st.st_uid != 0 || st.st_mode != (S_IFDIR | 0700)) {
273 if (timestamp_path(path, sizeof path) == -1)
276 fd = open(path, O_RDONLY|O_NOFOLLOW);
282 err(1, "open: %s", path);
284 n = snprintf(tmp, sizeof tmp, TIMESTAMP_DIR "/.tmp-%d", getpid());
285 if (n < 0 || n >= (int)sizeof tmp)
288 fd = open(tmp, O_RDONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0000);
291 if (futimens(fd, ts) == -1 || rename(tmp, path) == -1) {
299 *valid = timestamp_check(fd, secs);
309 if (timestamp_path(path, sizeof path) == -1)
311 if (unlink(path) == -1 && errno != ENOENT)