]> git.armaanb.net Git - opendoas.git/blob - timestamp.c
c90318dd71daf7ca6cc109ff2cad725949164ef4
[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         n = snprintf(path, sizeof path, "/proc/%d/stat", pid);
98         if (n < 0 || n >= (int)sizeof path)
99                 return -1;
100
101         if ((fd = open(path, O_RDONLY)) == -1)
102                 return -1;
103
104         while ((n = read(fd, p, buf + sizeof buf - p)) != 0) {
105                 if (n == -1) {
106                         if (errno == EAGAIN || errno == EINTR)
107                                 continue;
108                         close(fd);
109                         return -1;
110                 }
111                 p += n;
112                 if (p >= buf + sizeof buf)
113                         break;
114         }
115         close(fd);
116
117         /* error if it contains NULL bytes */
118         if (n != 0 || memchr(buf, '\0', p - buf))
119                 return -1;
120
121         /* Get the 7th field, 5 fields after the last ')',
122          * (2th field) because the 5th field 'comm' can include
123          * spaces and closing paranthesis too.
124          * See https://www.sudo.ws/alerts/linux_tty.html
125          */
126         if ((p = strrchr(buf, ')')) == NULL)
127                 return -1;
128
129         n = 2;
130         for ((p = strtok_r(p, " ", &saveptr)); p;
131             (p = strtok_r(NULL, " ", &saveptr))) {
132                 switch (n++) {
133                 case 7:
134                         *ttynr = strtonum(p, INT_MIN, INT_MAX, &errstr);
135                         if (errstr)
136                                 return -1;
137                         break;
138                 case 22:
139                         errno = 0;
140                         *starttime = strtoull(p, &ep, 10);
141                         if (p == ep ||
142                            (errno == ERANGE && *starttime == ULLONG_MAX))
143                                 return -1;
144                         return 0;
145                 }
146         }
147
148         return -1;
149 }
150 #else
151 #error "proc_info not implemented"
152 #endif
153
154 static int
155 timestamp_path(char *buf, size_t len)
156 {
157         pid_t ppid, sid;
158         unsigned long long starttime;
159         int n, ttynr;
160
161         ppid = getppid();
162         if ((sid = getsid(0)) == -1)
163                 return -1;
164         if (proc_info(ppid, &ttynr, &starttime) == -1)
165                 return -1;
166         n = snprintf(buf, len, TIMESTAMP_DIR "/%d-%d-%d-%llu-%d",
167             ppid, sid, ttynr, starttime, getuid());
168         if (n < 0 || n >= (int)len)
169                 return -1;
170         return 0;
171 }
172
173 int
174 timestamp_set(int fd, int secs)
175 {
176         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
177
178         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
179             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
180                 return -1;
181
182         timespecadd(&ts[0], &timeout, &ts[0]);
183         timespecadd(&ts[1], &timeout, &ts[1]);
184         return futimens(fd, ts);
185 }
186
187 /*
188  * Returns 1 if the timestamp is valid, 0 if its invalid
189  */
190 static int
191 timestamp_check(int fd, int secs)
192 {
193         struct timespec ts[2], timeout = { .tv_sec = secs, .tv_nsec = 0 };
194         struct stat st;
195
196         if (fstat(fd, &st) == -1)
197                 return 0;
198
199         if (!timespecisset(&st.st_atim) || !timespecisset(&st.st_mtim))
200                 return 0;
201
202         if (clock_gettime(CLOCK_BOOTTIME, &ts[0]) == -1 ||
203             clock_gettime(CLOCK_REALTIME, &ts[1]) == -1)
204                 return 0;
205
206         /* check if timestamp is too old */
207         if (timespeccmp(&st.st_atim, &ts[0], <) ||
208             timespeccmp(&st.st_mtim, &ts[1], <))
209                 return 0;
210
211         /* check if timestamp is too far in the future */
212         timespecadd(&ts[0], &timeout, &ts[0]);
213         timespecadd(&ts[1], &timeout, &ts[1]);
214         if (timespeccmp(&st.st_atim, &ts[0], >) ||
215             timespeccmp(&st.st_mtim, &ts[1], >))
216                 return 0;
217
218         return 1;
219 }
220
221 int
222 timestamp_open(int *valid, int secs)
223 {
224         struct timespec ts[2] = {0};
225         struct stat st;
226         int fd;
227         char path[256];
228         int serrno = 0;
229
230         *valid = 0;
231
232         if (stat(TIMESTAMP_DIR, &st) == -1) {
233                 if (errno != ENOENT)
234                         return -1;
235                 if (mkdir(TIMESTAMP_DIR, 0700) == -1)
236                         return -1;
237         } else if (st.st_uid != 0 || st.st_mode != (S_IFDIR | 0700)) {
238                 return -1;
239         }
240
241         if (timestamp_path(path, sizeof path) == -1)
242                 return -1;
243
244         if (stat(path, &st) != -1 && (st.st_uid != 0 || st.st_gid != getgid()|| st.st_mode != (S_IFREG | 0000)))
245                 return -1;
246
247         fd = open(path, O_RDONLY|O_NOFOLLOW);
248         if (fd == -1) {
249                 char tmp[256];
250                 int n;
251
252                 n = snprintf(tmp, sizeof tmp, TIMESTAMP_DIR "/.tmp-%d", getpid());
253                 if (n < 0 || n >= (int)sizeof tmp)
254                         return -1;
255
256                 fd = open(tmp, O_RDONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0000);
257                 if (fd == -1)
258                         return -1;
259                 if (futimens(fd, ts) == -1 || rename(tmp, path) == -1) {
260                         serrno = errno;
261                         close(fd);
262                         unlink(tmp);
263                         errno = serrno;
264                         return -1;
265                 }
266         } else {
267                 *valid = timestamp_check(fd, secs);
268         }
269         return fd;
270 }
271
272 int
273 timestamp_clear()
274 {
275         char path[256];
276
277         if (timestamp_path(path, sizeof path) == -1)
278                 return -1;
279         if (unlink(path) == -1 && errno != ENOENT)
280                 return -1;
281         return 0;
282 }