2 * Copyright (c) 2020 Duncan Overbruck <mail@duncano.de>
3 * Copyright (c) 2021-2022 Sergey Sushilin <sergeysushilin@protonmail.com>
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 * 1) Timestamp files and directories
21 * Timestamp files MUST NOT be accessible to users other than root,
22 * this includes the name, metadata and the content of timestamp files
25 * Symlinks can be used to create, manipulate or delete wrong files
26 * and directories. The Implementation MUST reject any symlinks for
27 * timestamp files or directories.
29 * To avoid race conditions the implementation MUST use the same
30 * file descriptor for permission checks and do read or write
31 * write operations after the permission checks.
33 * The timestamp files MUST be opened with openat(2) using the
34 * timestamp directory file descriptor. Permissions of the directory
35 * MUST be checked before opening the timestamp file descriptor.
37 * 2) Clock sources for timestamps
39 * Timestamp files MUST NOT rely on only one clock source, using the
40 * wall clock would allow to reset the clock to an earlier point in
41 * time to reuse a timestamp.
43 * The timestamp MUST consist of multiple clocks and MUST reject the
44 * timestamp if there is a change to any clock because there is no way
45 * to differentiate between malicious and legitimate clock changes.
47 * 3) Timestamp lifetime
49 * The implementation MUST NOT use the user controlled stdin, stdout
50 * and stderr file descriptors to determine the controlling terminal.
51 * On linux the /proc/$pid/stat file MUST be used to get the terminal
54 * There is no reliable way to determine the lifetime of a tty/pty.
55 * The start time of the session leader MUST be used as part of the
56 * timestamp to determine if the tty is still the same.
57 * If the start time of the session leader changed the timestamp MUST
61 #include <sys/ioctl.h>
65 #if !defined(timespecisset) || !defined(timespeccmp) || !defined(timespecadd)
66 # include "sys/time.h"
83 #include "timestamp.h"
85 #if !defined(TIMESTAMP_DIR)
86 # define TIMESTAMP_DIR "/run/doas"
89 #if defined(__linux__)
90 # define PROC_STATUS_PATH_FORMAT "/proc/%lu/stat"
91 # define TTY_NUMBER_FIELD_NUMBER 7
92 # define START_TIME_FIELD_NUMBER 22
93 #elif defined(__FreeBSD__)
94 # define PROC_STATUS_PATH_FORMAT "/proc/%lu/status"
95 # define TTY_NUMBER_FIELD_NUMBER 6
96 # define START_TIME_FIELD_NUMBER 8
99 #define TTY_NUMBER_SIZE 32
101 #if defined(__linux__) || defined(__FreeBSD__)
102 /* Use tty_nr from /proc/self/stat instead of using
103 * ttyname(3), stdin, stdout and stderr are user
104 * controllable and would allow to reuse timestamps
105 * from another writable terminal.
106 * See https://www.sudo.ws/alerts/tty_tickets.html
108 static int proc_info(pid_t pid
, char ttynr
[TTY_NUMBER_SIZE
], unsigned long long int *starttime
)
110 char path
[128], buf
[1024], *p
, *saveptr
;
122 if (safe_snprintf(path
, sizeof(path
), PROC_STATUS_PATH_FORMAT
, (u_long
)pid
) < 0)
125 if ((fd
= open(path
, O_RDONLY
| O_NOFOLLOW
)) < 0) {
126 warn("failed to open: %s", path
);
132 while ((n
= full_read(fd
, p
, endof(buf
) - p
- 1)) != 0) {
135 if (p
>= endof(buf
) - 1)
140 warn("read: %s", path
);
147 /* Error if it contains NULL bytes. */
148 if (memchr(buf
, '\0', p
- buf
- 1) != NULL
) {
149 warn("NUL in: %s", path
);
155 #if defined(__linux__)
157 * Get the 7th field, 5 fields after the last ')',
158 * (2th field) because the 5th field 'comm' can include
159 * spaces and closing paranthesis too.
160 * See https://www.sudo.ws/alerts/linux_tty.html
162 /* Be careful: program name may include ')' character,
163 so we find the last ')' entry searching from the end. */
164 if ((p
= strrchr(buf
, ')')) == NULL
)
168 #elif defined(__FreeBSD__)
172 for (p
= strtok_r(p
, " ", &saveptr
); p
!= NULL
; p
= strtok_r(NULL
, " ", &saveptr
)) {
174 case TTY_NUMBER_FIELD_NUMBER
: {
175 size_t ttylen
= saveptr
- p
- 1;
177 if (ttylen
>= TTY_NUMBER_SIZE
) {
182 memcpy(ttynr
, p
, ttylen
);
185 case START_TIME_FIELD_NUMBER
:
186 *starttime
= safe_strtounum(p
, ULLONG_MAX
, &succeed
);
187 return succeed
? 0 : -1;
194 /* proc_info not implemented. */
195 static int proc_info(pid_t pid __unused
, int *ttynr __unused
, unsigned long long int *starttime __unused
)
202 static char *timestamp_name(char *buf
, size_t len
)
205 unsigned long long starttime
;
206 char ttynr
[TTY_NUMBER_SIZE
] = { 0 };
210 if (proc_info(ppid
, ttynr
, &starttime
) < 0)
213 if ((sid
= getsid(0)) < 0)
216 if (safe_snprintf(buf
, len
, "%d-%d-%s-%llu-%ld",
217 ppid
, sid
, ttynr
, starttime
, (long)getuid()) < 0)
223 int timestamp_set(int fd
, time_t secs
)
225 struct timespec ts
[2], timeout
= { .tv_sec
= secs
, .tv_nsec
= 0 };
227 if (clock_gettime(CLOCK_BOOTTIME
, &ts
[0]) < 0
228 || clock_gettime(CLOCK_REALTIME
, &ts
[1]) < 0)
231 timespecadd(&ts
[0], &timeout
, &ts
[0]);
232 timespecadd(&ts
[1], &timeout
, &ts
[1]);
234 return futimens(fd
, ts
);
237 /* Returns true if the timestamp is valid, false if it is invalid. */
238 static bool timestamp_check(int fd
, time_t secs
)
240 struct timespec ts
[2], timeout
= { .tv_sec
= secs
, .tv_nsec
= 0 };
248 if (fstat(fd
, &st
) != 0) {
249 warn("can not get file status");
253 if (st
.st_uid
!= ROOT_UID
|| st
.st_gid
!= getgid() || st
.st_mode
!= (S_IFREG
| 0000)) {
254 warnx("timestamp uid, gid or mode wrong");
258 /* This timestamp was created, but never set.
259 Invalid, but no error. */
260 if (!timespecisset(&st
.st_atim
)
261 || !timespecisset(&st
.st_mtim
))
264 if (clock_gettime(CLOCK_BOOTTIME
, &ts
[0]) < 0
265 || clock_gettime(CLOCK_REALTIME
, &ts
[1]) < 0) {
266 warn("clock_gettime");
270 /* Check if timestamp is too old. */
271 if (timespeccmp(&st
.st_atim
, &ts
[0], <)
272 || timespeccmp(&st
.st_mtim
, &ts
[1], <))
275 /* Check if timestamp is too far in the future. */
276 timespecadd(&ts
[0], &timeout
, &ts
[0]);
277 timespecadd(&ts
[1], &timeout
, &ts
[1]);
279 if (timespeccmp(&st
.st_atim
, &ts
[0], >)
280 || timespeccmp(&st
.st_mtim
, &ts
[1], >)) {
281 warnx("timestamp too far in the future");
288 int timestamp_open(bool *valid
, time_t secs
)
290 struct timespec ts
[2] = { 0 };
302 dirfd
= open(TIMESTAMP_DIR
, O_DIRECTORY
| O_NOFOLLOW
);
308 if (mkdir(TIMESTAMP_DIR
, 0700) < 0)
311 dirfd
= open(TIMESTAMP_DIR
, O_DIRECTORY
| O_NOFOLLOW
);
317 if (fstat(dirfd
, &st
) < 0) {
319 } else if (st
.st_uid
!= ROOT_UID
|| st
.st_gid
!= ROOT_UID
|| st
.st_mode
!= (S_IFDIR
| 0700)) {
320 if (!S_ISDIR(st
.st_mode
)) {
325 /* If directory exists make sure that it has required mode and owner. */
326 if (fchmod(dirfd
, 0700) < 0)
329 if (fchown(dirfd
, ROOT_UID
, ROOT_UID
) < 0)
333 if (timestamp_name(name
, sizeof(name
)) == NULL
)
336 fd
= openat(dirfd
, name
, O_RDONLY
| O_NOFOLLOW
);
341 if (errno
!= ENOENT
) {
342 warn("can not open %s", name
);
346 if (safe_snprintf(tmp
, sizeof(tmp
), ".tmp-%ld", (long int)getpid()) < 0)
349 fd
= openat(dirfd
, tmp
, O_RDONLY
| O_CREAT
| O_EXCL
| O_NOFOLLOW
, 0000);
352 warn("can not open "TIMESTAMP_DIR
"/%s", tmp
);
356 if (futimens(fd
, ts
) < 0 || renameat(dirfd
, tmp
, dirfd
, name
) < 0) {
357 int saved_errno
= errno
;
360 unlinkat(dirfd
, tmp
, 0);
365 *valid
= timestamp_check(fd
, secs
);
371 int timestamp_clear(void)
374 int dirfd
= open(TIMESTAMP_DIR
, O_DIRECTORY
| O_NOFOLLOW
);
379 if (timestamp_name(name
, sizeof(name
)) == NULL
)
382 if (unlinkat(dirfd
, name
, 0) < 0 && errno
!= ENOENT
)