]> git.armaanb.net Git - opendoas.git/blob - timestamp.c
c3606ee7d8b45996124cc1eb3cc8a5b4a9a1be70
[opendoas.git] / timestamp.c
1 /*
2  * Copyright (c) 2020 Duncan Overbruck <mail@duncano.de>
3  *
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.
7  *
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.
15  */
16
17 #include "config.h"
18
19 /*
20  * 1) Timestamp files and directories
21  *
22  * Timestamp files MUST NOT be accessible to users other than root,
23  * this includes the name, metadata and the content of timestamp files
24  * and directories.
25  *
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.
29  *
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.
33  *
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.
37  *
38  * 2) Clock sources for timestamps
39  *
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.
43  *
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.
47  *
48  * 3) Timestamp lifetime
49  *
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
53  * number.
54  *
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
59  * be rejected.
60  *
61  */
62
63 #include <sys/ioctl.h>
64 #include <sys/stat.h>
65 #include <sys/vfs.h>
66
67 #if !defined(timespecisset) || \
68     !defined(timespeccmp) || \
69     !defined(timespecadd)
70 #       include "sys-time.h"
71 #endif
72
73 #include <ctype.h>
74 #include <dirent.h>
75 #include <err.h>
76 #include <errno.h>
77 #include <fcntl.h>
78 #include <limits.h>
79 #include <stdio.h>
80 #include <stdlib.h>
81 #include <string.h>
82 #include <time.h>
83 #include <unistd.h>
84
85 #include "includes.h"
86
87 #ifndef TIMESTAMP_DIR
88 #       define TIMESTAMP_DIR "/run/doas"
89 #endif
90
91 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
92 #       ifndef TMPFS_MAGIC
93 #               define TMPFS_MAGIC 0x01021994
94 #       endif
95 #endif
96
97 #ifdef __linux__
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
103  */
104 static int
105 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
106 {
107         char path[128];
108         char buf[1024];
109         char *p, *saveptr, *ep;
110         const char *errstr;
111         int fd, n;
112
113         p = buf;
114
115         n = snprintf(path, sizeof path, "/proc/%d/stat", pid);
116         if (n < 0 || n >= (int)sizeof path)
117                 return -1;
118
119         if ((fd = open(path, O_RDONLY|O_NOFOLLOW)) == -1) {
120                 warn("failed to open: %s", path);
121                 return -1;
122         }
123
124         while ((n = read(fd, p, buf + (sizeof buf - 1) - p)) != 0) {
125                 if (n == -1) {
126                         if (errno == EAGAIN || errno == EINTR)
127                                 continue;
128                         warn("read: %s", path);
129                         close(fd);
130                         return -1;
131                 }
132                 p += n;
133                 if (p >= buf + (sizeof buf - 1))
134                         break;
135         }
136         close(fd);
137
138         /* error if it contains NULL bytes */
139         if (n != 0 || memchr(buf, '\0', p - buf - 1) != NULL) {
140                 warn("NUL in: %s", path);
141                 return -1;
142         }
143
144         *p = '\0';
145
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
150          */
151         if ((p = strrchr(buf, ')')) == NULL)
152                 return -1;
153
154         n = 2;
155         for ((p = strtok_r(p, " ", &saveptr)); p;
156             (p = strtok_r(NULL, " ", &saveptr))) {
157                 switch (n++) {
158                 case 7:
159                         *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
160                         if (errstr)
161                                 return -1;
162                         break;
163                 case 22:
164                         errno = 0;
165                         *starttime = strtoull(p, &ep, 10);
166                         if (p == ep ||
167                            (errno == ERANGE && *starttime == ULLONG_MAX))
168                                 return -1;
169                         return 0;
170                 }
171         }
172
173         return -1;
174 }
175 #else
176 #error "proc_info not implemented"
177 #endif
178
179 static int
180 timestamp_path(char *buf, size_t len)
181 {
182         pid_t ppid, sid;
183         unsigned long long starttime;
184         int n, ttynr;
185
186         ppid = getppid();
187         if ((sid = getsid(0)) == -1)
188                 return -1;
189         if (proc_info(ppid, &ttynr, &starttime) == -1)
190                 return -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)
194                 return -1;
195         return 0;
196 }
197
198 int
199 timestamp_set(int fd, int secs)
200 {
201         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
202
203         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
204             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
205                 return -1;
206
207         timespecadd(&ts[0], &timeout, &ts[0]);
208         timespecadd(&ts[1], &timeout, &ts[1]);
209         return futimens(fd, ts);
210 }
211
212 /*
213  * Returns 1 if the timestamp is valid, 0 if its invalid
214  */
215 static int
216 timestamp_check(int fd, int secs)
217 {
218         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
219         struct stat st;
220
221         if (fstat(fd, &st) == -1)
222                 err(1, "fstat");
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");
225
226         /* this timestamp was created but never set, invalid but no error */
227         if (!timespecisset(&st.st_atim) || !timespecisset(&st.st_mtim))
228                 return 0;
229
230         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
231             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1) {
232                 warn("clock_gettime");
233                 return 0;
234         }
235
236         /* check if timestamp is too old */
237         if (timespeccmp(&st.st_atim, &ts[0], <) ||
238             timespeccmp(&st.st_mtim, &ts[1], <))
239                 return 0;
240
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");
247                 return 0;
248         }
249
250         return 1;
251 }
252
253 int
254 timestamp_open(int *valid, int secs)
255 {
256         struct timespec ts[2] = {0};
257         struct stat st;
258         int fd;
259         char path[256];
260         int serrno = 0;
261
262         *valid = 0;
263
264         if (stat(TIMESTAMP_DIR, &st) == -1) {
265                 if (errno != ENOENT)
266                         return -1;
267                 if (mkdir(TIMESTAMP_DIR, 0700) == -1)
268                         return -1;
269         } else if (st.st_uid != 0 || st.st_mode != (S_IFDIR | 0700)) {
270                 return -1;
271         }
272
273         if (timestamp_path(path, sizeof path) == -1)
274                 return -1;
275
276         fd = open(path, O_RDONLY|O_NOFOLLOW);
277         if (fd == -1) {
278                 char tmp[256];
279                 int n;
280
281                 if (errno != ENOENT)
282                         err(1, "open: %s", path);
283
284                 n = snprintf(tmp, sizeof tmp, TIMESTAMP_DIR "/.tmp-%d", getpid());
285                 if (n < 0 || n >= (int)sizeof tmp)
286                         return -1;
287
288                 fd = open(tmp, O_RDONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0000);
289                 if (fd == -1)
290                         return -1;
291                 if (futimens(fd, ts) == -1 || rename(tmp, path) == -1) {
292                         serrno = errno;
293                         close(fd);
294                         unlink(tmp);
295                         errno = serrno;
296                         return -1;
297                 }
298         } else {
299                 *valid = timestamp_check(fd, secs);
300         }
301         return fd;
302 }
303
304 int
305 timestamp_clear()
306 {
307         char path[256];
308
309         if (timestamp_path(path, sizeof path) == -1)
310                 return -1;
311         if (unlink(path) == -1 && errno != ENOENT)
312                 return -1;
313         return 0;
314 }