Coverage Report

Created: 2025-08-03 06:56

/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 with resolved.
45
 */
46
struct cache_item {
47
    unsigned int refcnt;
48
    char *pathname;
49
    char resolved[];
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
5.15k
{
59
5.15k
    const struct cache_item *ci1 = (const struct cache_item *)v1;
60
5.15k
    const struct cache_item *ci2 = (const struct cache_item *)v2;
61
5.15k
    return strcmp(ci1->pathname, ci2->pathname);
62
5.15k
}
63
64
/* Convert a pointer returned by canon_path() to a struct cache_item *. */
65
7.13k
#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
10.3k
{
73
10.3k
    struct cache_item *item = v;
74
10.3k
    debug_decl(canon_path_free_item, SUDOERS_DEBUG_UTIL);
75
76
10.3k
    if (--item->refcnt == 0)
77
3.26k
  free(item);
78
79
10.3k
    debug_return;
80
10.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
27.0k
{
89
27.0k
    debug_decl(canon_path_free, SUDOERS_DEBUG_UTIL);
90
27.0k
    if (resolved != NULL)
91
7.13k
  canon_path_free_item(resolved_to_item(resolved));
92
27.0k
    debug_return;
93
27.0k
}
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
14.7k
{
103
14.7k
    debug_decl(canon_path_free_cache, SUDOERS_DEBUG_UTIL);
104
105
14.7k
    if (canon_cache != NULL) {
106
3.15k
  rbdestroy(canon_cache, canon_path_free_item);
107
3.15k
  canon_cache = NULL;
108
3.15k
    }
109
110
14.7k
    debug_return;
111
14.7k
}
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
8.08k
{
120
8.08k
    size_t item_size, inlen, reslen = 0;
121
8.08k
    char *resolved, resbuf[PATH_MAX];
122
8.08k
    struct cache_item key, *item;
123
8.08k
    struct rbnode *node = NULL;
124
8.08k
    debug_decl(canon_path, SUDOERS_DEBUG_UTIL);
125
126
8.08k
    if (canon_cache == NULL) {
127
3.15k
  canon_cache = rbcreate(compare);
128
3.15k
  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
4.93k
    } else {
133
  /* Check cache. */
134
4.93k
  key.pathname = (char *)inpath;
135
4.93k
  if ((node = rbfind(canon_cache, &key)) != NULL) {
136
4.82k
      item = node->data;
137
4.82k
      goto done;
138
4.82k
  }
139
4.93k
    }
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
3.26k
    if (*inpath == '\0')
148
0
  resolved = (char *)"/";
149
3.26k
    else
150
3.26k
  resolved = realpath(inpath, resbuf);
151
152
3.26k
    inlen = strlen(inpath);
153
    /* one for NULL terminator of resolved, one for NULL terminator of pathname */
154
3.26k
    item_size = sizeof(*item) + inlen + 2;
155
3.26k
    if (resolved != NULL) {
156
2.84k
  reslen = strlen(resolved);
157
2.84k
  item_size += reslen;
158
2.84k
    }
159
3.26k
    item = malloc(item_size);
160
3.26k
    if (item == NULL) {
161
0
  sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
162
0
  debug_return_str(NULL);
163
0
    }
164
3.26k
    if (resolved != NULL)
165
2.84k
  memcpy(item->resolved, resolved, reslen);
166
3.26k
    item->resolved[reslen] = '\0';
167
3.26k
    item->pathname = item->resolved + reslen + 1;
168
3.26k
    memcpy(item->pathname, inpath, inlen);
169
3.26k
    item->pathname[inlen] = '\0';
170
3.26k
    item->refcnt = 1;
171
3.26k
    switch (rbinsert(canon_cache, item, NULL)) {
172
0
    case 1:
173
  /* should not happen */
174
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
175
0
      "path \"%s\" already exists in the cache", inpath);
176
0
  item->refcnt = 0;
177
0
  break;
178
0
    case -1:
179
  /* can't cache item, just return it */
180
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
181
0
      "can't cache path \"%s\"", inpath);
182
0
  item->refcnt = 0;
183
0
  break;
184
3.26k
    }
185
8.08k
done:
186
8.08k
    if (item->refcnt != 0) {
187
8.08k
        sudo_debug_printf(SUDO_DEBUG_DEBUG,
188
8.08k
            "%s: path %s -> %s (%s)", __func__, inpath,
189
8.08k
      item->resolved[0] ? item->resolved : "NULL",
190
8.08k
            node ? "cache hit" : "cached");
191
8.08k
    }
192
8.08k
    if (item->resolved[0] == '\0') {
193
  /* negative result, free item if not cached */
194
956
  if (item->refcnt == 0)
195
0
      free(item);
196
956
  debug_return_str(NULL);
197
956
    }
198
7.13k
    item->refcnt++;
199
7.13k
    debug_return_str(item->resolved);
200
7.13k
}