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