/src/sudo/plugins/sudoers/canon_path.c
Line | Count | Source |
1 | | /* |
2 | | * SPDX-License-Identifier: ISC |
3 | | * |
4 | | * Copyright (c) 2023 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 <sys/stat.h> |
22 | | #include <stddef.h> |
23 | | #include <stdio.h> |
24 | | #include <stdlib.h> |
25 | | #include <string.h> |
26 | | #include <unistd.h> |
27 | | #include <errno.h> |
28 | | |
29 | | #include <sudoers.h> |
30 | | #include <redblack.h> |
31 | | |
32 | | static struct rbtree *canon_cache; |
33 | | |
34 | | /* |
35 | | * A cache_item includes storage for both the original path and the |
36 | | * resolved path. The resolved path is directly embedded into the |
37 | | * struct so that we can find the start of the struct cache_item |
38 | | * given the value of resolved. Storage for pathname is embedded |
39 | | * at the end with resolved. |
40 | | */ |
41 | | struct cache_item { |
42 | | unsigned int refcnt; |
43 | | char *pathname; |
44 | | char resolved[]; |
45 | | }; |
46 | | |
47 | | /* |
48 | | * Compare function for canon_cache. |
49 | | * v1 is the key to find or data to insert, v2 is in-tree data. |
50 | | */ |
51 | | static int |
52 | | compare(const void *v1, const void *v2) |
53 | 9.44k | { |
54 | 9.44k | const struct cache_item *ci1 = (const struct cache_item *)v1; |
55 | 9.44k | const struct cache_item *ci2 = (const struct cache_item *)v2; |
56 | 9.44k | return strcmp(ci1->pathname, ci2->pathname); |
57 | 9.44k | } |
58 | | |
59 | | /* Convert a pointer returned by canon_path() to a struct cache_item *. */ |
60 | 13.7k | #define resolved_to_item(_r) ((struct cache_item *)((_r) - offsetof(struct cache_item, resolved))) |
61 | | |
62 | | /* |
63 | | * Delete a ref from item and free if the refcount reaches 0. |
64 | | */ |
65 | | static void |
66 | | canon_path_free_item(void *v) |
67 | 19.5k | { |
68 | 19.5k | struct cache_item *item = v; |
69 | 19.5k | debug_decl(canon_path_free_item, SUDOERS_DEBUG_UTIL); |
70 | | |
71 | 19.5k | if (--item->refcnt == 0) |
72 | 5.80k | free(item); |
73 | | |
74 | 19.5k | debug_return; |
75 | 19.5k | } |
76 | | |
77 | | /* |
78 | | * Delete a ref from the item containing "resolved" and free if |
79 | | * the refcount reaches 0. |
80 | | */ |
81 | | void |
82 | | canon_path_free(char *resolved) |
83 | 48.1k | { |
84 | 48.1k | debug_decl(canon_path_free, SUDOERS_DEBUG_UTIL); |
85 | 48.1k | if (resolved != NULL) |
86 | 13.7k | canon_path_free_item(resolved_to_item(resolved)); |
87 | 48.1k | debug_return; |
88 | 48.1k | } |
89 | | |
90 | | /* |
91 | | * Free canon_cache. |
92 | | * This only removes the reference for that the cache owns. |
93 | | * Other references remain valid until canon_path_free() is called. |
94 | | */ |
95 | | void |
96 | | canon_path_free_cache(void) |
97 | 25.6k | { |
98 | 25.6k | debug_decl(canon_path_free_cache, SUDOERS_DEBUG_UTIL); |
99 | | |
100 | 25.6k | if (canon_cache != NULL) { |
101 | 5.68k | rbdestroy(canon_cache, canon_path_free_item); |
102 | 5.68k | canon_cache = NULL; |
103 | 5.68k | } |
104 | | |
105 | 25.6k | debug_return; |
106 | 25.6k | } |
107 | | |
108 | | /* |
109 | | * Like realpath(3) but caches the result. Returns an entry from the |
110 | | * cache on success (with an added reference) or NULL on failure. |
111 | | */ |
112 | | char * |
113 | | canon_path(const char *inpath) |
114 | 14.8k | { |
115 | 14.8k | size_t item_size, inlen, reslen = 0; |
116 | 14.8k | char *resolved, resbuf[PATH_MAX]; |
117 | 14.8k | struct cache_item key, *item; |
118 | 14.8k | struct rbnode *node = NULL; |
119 | 14.8k | debug_decl(canon_path, SUDOERS_DEBUG_UTIL); |
120 | | |
121 | 14.8k | if (canon_cache == NULL) { |
122 | 5.68k | canon_cache = rbcreate(compare); |
123 | 5.68k | if (canon_cache == NULL) { |
124 | 0 | sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
125 | 0 | debug_return_str(NULL); |
126 | 0 | } |
127 | 9.20k | } else { |
128 | | /* Check cache. */ |
129 | 9.20k | key.pathname = (char *)inpath; |
130 | 9.20k | if ((node = rbfind(canon_cache, &key)) != NULL) { |
131 | 9.09k | item = node->data; |
132 | 9.09k | goto done; |
133 | 9.09k | } |
134 | 9.20k | } |
135 | | |
136 | | /* |
137 | | * Not cached, call realpath(3). |
138 | | * Older realpath() doesn't support passing a NULL buffer. |
139 | | * We special-case the empty string to resolve to "/". |
140 | | * XXX - warn on errors other than ENOENT? |
141 | | */ |
142 | 5.80k | if (*inpath == '\0') |
143 | 0 | resolved = (char *)"/"; |
144 | 5.80k | else |
145 | 5.80k | resolved = realpath(inpath, resbuf); |
146 | | |
147 | 5.80k | inlen = strlen(inpath); |
148 | | /* one for NULL terminator of resolved, one for NULL terminator of pathname */ |
149 | 5.80k | item_size = sizeof(*item) + inlen + 2; |
150 | 5.80k | if (resolved != NULL) { |
151 | 5.29k | reslen = strlen(resolved); |
152 | 5.29k | item_size += reslen; |
153 | 5.29k | } |
154 | 5.80k | item = malloc(item_size); |
155 | 5.80k | if (item == NULL) { |
156 | 0 | sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
157 | 0 | debug_return_str(NULL); |
158 | 0 | } |
159 | 5.80k | if (resolved != NULL) |
160 | 5.29k | memcpy(item->resolved, resolved, reslen); |
161 | 5.80k | item->resolved[reslen] = '\0'; |
162 | 5.80k | item->pathname = item->resolved + reslen + 1; |
163 | 5.80k | memcpy(item->pathname, inpath, inlen); |
164 | 5.80k | item->pathname[inlen] = '\0'; |
165 | 5.80k | item->refcnt = 1; |
166 | 5.80k | switch (rbinsert(canon_cache, item, NULL)) { |
167 | 0 | case 1: |
168 | | /* should not happen */ |
169 | 0 | sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, |
170 | 0 | "path \"%s\" already exists in the cache", inpath); |
171 | 0 | item->refcnt = 0; |
172 | 0 | break; |
173 | 0 | case -1: |
174 | | /* can't cache item, just return it */ |
175 | 0 | sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, |
176 | 0 | "can't cache path \"%s\"", inpath); |
177 | 0 | item->refcnt = 0; |
178 | 0 | break; |
179 | 5.80k | } |
180 | 14.8k | done: |
181 | 14.8k | if (item->refcnt != 0) { |
182 | 14.8k | sudo_debug_printf(SUDO_DEBUG_DEBUG, |
183 | 14.8k | "%s: path %s -> %s (%s)", __func__, inpath, |
184 | 14.8k | item->resolved[0] ? item->resolved : "NULL", |
185 | 14.8k | node ? "cache hit" : "cached"); |
186 | 14.8k | } |
187 | 14.8k | if (item->resolved[0] == '\0') { |
188 | | /* negative result, free item if not cached */ |
189 | 1.15k | if (item->refcnt == 0) |
190 | 0 | free(item); |
191 | 1.15k | debug_return_str(NULL); |
192 | 1.15k | } |
193 | 13.7k | item->refcnt++; |
194 | 13.7k | debug_return_str(item->resolved); |
195 | 13.7k | } |