]> git.armaanb.net Git - opendoas.git/blob - timestamp.c
Handle empty argv
[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 "openbsd.h"
86 #include "doas.h"
87
88 #ifndef TIMESTAMP_DIR
89 #       define TIMESTAMP_DIR "/run/doas"
90 #endif
91
92 #if defined(TIMESTAMP_TMPFS) && defined(__linux__)
93 #       ifndef TMPFS_MAGIC
94 #               define TMPFS_MAGIC 0x01021994
95 #       endif
96 #endif
97
98 #ifdef __linux__
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
104  */
105 static int
106 proc_info(pid_t pid, int *ttynr, unsigned long long *starttime)
107 {
108         char path[128];
109         char buf[1024];
110         char *p, *saveptr, *ep;
111         const char *errstr;
112         int fd, n;
113
114         p = buf;
115
116         n = snprintf(path, sizeof path, "/proc/%d/stat", pid);
117         if (n < 0 || n >= (int)sizeof path)
118                 return -1;
119
120         if ((fd = open(path, O_RDONLY|O_NOFOLLOW)) == -1) {
121                 warn("failed to open: %s", path);
122                 return -1;
123         }
124
125         while ((n = read(fd, p, buf + (sizeof buf - 1) - p)) != 0) {
126                 if (n == -1) {
127                         if (errno == EAGAIN || errno == EINTR)
128                                 continue;
129                         warn("read: %s", path);
130                         close(fd);
131                         return -1;
132                 }
133                 p += n;
134                 if (p >= buf + (sizeof buf - 1))
135                         break;
136         }
137         close(fd);
138
139         /* error if it contains NULL bytes */
140         if (n != 0 || memchr(buf, '\0', p - buf - 1) != NULL) {
141                 warn("NUL in: %s", path);
142                 return -1;
143         }
144
145         *p = '\0';
146
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
151          */
152         if ((p = strrchr(buf, ')')) == NULL)
153                 return -1;
154
155         n = 2;
156         for ((p = strtok_r(p, " ", &saveptr)); p;
157             (p = strtok_r(NULL, " ", &saveptr))) {
158                 switch (n++) {
159                 case 7:
160                         *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
161                         if (errstr)
162                                 return -1;
163                         break;
164                 case 22:
165                         errno = 0;
166                         *starttime = strtoull(p, &ep, 10);
167                         if (p == ep ||
168                            (errno == ERANGE && *starttime == ULLONG_MAX))
169                                 return -1;
170                         return 0;
171                 }
172         }
173
174         return -1;
175 }
176 #else
177 #error "proc_info not implemented"
178 #endif
179
180 static int
181 timestamp_path(char *buf, size_t len)
182 {
183         pid_t ppid, sid;
184         unsigned long long starttime;
185         int n, ttynr;
186
187         ppid = getppid();
188         if ((sid = getsid(0)) == -1)
189                 return -1;
190         if (proc_info(ppid, &ttynr, &starttime) == -1)
191                 return -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)
195                 return -1;
196         return 0;
197 }
198
199 int
200 timestamp_set(int fd, int secs)
201 {
202         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
203
204         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
205             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
206                 return -1;
207
208         timespecadd(&ts[0], &timeout, &ts[0]);
209         timespecadd(&ts[1], &timeout, &ts[1]);
210         return futimens(fd, ts);
211 }
212
213 /*
214  * Returns 1 if the timestamp is valid, 0 if its invalid
215  */
216 static int
217 timestamp_check(int fd, int secs)
218 {
219         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
220         struct stat st;
221
222         if (fstat(fd, &st) == -1)
223                 err(1, "fstat");
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");
226
227         /* this timestamp was created but never set, invalid but no error */
228         if (!timespecisset(&st.st_atim) || !timespecisset(&st.st_mtim))
229                 return 0;
230
231         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
232             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1) {
233                 warn("clock_gettime");
234                 return 0;
235         }
236
237         /* check if timestamp is too old */
238         if (timespeccmp(&st.st_atim, &ts[0], <) ||
239             timespeccmp(&st.st_mtim, &ts[1], <))
240                 return 0;
241
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");
248                 return 0;
249         }
250
251         return 1;
252 }
253
254 int
255 timestamp_open(int *valid, int secs)
256 {
257         struct timespec ts[2] = {0};
258         struct stat st;
259         int fd;
260         char path[256];
261         int serrno = 0;
262
263         *valid = 0;
264
265         if (stat(TIMESTAMP_DIR, &st) == -1) {
266                 if (errno != ENOENT)
267                         return -1;
268                 if (mkdir(TIMESTAMP_DIR, 0700) == -1)
269                         return -1;
270         } else if (st.st_uid != 0 || st.st_mode != (S_IFDIR | 0700)) {
271                 return -1;
272         }
273
274         if (timestamp_path(path, sizeof path) == -1)
275                 return -1;
276
277         fd = open(path, O_RDONLY|O_NOFOLLOW);
278         if (fd == -1) {
279                 char tmp[256];
280                 int n;
281
282                 if (errno != ENOENT)
283                         err(1, "open: %s", path);
284
285                 n = snprintf(tmp, sizeof tmp, TIMESTAMP_DIR "/.tmp-%d", getpid());
286                 if (n < 0 || n >= (int)sizeof tmp)
287                         return -1;
288
289                 fd = open(tmp, O_RDONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0000);
290                 if (fd == -1)
291                         return -1;
292                 if (futimens(fd, ts) == -1 || rename(tmp, path) == -1) {
293                         serrno = errno;
294                         close(fd);
295                         unlink(tmp);
296                         errno = serrno;
297                         return -1;
298                 }
299         } else {
300                 *valid = timestamp_check(fd, secs);
301         }
302         return fd;
303 }
304
305 int
306 timestamp_clear()
307 {
308         char path[256];
309
310         if (timestamp_path(path, sizeof path) == -1)
311                 return -1;
312         if (unlink(path) == -1 && errno != ENOENT)
313                 return -1;
314         return 0;
315 }