/src/sudo/plugins/sudoers/rationalize.c
Line | Count | Source |
1 | | /* |
2 | | * SPDX-License-Identifier: ISC |
3 | | * |
4 | | * Copyright (c) 2026 Todd C. Miller <Todd.Miller@sudo.ws> |
5 | | * |
6 | | * Permission to use, copy, modify, and distribute this software for any |
7 | | * purpose with or without fee is hereby granted, provided that the above |
8 | | * copyright notice and this permission notice appear in all copies. |
9 | | * |
10 | | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
11 | | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
12 | | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
13 | | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
14 | | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
15 | | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
16 | | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
17 | | */ |
18 | | |
19 | | #include <config.h> |
20 | | |
21 | | #include <assert.h> |
22 | | #include <string.h> |
23 | | |
24 | | #include <sudoers.h> |
25 | | |
26 | | /* |
27 | | * Rationalize a path by removing consecutive '/' and "/./" path elements. |
28 | | * Also removes "/../" elements if dotdot is true. |
29 | | */ |
30 | | char * |
31 | | rationalize_path(char *path, bool dotdot) |
32 | 3.72M | { |
33 | 3.72M | char *cp, *ep, *path_end; |
34 | 3.72M | debug_decl(rationalize_path, SUDOERS_DEBUG_UTIL); |
35 | | |
36 | 3.72M | path_end = path + strlen(path); |
37 | 1.48G | for (cp = path; *cp != '\0';) { |
38 | 1.48G | if (cp[0] == '/') { |
39 | | /* Collapse consecutive '/' characters. */ |
40 | 4.60M | if (cp[1] == '/') { |
41 | 509k | for (ep = cp + 1; ep[1] == '/'; ep++) |
42 | 192k | continue; |
43 | | |
44 | | /* The size argument includes the terminating NUL byte. */ |
45 | 317k | assert(ep <= path_end); |
46 | 317k | memmove(cp, ep, (size_t)(path_end - ep) + 1); |
47 | 317k | path_end -= ep - cp; |
48 | 317k | assert(*path_end == '\0'); |
49 | 317k | continue; |
50 | 317k | } |
51 | | |
52 | | /* Remove "/./" in path or "/." at path_end. */ |
53 | 4.28M | if (cp[1] == '.' && (cp[2] == '/' || cp[2] == '\0')) { |
54 | | /* /./foo -> /foo OR /foo/. -> /foo */ |
55 | 31.6k | ep = cp + 2; |
56 | | |
57 | | /* The size argument includes the terminating NUL byte. */ |
58 | 31.6k | assert(ep <= path_end); |
59 | 31.6k | memmove(cp, ep, (size_t)(path_end - ep) + 1); |
60 | 31.6k | path_end -= ep - cp; |
61 | | |
62 | | /* An absolute path must not collapse to the empty string. */ |
63 | 31.6k | if (cp == path && *cp == '\0') { |
64 | | /* /. -> / */ |
65 | 4.15k | path[0] = '/'; |
66 | 4.15k | path[1] = '\0'; |
67 | 4.15k | path_end = path + 1; |
68 | 4.15k | } |
69 | 31.6k | assert(*path_end == '\0'); |
70 | 31.6k | continue; |
71 | 31.6k | } |
72 | | |
73 | | /* Remove "/../" in path or "/.." at path_end. */ |
74 | 4.25M | if (dotdot && cp[1] == '.' && cp[2] == '.' && |
75 | 204k | (cp[3] == '/' || cp[3] == '\0')) { |
76 | | |
77 | | /* Skip ep past "/../" or "/.." */ |
78 | 171k | ep = cp + 3 + (cp[3] == '/'); |
79 | | |
80 | | /* Set cp to the previous '/' if there is one. */ |
81 | 171k | if (cp != path) { |
82 | 47.4k | char *saved_cp = cp; |
83 | 47.4k | ep = cp + 3 + (cp[3] == '/'); |
84 | 335k | do { |
85 | 335k | cp--; |
86 | 335k | } while (cp > path && *cp != '/'); |
87 | 47.4k | if (*cp != '/') { |
88 | | /* No previous '/', path doesn't start with '/'? */ |
89 | 22.8k | cp = saved_cp; |
90 | 22.8k | } |
91 | 47.4k | } |
92 | | |
93 | | /* The size argument includes the terminating NUL byte. */ |
94 | 171k | assert(ep <= path_end); |
95 | 171k | memmove(cp + 1, ep, (size_t)(path_end - ep) + 1); |
96 | 171k | path_end -= ep - (cp + 1); |
97 | 171k | assert(*path_end == '\0'); |
98 | 171k | continue; |
99 | 171k | } |
100 | 4.25M | } |
101 | 1.47G | cp++; |
102 | 1.47G | } |
103 | 3.72M | debug_return_str(path); |
104 | 3.72M | } |