]> git.armaanb.net Git - opendoas.git/blob - persist_timestamp.c
doas.c: put login_style in ifdef to compile on Linux
[opendoas.git] / persist_timestamp.c
1 /*
2  * 1) Timestamp files and directories
3  *
4  * Timestamp files MUST NOT be accessible to users other than root,
5  * this includes the name, metadata and the content of timestamp files
6  * and directories.
7  *
8  * Symlinks can be used to create, manipulate or delete wrong files
9  * and directories. The Implementation MUST reject any symlinks for
10  * timestamp files or directories.
11  *
12  * To avoid race conditions the implementation MUST use the same
13  * file descriptor for permission checks and do read or write
14  * write operations after the permission checks.
15  *
16  * The timestamp files MUST be opened with openat(2) using the
17  * timestamp directory file descriptor. Permissions of the directory
18  * MUST be checked before opening the timestamp file descriptor.
19  *
20  * 2) Clock sources for timestamps
21  *
22  * Timestamp files MUST NOT rely on only one clock source, using the
23  * wall clock would allow to reset the clock to an earlier point in
24  * time to reuse a timestamp.
25  *
26  * The timestamp MUST consist of multiple clocks and MUST reject the
27  * timestamp if there is a change to any clock because there is no way
28  * to differentiate between malicious and legitimate clock changes.
29  *
30  * 3) Timestamp lifetime
31  *
32  * The implementation MUST NOT use the user controlled stdin, stdout
33  * and stderr file descriptors to determine the controlling terminal.
34  * On linux the /proc/$pid/stat file MUST be used to get the terminal
35  * number.
36  *
37  * There is no reliable way to determine the lifetime of a tty/pty.
38  * The start time of the session leader MUST be used as part of the
39  * timestamp to determine if the tty is still the same.
40  * If the start time of the session leader changed the timestamp MUST
41  * be rejected.
42  *
43  */
44
45 #include <sys/ioctl.h>
46 #include <sys/stat.h>
47 #include <sys/vfs.h>
48
49 #if !defined(timespecisset) || \
50     !defined(timespeccmp) || \
51     !defined(timespecadd)
52 #       include "sys-time.h"
53 #endif
54
55 #include <ctype.h>
56 #include <err.h>
57 #include <errno.h>
58 #include <fcntl.h>
59 #include <limits.h>
60 #include <stdio.h>
61 #include <stdlib.h>
62 #include <string.h>
63 #include <time.h>
64 #include <unistd.h>
65
66 #include "includes.h"
67
68 #ifndef TIMESTAMP_DIR
69 #       define TIMESTAMP_DIR "/tmp/doas"
70 #endif
71
72 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
73 #       ifndef TMPFS_MAGIC
74 #               define TMPFS_MAGIC 0x01021994
75 #       endif
76 #endif
77
78 #ifdef __linux__
79 /* Use tty_nr from /proc/self/stat instead of using
80  * ttyname(3), stdin, stdout and stderr are user
81  * controllable and would allow to reuse timestamps
82  * from another writable terminal.
83  * See https://www.sudo.ws/alerts/tty_tickets.html
84  */
85 static int
86 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
87 {
88         char path[128];
89         char buf[1024];
90         char *p, *saveptr, *ep;
91         const char *errstr;
92         int fd, n;
93
94         p = buf;
95
96         if (snprintf(path, sizeof path, "/proc/%d/stat", pid) == -1)
97                 return -1;
98
99         if ((fd = open(path, O_RDONLY)) == -1)
100                 return -1;
101
102         while ((n = read(fd, p, buf + sizeof buf - p)) != 0) {
103                 if (n == -1) {
104                         if (errno == EAGAIN || errno == EINTR)
105                                 continue;
106                         break;
107                 }
108                 p += n;
109                 if (p >= buf + sizeof buf)
110                         break;
111         }
112         close(fd);
113
114         /* error if it contains NULL bytes */
115         if (n != 0 || memchr(buf, '\0', p - buf))
116                 return -1;
117
118         /* Get the 7th field, 5 fields after the last ')',
119          * (2th field) because the 5th field 'comm' can include
120          * spaces and closing paranthesis too.
121          * See https://www.sudo.ws/alerts/linux_tty.html
122          */
123         if ((p = strrchr(buf, ')')) == NULL)
124                 return -1;
125
126         n = 2;
127         for ((p = strtok_r(p, " ", &saveptr)); p;
128             (p = strtok_r(NULL, " ", &saveptr))) {
129                 switch (n++) {
130                 case 7:
131                         *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
132                         if (errstr)
133                                 return -1;
134                         break;
135                 case 22:
136                         errno = 0;
137                         *starttime = strtoull(p, &ep, 10);
138                         if (p == ep ||
139                            (errno == ERANGE && *starttime == ULLONG_MAX))
140                                 return -1;
141                         break;
142                 }
143                 if (n == 23)
144                         break;
145         }
146
147         return 0;
148 }
149 #else
150 #error "proc_info not implemented"
151 #endif
152
153 static const char *
154 tsname()
155 {
156         static char buf[128];
157         int tty, fd;
158         unsigned long long starttime;
159         pid_t ppid, sid, leader;
160         if ((fd = open("/dev/tty", O_RDONLY)) == -1)
161                 err(1, "open: /dev/tty");
162         if (ioctl(fd, TIOCGSID, &leader) == -1)
163                 err(1, "ioctl: failed to get session leader");
164         close(fd);
165         if (proc_info(leader, &tty, &starttime) == -1)
166                 errx(1, "failed to get tty number");
167         ppid = getppid();
168         if ((sid = getsid(0)) == -1)
169                 err(1, "getsid");
170         if (snprintf(buf, sizeof buf, ".%d_%d_%llu_%d_%d",
171             tty, leader, starttime, ppid, sid) == -1)
172                 return NULL;
173         return buf;
174 }
175
176 static int
177 checktsdir(int fd)
178 {
179         struct stat st;
180
181         if (fstat(fd, &st) == -1)
182                 err(1, "fstatat");
183
184         if ((st.st_mode & S_IFMT) != S_IFDIR)
185                 errx(0, "timestamp directory is not a directory");
186         if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
187                 errx(1, "timestamp directory permissions wrong");
188         if (st.st_uid != 0 || st.st_gid != 0)
189                 errx(1, "timestamp directory is not owned by root");
190
191 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
192         struct statfs sf;
193         if (fstatfs(fd, &sf) == -1)
194                 err(1, "statfs");
195
196         if (sf.f_type != TMPFS_MAGIC)
197                 errx(1, "timestamp directory not on tmpfs");
198 #endif
199
200         return 0;
201 }
202
203 static int
204 opentsdir()
205 {
206         gid_t gid;
207         int fd;
208
209 reopen:
210         if ((fd = open(TIMESTAMP_DIR, O_RDONLY | O_DIRECTORY)) == -1) {
211                 if (errno == ENOENT) {
212                         gid = getegid();
213                         if (setegid(0) != 0)
214                                 err(1, "setegid");
215                         if (mkdir(TIMESTAMP_DIR, (S_IRUSR|S_IWUSR|S_IXUSR)) != 0)
216                                 err(1, "mkdir");
217                         if (setegid(gid) != 0)
218                                 err(1, "setegid");
219                         goto reopen;
220                 } else {
221                         err(1, "failed to open timestamp directory: %s", TIMESTAMP_DIR);
222                 }
223         }
224
225         if (checktsdir(fd) != 0)
226                 return -1;
227
228         return fd;
229 }
230
231 static int
232 checktsfile(int fd, size_t *tssize)
233 {
234         struct stat st;
235         gid_t gid;
236
237         if (fstat(fd, &st) == -1)
238                 err(1, "stat");
239         if ((st.st_mode & S_IFMT) != S_IFREG)
240                 errx(1, "timestamp is not a regular file");
241         if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
242                 errx(1, "timestamp has wrong permissions");
243
244         gid = getegid();
245         if (st.st_uid != 0 || st.st_gid != gid)
246                 errx(1, "timestamp has wrong owner");
247
248         *tssize = st.st_size;
249
250         return 0;
251 }
252
253 static int
254 validts(int fd, int secs)
255 {
256         struct timespec mono, real, ts_mono, ts_real, timeout;
257
258         if (read(fd, &ts_mono, sizeof ts_mono) != sizeof ts_mono ||
259             read(fd, &ts_real, sizeof ts_real) != sizeof ts_real)
260                 err(1, "read");
261         if (!timespecisset(&ts_mono) || !timespecisset(&ts_real))
262                 errx(1, "timespecisset");
263
264         if (clock_gettime(CLOCK_MONOTONIC_RAW, &mono) == -1 ||
265             clock_gettime(CLOCK_REALTIME, &real) == -1)
266                 err(1, "clock_gettime");
267
268         if (timespeccmp(&mono, &ts_mono, >) ||
269             timespeccmp(&real, &ts_real, >))
270                 return -1;
271
272         memset(&timeout, 0, sizeof timeout);
273         timeout.tv_sec = secs;
274         timespecadd(&timeout, &mono, &mono);
275         timespecadd(&timeout, &real, &real);
276
277         if (timespeccmp(&mono, &ts_mono, <) ||
278             timespeccmp(&real, &ts_real, <))
279                 errx(1, "timestamp is too far in the future");
280
281         return 0;
282 }
283
284 int
285 persist_set(int fd, int secs)
286 {
287         struct timespec mono, real, ts_mono, ts_real, timeout;
288
289         if (clock_gettime(CLOCK_MONOTONIC_RAW, &mono) == -1 ||
290             clock_gettime(CLOCK_REALTIME, &real) == -1)
291                 err(1, "clock_gettime");
292
293         memset(&timeout, 0, sizeof timeout);
294         timeout.tv_sec = secs;
295         timespecadd(&timeout, &mono, &ts_mono);
296         timespecadd(&timeout, &real, &ts_real);
297
298         if (lseek(fd, 0, SEEK_SET) == -1)
299                 err(1, "lseek");
300         if (write(fd, (void *)&ts_mono, sizeof ts_mono) != sizeof ts_mono ||
301             write(fd, (void *)&ts_real, sizeof ts_real) != sizeof ts_real)
302                 err(1, "write");
303
304         return 0;
305 }
306
307 int
308 persist_open(int *valid, int secs)
309 {
310         int dirfd, fd;
311         const char *name;
312
313         *valid = 0;
314
315         if ((name = tsname()) == NULL)
316                 errx(1, "failed to get timestamp name");
317         if ((dirfd = opentsdir()) == -1)
318                 errx(1, "opentsdir");
319
320         if ((fd = openat(dirfd, name, (O_RDWR), (S_IRUSR|S_IWUSR))) == -1)
321                 if (errno != ENOENT)
322                         err(1, "open timestamp file");
323
324         if (fd == -1) {
325                 if ((fd = openat(dirfd, name, (O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW),
326                     (S_IRUSR|S_IWUSR))) == -1)
327                         err(1, "open timestamp file");
328         }
329
330         size_t tssize;
331         if (checktsfile(fd, &tssize) == -1)
332                 err(1, "checktsfile");
333
334         /* The timestamp size is 0 if its a new file or a
335          * timestamp that was never set, its not valid but
336          * can be used to write the new timestamp.
337          * If the size does not match the expected size it
338          * is incomplete and should never be used
339          */
340         if (tssize == sizeof(struct timespec) * 2)
341                 *valid = validts(fd, secs) == 0;
342         else if (tssize != 0)
343                 errx(1, "corrupt timestamp file");
344
345         close(dirfd);
346         return fd;
347 }
348
349 int
350 persist_clear()
351 {
352         const char *name;
353         int dirfd;
354         if ((name = tsname()) == NULL)
355                 errx(1, "failed to get timestamp name");
356         if ((dirfd = opentsdir()) == -1)
357                 errx(1, "opentsdir");
358         if (unlinkat(dirfd, name, 0) == -1 && errno != ENOENT)
359                 return -1;
360         close(dirfd);
361         return 0;
362 }