diff --git a/MANIFEST b/MANIFEST index 626b5930a..dea73a8ad 100644 --- a/MANIFEST +++ b/MANIFEST @@ -740,6 +740,7 @@ plugins/sudoers/prompt.c plugins/sudoers/pwutil.c plugins/sudoers/pwutil.h plugins/sudoers/pwutil_impl.c +plugins/sudoers/canon_path.c plugins/sudoers/redblack.c plugins/sudoers/redblack.h plugins/sudoers/regress/check_symbols/check_symbols.c diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in index b2174332c..ca65b2618 100644 --- a/plugins/sudoers/Makefile.in +++ b/plugins/sudoers/Makefile.in @@ -1,7 +1,7 @@ # # SPDX-License-Identifier: ISC # -# Copyright (c) 1996, 1998-2005, 2007-2022 +# Copyright (c) 1996, 1998-2005, 2007-2023 # Todd C. Miller # # Permission to use, copy, modify, and distribute this software for any @@ -173,9 +173,9 @@ FUZZ_VERBOSE = AUTH_OBJS = sudo_auth.lo @AUTH_OBJS@ -LIBPARSESUDOERS_OBJS = alias.lo b64_decode.lo defaults.lo digestname.lo \ - exptilde.lo filedigest.lo gentime.lo gram.lo \ - match.lo match_addr.lo match_command.lo \ +LIBPARSESUDOERS_OBJS = alias.lo b64_decode.lo canon_path.lo defaults.lo \ + digestname.lo exptilde.lo filedigest.lo gentime.lo \ + gram.lo match.lo match_addr.lo match_command.lo \ match_digest.lo pivot.lo pwutil.lo pwutil_impl.lo \ redblack.lo strlist.lo sudoers_debug.lo timeout.lo \ timestr.lo toke.lo toke_util.lo @@ -914,6 +914,30 @@ bsm_audit.i: $(srcdir)/bsm_audit.c $(devdir)/def_data.h \ $(CC) -E -o $@ $(CPPFLAGS) $< bsm_audit.plog: bsm_audit.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/bsm_audit.c --i-file $< --output-file $@ +canon_path.lo: $(srcdir)/canon_path.c $(devdir)/def_data.h \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/redblack.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/canon_path.c +canon_path.i: $(srcdir)/canon_path.c $(devdir)/def_data.h \ + $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ + $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/redblack.h $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +canon_path.plog: canon_path.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/canon_path.c --i-file $< --output-file $@ check.lo: $(srcdir)/check.c $(devdir)/def_data.h $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \ diff --git a/plugins/sudoers/canon_path.c b/plugins/sudoers/canon_path.c new file mode 100644 index 000000000..b381859d2 --- /dev/null +++ b/plugins/sudoers/canon_path.c @@ -0,0 +1,199 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2023 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "sudoers.h" +#include "redblack.h" + +static struct rbtree *canon_cache; + +/* + * A cache_item includes storage for both the original path and the + * resolved path. The resolved path is directly embedded into the + * struct so that we can find the start of the struct cache_item + * given the value of resolved. Storage for pathname is embedded + * at the end, after resolved. + */ +struct cache_item { + unsigned int refcnt; + char *pathname; + char resolved[1]; /* actually bigger */ +}; + +/* + * Compare function for canon_cache. + * v1 is the key to find or data to insert, v2 is in-tree data. + */ +static int +compare(const void *v1, const void *v2) +{ + const struct cache_item *ci1 = (const struct cache_item *)v1; + const struct cache_item *ci2 = (const struct cache_item *)v2; + return strcmp(ci1->pathname, ci2->pathname); +} + +/* Convert a pointer returned by canon_path() to a struct cache_item *. */ +#define resolved_to_item(_r) ((struct cache_item *)((_r) - offsetof(struct cache_item, resolved))) + +/* + * Delete a ref from item and free if the refcount reaches 0. + */ +static void +canon_path_free_item(void *v) +{ + struct cache_item *item = v; + debug_decl(canon_path_free_item, SUDOERS_DEBUG_UTIL); + + if (--item->refcnt == 0) + free(item); + + debug_return; +} + +/* + * Delete a ref from the item containing "resolved" and free if + * the refcount reaches 0. + */ +void +canon_path_free(char *resolved) +{ + debug_decl(canon_path_free, SUDOERS_DEBUG_UTIL); + if (resolved != NULL) + canon_path_free_item(resolved_to_item(resolved)); + debug_return; +} + +/* + * Free canon_cache. + * This only removes the reference for that the cache owns. + * Other references remain valid until canon_path_free() is called. + */ +void +canon_path_free_cache(void) +{ + debug_decl(canon_path_free_cache, SUDOERS_DEBUG_UTIL); + + if (canon_cache != NULL) { + rbdestroy(canon_cache, canon_path_free_item); + canon_cache = NULL; + } + + debug_return; +} + +/* + * Like realpath(3) but caches the result. Returns an entry from the + * cache on success (with an added reference) or NULL on failure. + */ +char * +canon_path(const char *inpath) +{ + size_t item_size, inlen, reslen = 0; + char *resolved, resbuf[PATH_MAX]; + struct cache_item key, *item; + struct rbnode *node = NULL; + debug_decl(canon_path, SUDOERS_DEBUG_UTIL); + + if (canon_cache == NULL) { + canon_cache = rbcreate(compare); + if (canon_cache == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_str(NULL); + } + } else { + /* Check cache. */ + key.pathname = (char *)inpath; + if ((node = rbfind(canon_cache, &key)) != NULL) { + item = node->data; + goto done; + } + } + + /* + * Not cached, call realpath(3). + * Older realpath() doesn't support passing a NULL buffer. + * We special-case the empty string to resolve to "/". + * XXX - warn on errors other than ENOENT? + */ + if (*inpath == '\0') + resolved = (char *)"/"; + else + resolved = realpath(inpath, resbuf); + + inlen = strlen(inpath); + item_size = sizeof(*item) + inlen + 1; + if (resolved != NULL) { + reslen = strlen(resolved); + item_size += reslen; + } + item = malloc(item_size); + if (item == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_str(NULL); + } + if (resolved != NULL) + memcpy(item->resolved, resolved, reslen); + item->resolved[reslen] = '\0'; + item->pathname = item->resolved + reslen + 1; + memcpy(item->pathname, inpath, inlen); + item->pathname[inlen] = '\0'; + item->refcnt = 1; + switch (rbinsert(canon_cache, item, NULL)) { + case 1: + /* should not happen */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "path \"%s\" already exists in the cache", inpath); + item->refcnt = 0; + break; + case -1: + /* can't cache item, just return it */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "can't cache path \"%s\"", inpath); + item->refcnt = 0; + break; + } +done: + if (item->refcnt != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG, + "%s: path %s -> %s (%s)", __func__, inpath, + item->resolved[0] ? item->resolved : "NULL", + node ? "cache hit" : "cached"); + } + if (item->resolved[0] == '\0') { + /* negative result, free item if not cached */ + if (item->refcnt == 0) + free(item); + debug_return_str(NULL); + } + item->refcnt++; + debug_return_str(item->resolved); +} diff --git a/plugins/sudoers/sudoers.c b/plugins/sudoers/sudoers.c index 2b8d401e3..632cebb0f 100644 --- a/plugins/sudoers/sudoers.c +++ b/plugins/sudoers/sudoers.c @@ -1007,6 +1007,8 @@ set_cmnd_path(const char *runchroot) list_cmnd = NULL; free(user_cmnd); user_cmnd = NULL; + canon_path_free(user_cmnd_dir); + user_cmnd_dir = NULL; if (def_secure_path && !user_is_exempt()) path = def_secure_path; @@ -1031,6 +1033,17 @@ set_cmnd_path(const char *runchroot) goto error; } + if (cmnd_out != NULL) { + char *slash = strrchr(cmnd_out, '/'); + if (slash != NULL) { + *slash = '\0'; + user_cmnd_dir = canon_path(cmnd_out); + if (user_cmnd_dir == NULL && errno == ENOMEM) + goto error; + *slash = '/'; + } + } + if (ISSET(sudo_mode, MODE_CHECK)) list_cmnd = cmnd_out; else @@ -1849,6 +1862,7 @@ sudoers_cleanup(void) sudo_user_free(); sudo_freepwcache(); sudo_freegrcache(); + canon_path_free_cache(); /* Clear globals */ list_pw = NULL; @@ -1908,6 +1922,7 @@ sudo_user_free(void) free(user_srunhost); free(user_runhost); free(user_cmnd); + canon_path_free(user_cmnd_dir); free(user_args); free(list_cmnd); free(safe_cmnd); diff --git a/plugins/sudoers/sudoers.h b/plugins/sudoers/sudoers.h index a5e6d4425..591ba1004 100644 --- a/plugins/sudoers/sudoers.h +++ b/plugins/sudoers/sudoers.h @@ -102,6 +102,7 @@ struct sudo_user { char *cmnd; char *cmnd_args; char *cmnd_base; + char *cmnd_dir; char *cmnd_list; char *cmnd_safe; char *cmnd_saved; @@ -239,6 +240,7 @@ struct sudo_user { #define user_ttypath (sudo_user.ttypath) #define user_cwd (sudo_user.cwd) #define user_cmnd (sudo_user.cmnd) +#define user_cmnd_dir (sudo_user.cmnd_dir) #define user_args (sudo_user.cmnd_args) #define user_base (sudo_user.cmnd_base) #define user_stat (sudo_user.cmnd_stat) @@ -476,6 +478,11 @@ bool sudoers_gc_remove(enum sudoers_gc_types type, void *ptr); void sudoers_gc_init(void); void sudoers_gc_run(void); +/* canon_path.c */ +char *canon_path(const char *inpath); +void canon_path_free(char *resolved); +void canon_path_free_cache(void); + /* strlcpy_unesc.c */ size_t strlcpy_unescape(char *dst, const char *src, size_t size);