]> git.armaanb.net Git - opendoas.git/blob - timestamp.c
timestamp: error out if fstat and lstat st_ino and st_dev are not the same
[opendoas.git] / 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 <dirent.h>
57 #include <err.h>
58 #include <errno.h>
59 #include <fcntl.h>
60 #include <limits.h>
61 #include <stdio.h>
62 #include <stdlib.h>
63 #include <string.h>
64 #include <time.h>
65 #include <unistd.h>
66
67 #include "includes.h"
68
69 #ifndef TIMESTAMP_DIR
70 #       define TIMESTAMP_DIR "/run/doas"
71 #endif
72
73 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
74 #       ifndef TMPFS_MAGIC
75 #               define TMPFS_MAGIC 0x01021994
76 #       endif
77 #endif
78
79 #ifdef __linux__
80 /* Use tty_nr from /proc/self/stat instead of using
81  * ttyname(3), stdin, stdout and stderr are user
82  * controllable and would allow to reuse timestamps
83  * from another writable terminal.
84  * See https://www.sudo.ws/alerts/tty_tickets.html
85  */
86 static int
87 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
88 {
89         char path[128];
90         char buf[1024];
91         char *p, *saveptr, *ep;
92         const char *errstr;
93         int fd, n;
94
95         p = buf;
96
97         if (snprintf(path, sizeof path, "/proc/%d/stat", pid) == -1)
98                 return -1;
99
100         if ((fd = open(path, O_RDONLY)) == -1)
101                 return -1;
102
103         while ((n = read(fd, p, buf + sizeof buf - p)) != 0) {
104                 if (n == -1) {
105                         if (errno == EAGAIN || errno == EINTR)
106                                 continue;
107                         close(fd);
108                         return -1;
109                 }
110                 p += n;
111                 if (p >= buf + sizeof buf)
112                         break;
113         }
114         close(fd);
115
116         /* error if it contains NULL bytes */
117         if (n != 0 || memchr(buf, '\0', p - buf))
118                 return -1;
119
120         /* Get the 7th field, 5 fields after the last ')',
121          * (2th field) because the 5th field 'comm' can include
122          * spaces and closing paranthesis too.
123          * See https://www.sudo.ws/alerts/linux_tty.html
124          */
125         if ((p = strrchr(buf, ')')) == NULL)
126                 return -1;
127
128         n = 2;
129         for ((p = strtok_r(p, " ", &saveptr)); p;
130             (p = strtok_r(NULL, " ", &saveptr))) {
131                 switch (n++) {
132                 case 7:
133                         *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
134                         if (errstr)
135                                 return -1;
136                         break;
137                 case 22:
138                         errno = 0;
139                         *starttime = strtoull(p, &ep, 10);
140                         if (p == ep ||
141                            (errno == ERANGE && *starttime == ULLONG_MAX))
142                                 return -1;
143                         break;
144                 }
145                 if (n == 23)
146                         break;
147         }
148
149         return 0;
150 }
151 #else
152 #error "proc_info not implemented"
153 #endif
154
155 /* The session prefix is the tty number, the pid
156  * of the session leader and the start time of the
157  * session leader.
158  */
159 static const char *
160 session_prefix()
161 {
162         static char prefix[128];
163         int tty, fd;
164         unsigned long long starttime;
165         pid_t leader;
166
167         if ((fd = open("/dev/tty", O_RDONLY)) == -1)
168                 err(1, "open: /dev/tty");
169         if (ioctl(fd, TIOCGSID, &leader) == -1)
170                 err(1, "ioctl: failed to get session leader");
171         close(fd);
172
173         if (proc_info(leader, &tty, &starttime) == -1)
174                 errx(1, "failed to get tty number");
175         if (snprintf(prefix, sizeof prefix, ".%d_%d_%llu_",
176             tty, leader, starttime) == -1)
177                 return NULL;
178         return prefix;
179 }
180
181 static const char *
182 timestamp_name()
183 {
184         static char name[128];
185         pid_t ppid, sid;
186         const char *prefix;
187
188         ppid = getppid();
189         if ((sid = getsid(0)) == -1)
190                 err(1, "getsid");
191         if ((prefix = session_prefix()) == NULL)
192                 return NULL;
193         if (snprintf(name, sizeof name, "%s_%d_%d", prefix, ppid, sid) == -1)
194                 return NULL;
195         return name;
196 }
197
198 static int
199 opentsdir()
200 {
201         struct stat st, stl;
202         int fd;
203         int isnew;
204
205         isnew = 0;
206
207
208 reopen:
209         if (lstat(TIMESTAMP_DIR, &stl) == -1) {
210                 if (!(errno == ENOENT && isnew == 0))
211                         err(1, "lstat: %s", TIMESTAMP_DIR);
212         } else if ((stl.st_mode & S_IFMT) != S_IFDIR) {
213                 errx(1, "%s: not a directory", TIMESTAMP_DIR);
214         }
215
216         if ((fd = open(TIMESTAMP_DIR, O_RDONLY | O_DIRECTORY)) == -1) {
217                 if (errno == ENOENT && isnew == 0) {
218                         if (mkdir(TIMESTAMP_DIR, (S_IRUSR|S_IWUSR|S_IXUSR)) != 0)
219                                 err(1, "mkdir");
220                         isnew = 1;
221                         goto reopen;
222                 } else {
223                         err(1, "failed to open timestamp directory: %s", TIMESTAMP_DIR);
224                 }
225         }
226
227 recheck:
228         if (fstat(fd, &st) == -1)
229                 err(1, "fstatat");
230
231         if (stl.st_ino != st.st_ino || stl.st_dev != st.st_dev)
232                 errx(1, "timestamp directory lstat and fstat are different files");
233
234         if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
235                 errx(1, "timestamp directory has wrong permissions");
236
237         if (isnew == 1) {
238                 if (fchown(fd, 0, 0) == -1)
239                         err(1, "fchown");
240                 isnew = 0;
241                 goto recheck;
242         } else {
243                 if (st.st_uid != 0 || st.st_gid != 0)
244                         errx(1, "timestamp directory has wrong owner or group");
245         }
246
247 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
248         struct statfs sf;
249         if (fstatfs(fd, &sf) == -1)
250                 err(1, "statfs");
251
252         if (sf.f_type != TMPFS_MAGIC)
253                 errx(1, "timestamp directory not on tmpfs");
254 #endif
255
256         return fd;
257 }
258
259 /*
260  * Returns 1 if the timestamp is valid, 0 if its invalid
261  */
262 static int
263 timestamp_valid(int fd, int secs)
264 {
265         struct timespec mono, real, ts_mono, ts_real, timeout;
266
267         if (read(fd, &ts_mono, sizeof ts_mono) != sizeof ts_mono ||
268             read(fd, &ts_real, sizeof ts_real) != sizeof ts_real)
269                 err(1, "read");
270         if (!timespecisset(&ts_mono) || !timespecisset(&ts_real))
271                 errx(1, "timespecisset");
272
273         if (clock_gettime(CLOCK_MONOTONIC_RAW, &mono) == -1 ||
274             clock_gettime(CLOCK_REALTIME, &real) == -1)
275                 err(1, "clock_gettime");
276
277         if (timespeccmp(&mono, &ts_mono, >) ||
278             timespeccmp(&real, &ts_real, >))
279                 return 0;
280
281         memset(&timeout, 0, sizeof timeout);
282         timeout.tv_sec = secs;
283         timespecadd(&timeout, &mono, &mono);
284         timespecadd(&timeout, &real, &real);
285
286         if (timespeccmp(&mono, &ts_mono, <) ||
287             timespeccmp(&real, &ts_real, <))
288                 errx(1, "timestamp is too far in the future");
289
290         return 1;
291 }
292
293 int
294 timestamp_set(int fd, int secs)
295 {
296         struct timespec mono, real, ts_mono, ts_real, timeout;
297
298         if (clock_gettime(CLOCK_MONOTONIC_RAW, &mono) == -1 ||
299             clock_gettime(CLOCK_REALTIME, &real) == -1)
300                 err(1, "clock_gettime");
301
302         memset(&timeout, 0, sizeof timeout);
303         timeout.tv_sec = secs;
304         timespecadd(&timeout, &mono, &ts_mono);
305         timespecadd(&timeout, &real, &ts_real);
306
307         if (lseek(fd, 0, SEEK_SET) == -1)
308                 err(1, "lseek");
309         if (ftruncate(fd, 0) == -1)
310                 err(1, "ftruncate");
311         if (write(fd, (void *)&ts_mono, sizeof ts_mono) != sizeof ts_mono ||
312             write(fd, (void *)&ts_real, sizeof ts_real) != sizeof ts_real)
313                 err(1, "write");
314
315         return 0;
316 }
317
318 int
319 timestamp_open(int *valid, int secs)
320 {
321         struct stat st, stl;
322         int dirfd, fd;
323         gid_t gid;
324         const char *name;
325
326         *valid = 0;
327
328         if ((name = timestamp_name()) == NULL)
329                 errx(1, "failed to get timestamp name");
330         if ((dirfd = opentsdir()) == -1)
331                 errx(1, "opentsdir");
332
333         if (fstatat(dirfd, name, &stl, AT_SYMLINK_NOFOLLOW) == -1) {
334                 if (errno != ENOENT)
335                         err(1, "fstatat");
336         } else if ((stl.st_mode & S_IFMT) != S_IFREG) {
337                 errx(1, "timestamp: not a regular file");
338         }
339
340         if ((fd = openat(dirfd, name, (O_RDWR), (S_IRUSR|S_IWUSR))) == -1)
341                 if (errno != ENOENT)
342                         err(1, "open timestamp file");
343
344         /*
345          * If the file was opened, check if fstat and lstat results are
346          * the same file.
347          * If the file doesn't exist and we create it with O_CREAT|O_EXCL,
348          * it is already known that the file is a regular file.
349          */
350         if (fd != -1) {
351                 if (fstat(fd, &st) == -1)
352                         err(1, "stat");
353                 if (stl.st_ino != st.st_ino || stl.st_dev != st.st_dev)
354                         errx(1, "timestamp file lstat and fstat are different files");
355         } else {
356                 if ((fd = openat(dirfd, name, (O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW),
357                     (S_IRUSR|S_IWUSR))) == -1)
358                         err(1, "open timestamp file");
359                 if (fstat(fd, &st) == -1)
360                         err(1, "stat");
361         }
362
363         if ((st.st_mode & (S_IWGRP|S_IRGRP|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH)) != 0)
364                 errx(1, "timestamp file has wrong permissions");
365
366         gid = getegid();
367         if (st.st_uid != 0 || st.st_gid != gid)
368                 errx(1, "timestamp file has wrong owner or group");
369
370         /* The timestamp size is 0 if its a new file or a
371          * timestamp that was never set, its not valid but
372          * can be used to write the new timestamp.
373          * If the size does not match the expected size it
374          * is incomplete and should never be used
375          */
376         if (st.st_size == sizeof (struct timespec) * 2) {
377                 *valid = timestamp_valid(fd, secs);
378         } else if (st.st_size == 0) {
379                 *valid = 0;
380         } else {
381                 errx(1, "corrupt timestamp file");
382         }
383
384         close(dirfd);
385         return fd;
386 }
387
388 int
389 timestamp_clear()
390 {
391         struct dirent *ent;
392         DIR *dp;
393         const char *prefix;
394         int dirfd;
395         int ret = 0;
396         size_t plen;
397
398         if ((prefix = session_prefix()) == NULL)
399                 errx(1, "failed to get timestamp session prefix");
400         plen = strlen(prefix);
401
402         if ((dirfd = opentsdir()) == -1)
403                 errx(1, "opentsdir");
404         if ((dp = fdopendir(dirfd)) == NULL)
405                 err(1, "fopendir");
406
407         /*
408          * Delete all files in the timestamp directory
409          * with the same session prefix.
410          */
411         while ((ent = readdir(dp)) != NULL) {
412                 if (ent->d_type != DT_REG)
413                         continue;
414                 if (strncmp(prefix, ent->d_name, plen) != 0)
415                         continue;
416                 if (unlinkat(dirfd, ent->d_name, 0) == -1)
417                         ret = -1;
418         }
419         closedir(dp);
420         close(dirfd);
421
422         return ret;
423 }