diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 13:14:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 13:14:46 +0000 |
commit | 025c439e829e0db9ac511cd9c1b8d5fd53475ead (patch) | |
tree | fa6986b4690f991613ffb97cea1f6942427baf5d /plugins/sudoers/cvtsudoers_merge.c | |
parent | Initial commit. (diff) | |
download | sudo-025c439e829e0db9ac511cd9c1b8d5fd53475ead.tar.xz sudo-025c439e829e0db9ac511cd9c1b8d5fd53475ead.zip |
Adding upstream version 1.9.15p5.upstream/1.9.15p5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins/sudoers/cvtsudoers_merge.c')
-rw-r--r-- | plugins/sudoers/cvtsudoers_merge.c | 1247 |
1 files changed, 1247 insertions, 0 deletions
diff --git a/plugins/sudoers/cvtsudoers_merge.c b/plugins/sudoers/cvtsudoers_merge.c new file mode 100644 index 0000000..d5e5b08 --- /dev/null +++ b/plugins/sudoers/cvtsudoers_merge.c @@ -0,0 +1,1247 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <unistd.h> +#include <ctype.h> +#include <errno.h> + +#include <sudoers.h> +#include <redblack.h> +#include <cvtsudoers.h> +#include <gram.h> + +static struct member * +new_member(const char *name, short type) +{ + struct member *m; + debug_decl(digest_list_equivalent, SUDOERS_DEBUG_PARSER); + + m = calloc(1, sizeof(struct member)); + if (m == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + if (name != NULL) { + m->name = strdup(name); + if (m->name == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + } + m->type = type; + + debug_return_ptr(m); +} + +/* + * Compare two digest lists. + * Returns true if they are the same, else false. + * XXX - should not care about order + */ +static bool +digest_list_equivalent(struct command_digest_list *cdl1, + struct command_digest_list *cdl2) +{ + struct command_digest *cd1 = TAILQ_FIRST(cdl1); + struct command_digest *cd2 = TAILQ_FIRST(cdl2); + debug_decl(digest_list_equivalent, SUDOERS_DEBUG_PARSER); + + while (cd1 != NULL && cd2 != NULL) { + if (cd1->digest_type != cd2->digest_type) + debug_return_bool(false); + if (strcmp(cd1->digest_str, cd2->digest_str) != 0) + debug_return_bool(false); + cd1 = TAILQ_NEXT(cd1, entries); + cd2 = TAILQ_NEXT(cd2, entries); + } + + if (cd1 != NULL || cd2 != NULL) + debug_return_bool(false); + debug_return_bool(true); +} + +/* + * Compare two members. + * Returns true if they are the same, else false. + */ +static bool +member_equivalent(struct member *m1, struct member *m2) +{ + debug_decl(member_equivalent, SUDOERS_DEBUG_PARSER); + + if (m1->type != m2->type || m1->negated != m2->negated) + debug_return_bool(false); + + if (m1->type == COMMAND) { + struct sudo_command *c1 = (struct sudo_command *)m1->name; + struct sudo_command *c2 = (struct sudo_command *)m2->name; + if (c1->cmnd != NULL && c2->cmnd != NULL) { + if (strcmp(c1->cmnd, c2->cmnd) != 0) + debug_return_bool(false); + } else if (c1->cmnd != c2->cmnd) { + debug_return_bool(false); + } + + if (c1->args != NULL && c2->args != NULL) { + if (strcmp(c1->args, c2->args) != 0) + debug_return_bool(false); + } else if (c1->args != c2->args) { + debug_return_bool(false); + } + + if (!digest_list_equivalent(&c1->digests, &c2->digests)) { + debug_return_bool(false); + } + } else { + if (m1->name != NULL && m2->name != NULL) { + if (strcmp(m1->name, m2->name) != 0) + debug_return_bool(false); + } else if (m1->name != m2->name) { + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +/* + * Compare two members, m1 and m2. + * Returns true if m2 overrides m1, else false. + */ +static bool +member_overridden(struct member *m1, struct member *m2, bool check_negated) +{ + debug_decl(member_overridden, SUDOERS_DEBUG_PARSER); + + if (check_negated && m1->negated != m2->negated) + debug_return_bool(false); + + /* "ALL" always wins (modulo digest). */ + if (m2->type == ALL) { + if (m2->name != NULL) { + struct sudo_command *c1 = (struct sudo_command *)m1->name; + struct sudo_command *c2 = (struct sudo_command *)m2->name; + debug_return_bool(digest_list_equivalent(&c1->digests, &c2->digests)); + } + debug_return_bool(true); + } + + if (m1->type != m2->type) + debug_return_bool(false); + + if (m1->type == COMMAND) { + struct sudo_command *c1 = (struct sudo_command *)m1->name; + struct sudo_command *c2 = (struct sudo_command *)m2->name; + if (strcmp(c1->cmnd, c2->cmnd) != 0) + debug_return_bool(false); + + if (c1->args != NULL && c2->args != NULL) { + if (strcmp(c1->args, c2->args) != 0) + debug_return_bool(false); + } else if (c1->args != c2->args) { + debug_return_bool(false); + } + + if (!digest_list_equivalent(&c1->digests, &c2->digests)) { + debug_return_bool(false); + } + } else { + if (strcmp(m1->name, m2->name) != 0) + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Given two member lists, ml1 and ml2. + * Returns true if the every element of ml1 is overridden by ml2, else false. + */ +static bool +member_list_override(struct member_list *ml1, struct member_list *ml2, + bool check_negated) +{ + struct member *m1, *m2; + debug_decl(member_list_override, SUDOERS_DEBUG_PARSER); + + /* An empty member_list only overrides another empty list. */ + if (TAILQ_EMPTY(ml2)) { + debug_return_bool(TAILQ_EMPTY(ml1)); + } + + /* Check whether each element of ml1 is also covered by ml2. */ + TAILQ_FOREACH_REVERSE(m1, ml1, member_list, entries) { + bool overridden = false; + TAILQ_FOREACH_REVERSE(m2, ml2, member_list, entries) { + if (member_overridden(m1, m2, check_negated)) { + overridden = true; + break; + } + } + if (!overridden) + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Compare two member lists. + * Returns true if they are the same, else false. + * XXX - should not care about order if things are not negated. + */ +static bool +member_list_equivalent(struct member_list *ml1, struct member_list *ml2) +{ + struct member *m1 = TAILQ_FIRST(ml1); + struct member *m2 = TAILQ_FIRST(ml2); + debug_decl(member_list_equivalent, SUDOERS_DEBUG_PARSER); + + while (m1 != NULL && m2 != NULL) { + if (!member_equivalent(m1, m2)) + debug_return_bool(false); + m1 = TAILQ_NEXT(m1, entries); + m2 = TAILQ_NEXT(m2, entries); + } + + if (m1 != NULL || m2 != NULL) + debug_return_bool(false); + debug_return_bool(true); +} + +/* + * Attempt to simplify a host list. + * If a host list contains all hosts in bound_hosts, replace them with + * "ALL". Also prune hosts on either side of "ALL" when possible. + */ +static void +simplify_host_list(struct member_list *hosts, const char *file, int line, + int column, struct member_list *bound_hosts) +{ + struct member *m, *n, *next; + bool logged = false; + debug_decl(simplify_host_list, SUDOERS_DEBUG_PARSER); + + /* + * If all sudoers sources have an associated host, replace a + * list of those hosts with "ALL". + */ + if (!TAILQ_EMPTY(bound_hosts)) { + TAILQ_FOREACH_REVERSE(n, bound_hosts, member_list, entries) { + TAILQ_FOREACH_REVERSE(m, hosts, member_list, entries) { + if (m->negated) { + /* Don't try to handled negated entries. */ + m = NULL; + break; + } + if (m->type == n->type && strcmp(m->name, n->name) == 0) { + /* match */ + break; + } + } + if (m == NULL) { + /* no match */ + break; + } + } + if (n == NULL) { + /* found all hosts */ + log_warnx(U_("%s:%d:%d: converting host list to ALL"), + file, line, column); + logged = true; + + TAILQ_FOREACH_REVERSE(n, bound_hosts, member_list, entries) { + TAILQ_FOREACH_REVERSE_SAFE(m, hosts, member_list, entries, next) { + if (m->negated) { + /* Don't try to handled negated entries. */ + m = NULL; + break; + } + if (m->type == n->type && strcmp(m->name, n->name) == 0) { + /* remove matching host */ + TAILQ_REMOVE(hosts, m, entries); + free_member(m); + break; + } + } + } + m = new_member(NULL, ALL); + TAILQ_INSERT_TAIL(hosts, m, entries); + } + } + + /* + * A host list that contains ALL with no negated entries past it + * is equivalent to a list containing just "ALL". + */ + TAILQ_FOREACH_REVERSE(m, hosts, member_list, entries) { + if (m->negated) { + /* Don't try to handled negated entries. */ + break; + } + if (m->type == ALL) { + /* Replace member list with a single ALL entry. */ + if (!logged) { + log_warnx(U_("%s:%d:%d: converting host list to ALL"), + file, line, column); + } + TAILQ_REMOVE(hosts, m, entries); + free_members(hosts); + TAILQ_INSERT_TAIL(hosts, m, entries); + break; + } + } + + debug_return; +} + +/* + * Generate a unique name from old_name that is not used in parse_tree, + * subsequent parse_trees or merged_tree. + */ +static char * +alias_make_unique(const char *old_name, short type, + struct sudoers_parse_tree *parse_tree0, + struct sudoers_parse_tree *merged_tree) +{ + struct sudoers_parse_tree *parse_tree; + char *cp, *new_name = NULL; + struct alias *a; + long long suffix; + size_t namelen; + debug_decl(alias_make_unique, SUDOERS_DEBUG_ALIAS); + + /* If old_name already has a suffix, increment it, else start with "_1". */ + suffix = 0; + namelen = strlen(old_name); + cp = strrchr(old_name, '_'); + if (cp != NULL && isdigit((unsigned char)cp[1])) { + suffix = sudo_strtonum(cp + 1, 0, LLONG_MAX, NULL); + if (suffix != 0) { + namelen = (size_t)(cp - old_name); + } + } + + for (;;) { + suffix++; + free(new_name); + if (asprintf(&new_name, "%.*s_%lld", (int)namelen, old_name, suffix) == -1) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + /* Make sure new_name is not already in use. */ + a = alias_get(merged_tree, new_name, type); + if (a != NULL) { + alias_put(a); + continue; + } + parse_tree = parse_tree0; + while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) { + a = alias_get(parse_tree, new_name, type); + if (a != NULL) { + alias_put(a); + break; + } + } + if (a == NULL) { + /* Must be unique. */ + break; + } + } + + debug_return_ptr(new_name); +} + +struct alias_rename_closure { + const char *old_name; + const char *new_name; + int type; +}; + +static int +alias_rename_members(struct sudoers_parse_tree *parse_tree, struct alias *a, + void *v) +{ + struct alias_rename_closure *closure = v; + struct member *m; + debug_decl(alias_rename_members, SUDOERS_DEBUG_ALIAS); + + if (a->type != closure->type) + debug_return_int(0); + + /* Replace old_name in member list, if present. */ + TAILQ_FOREACH(m, &a->members, entries) { + if (m->type == ALIAS && strcmp(m->name, closure->old_name) == 0) { + char *copy = strdup(closure->new_name); + if (copy == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(m->name); + m->name = copy; + } + } + + debug_return_int(0); +} + +static void +alias_rename_defaults(const char *old_name, const char *new_name, + short alias_type, struct defaults_list *defaults) +{ + struct defaults *def, *def_next; + struct member *m; + debug_decl(alias_rename_defaults, SUDOERS_DEBUG_ALIAS); + + TAILQ_FOREACH_SAFE(def, defaults, entries, def_next) { + /* Consecutive Defaults can share the same binding. */ + if (def_next != NULL && def->binding == def_next->binding) + continue; + + switch (def->type) { + case DEFAULTS_USER: + if (alias_type != USERALIAS) + continue; + break; + case DEFAULTS_RUNAS: + if (alias_type != RUNASALIAS) + continue; + break; + case DEFAULTS_HOST: + if (alias_type != HOSTALIAS) + continue; + break; + default: + continue; + } + + /* Rename matching aliases in the binding's member_list. */ + TAILQ_FOREACH(m, &def->binding->members, entries) { + if (m->type != ALIAS) + continue; + if (strcmp(m->name, old_name) == 0) { + char *copy = strdup(new_name); + if (copy == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + free(m->name); + m->name = copy; + } + } + } + + debug_return; +} + +static void +alias_rename_member(const char *old_name, const char *new_name, + struct member *m) +{ + debug_decl(alias_rename_member, SUDOERS_DEBUG_ALIAS); + + if (m->type == ALIAS && strcmp(m->name, old_name) == 0) { + char *copy = strdup(new_name); + if (copy == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + free(m->name); + m->name = copy; + } + + debug_return; +} + +static void +alias_rename_member_list(const char *old_name, const char *new_name, + struct member_list *members) +{ + struct member *m; + debug_decl(alias_rename_member_list, SUDOERS_DEBUG_ALIAS); + + TAILQ_FOREACH(m, members, entries) { + alias_rename_member(old_name, new_name, m); + } + + debug_return; +} + +static bool +alias_rename_userspecs(const char *old_name, const char *new_name, + short alias_type, struct userspec_list *userspecs) +{ + struct privilege *priv; + struct cmndspec *cs; + struct userspec *us; + bool ret = true; + debug_decl(alias_rename_userspecs, SUDOERS_DEBUG_ALIAS); + + TAILQ_FOREACH(us, userspecs, entries) { + if (alias_type == USERALIAS) { + alias_rename_member_list(old_name, new_name, &us->users); + } + TAILQ_FOREACH(priv, &us->privileges, entries) { + alias_rename_defaults(old_name, new_name, alias_type, &priv->defaults); + if (alias_type == HOSTALIAS) { + alias_rename_member_list(old_name, new_name, &priv->hostlist); + continue; + } + TAILQ_FOREACH(cs, &priv->cmndlist, entries) { + if (alias_type == CMNDALIAS) { + alias_rename_member(old_name, new_name, cs->cmnd); + continue; + } + if (alias_type == RUNASALIAS) { + if (cs->runasuserlist != NULL) { + alias_rename_member_list(old_name, new_name, cs->runasuserlist); + } + if (cs->runasgrouplist != NULL) { + alias_rename_member_list(old_name, new_name, cs->runasgrouplist); + } + } + } + } + } + + debug_return_bool(ret); +} + +/* + * Rename an alias in parse_tree and all the places where it is used. + */ +static bool +alias_rename(const char *old_name, const char *new_name, + short alias_type, struct sudoers_parse_tree *parse_tree) +{ + struct alias_rename_closure closure = { old_name, new_name, alias_type }; + struct alias *a; + debug_decl(alias_rename, SUDOERS_DEBUG_ALIAS); + + /* Remove under old name and add via new to maintain tree properties. */ + a = alias_remove(parse_tree, old_name, alias_type); + if (a == NULL) { + /* Should not happen. */ + sudo_warnx(U_("unable to find alias %s"), old_name); + debug_return_bool(false); + } + log_warnx(U_("%s:%d:%d: renaming alias %s to %s"), + a->file, a->line, a->column, a->name, new_name); + free(a->name); + a->name = strdup(new_name); + if (a->name == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + switch (rbinsert(parse_tree->aliases, a, NULL)) { + case 0: + /* success */ + break; + case 1: + /* Already present, should not happen. */ + errno = EEXIST; + sudo_warn(U_("%s: %s"), __func__, a->name); + break; + default: + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + } + + /* Rename it in the aliases tree itself (aliases can be nested). */ + alias_apply(parse_tree, alias_rename_members, &closure); + + /* Rename it in the Defaults list. */ + alias_rename_defaults(old_name, new_name, alias_type, &parse_tree->defaults); + + /* Rename it in the userspecs list. */ + alias_rename_userspecs(old_name, new_name, alias_type, &parse_tree->userspecs); + + debug_return_bool(true); +} + +static int +alias_resolve_conflicts(struct sudoers_parse_tree *parse_tree0, struct alias *a, + void *v) +{ + struct sudoers_parse_tree *parse_tree = parse_tree0; + struct sudoers_parse_tree *merged_tree = v; + char *new_name; + int ret; + debug_decl(alias_resolve_conflicts, SUDOERS_DEBUG_ALIAS); + + /* + * Check for conflicting alias names in the subsequent sudoers files. + * Duplicates are removed and conflicting aliases are renamed. + * We cannot modify the alias tree that we are traversing. + */ + while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) { + struct alias *b = alias_get(parse_tree, a->name, a->type); + if (b == NULL) + continue; + + /* If alias 'b' is equivalent, remove it. */ + alias_put(b); + if (member_list_equivalent(&a->members, &b->members)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "removing duplicate alias %s from %p", a->name, parse_tree); + b = alias_remove(parse_tree, a->name, a->type); + log_warnx(U_("%s:%d:%d: removing duplicate alias %s"), + b->file, b->line, b->column, b->name); + alias_free(b); + continue; + } + + /* Rename alias 'b' to avoid a naming conflict. */ + new_name = alias_make_unique(a->name, a->type, parse_tree, merged_tree); + alias_rename(a->name, new_name, a->type, parse_tree); + free(new_name); + } + + /* + * The alias will exist in both the original and merged trees. + * This is not a problem as the caller will delete the old trees + * (without freeing the data). + */ + ret = rbinsert(merged_tree->aliases, a, NULL); + switch (ret) { + case 0: + /* success */ + break; + case 1: + /* already present, should not happen. */ + errno = EEXIST; + sudo_warn(U_("%s: %s"), __func__, a->name); + break; + default: + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + } + + debug_return_int(0); +} + +static bool +merge_aliases(struct sudoers_parse_tree_list *parse_trees, + struct sudoers_parse_tree *merged_tree) +{ + struct sudoers_parse_tree *parse_tree; + debug_decl(merge_aliases, SUDOERS_DEBUG_ALIAS); + + /* + * For each parse_tree, check for collisions with alias names + * in subsequent parse trees. On collision, add a numbered + * suffix (e.g. ALIAS_1) to make the name unique and rename + * any uses of that alias in the affected parse_tree. + */ + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + if (parse_tree->aliases == NULL) + continue; + + /* + * Resolve any conflicts in alias names, renaming aliases as + * needed and eliminating duplicates. + */ + alias_apply(parse_tree, alias_resolve_conflicts, merged_tree); + + /* + * Destroy the old alias tree without freeing the alias data + * which has been copied to merged_tree. + */ + rbdestroy(parse_tree->aliases, NULL); + parse_tree->aliases = NULL; + } + + debug_return_bool(true); +} + +/* + * Compare two defaults structs but not their actual value. + * Returns true if they refer to the same Defaults variable and binding. + * Also sets mergeable if they only differ in the binding. + */ +static bool +defaults_var_matches(struct defaults *d1, struct defaults *d2, + bool *mergeable) +{ + debug_decl(defaults_var_matches, SUDOERS_DEBUG_DEFAULTS); + + if (strcmp(d1->var, d2->var) != 0) + debug_return_bool(false); + if (d1->type != d2->type) { + if ((d1->type == DEFAULTS && d2->type == DEFAULTS_HOST) || + (d1->type == DEFAULTS_HOST && d2->type == DEFAULTS)) { + /* We can merge host and global bindings. */ + if (mergeable != NULL) + *mergeable = true; + } + debug_return_bool(false); + } + if (d1->type != DEFAULTS) { + if (!member_list_equivalent(&d1->binding->members, &d2->binding->members)) { + if (mergeable != NULL) + *mergeable = true; + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +/* + * Compare the values of two defaults structs, which must be of the same type. + * Returns true if the value and operator match, else false. + */ +static bool +defaults_val_matches(struct defaults *d1, struct defaults *d2) +{ + debug_decl(defaults_val_matches, SUDOERS_DEBUG_DEFAULTS); + + /* XXX - what about list operators? */ + if (d1->op != d2->op) + debug_return_bool(false); + + /* Either both must be NULL or both non-NULL _and_ matching. */ + if (d1->val != NULL && d2->val != NULL) { + if (strcmp(d1->val, d2->val) != 0) + debug_return_bool(false); + } else { + if (d1->val != NULL || d2->val != NULL) + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Returns true if d1 is equivalent to d2, else false. + */ +static bool +defaults_equivalent(struct defaults *d1, struct defaults *d2) +{ + debug_decl(defaults_equivalent, SUDOERS_DEBUG_DEFAULTS); + + if (!defaults_var_matches(d1, d2, NULL)) + debug_return_bool(false); + debug_return_bool(defaults_val_matches(d1, d2)); +} + +/* + * Returns true if dl1 is equivalent to dl2, else false. + */ +static bool +defaults_list_equivalent(struct defaults_list *dl1, struct defaults_list *dl2) +{ + struct defaults *d1 = TAILQ_FIRST(dl1); + struct defaults *d2 = TAILQ_FIRST(dl2); + debug_decl(defaults_list_equivalent, SUDOERS_DEBUG_DEFAULTS); + + while (d1 != NULL && d2 != NULL) { + if (!defaults_equivalent(d1, d2)) + debug_return_bool(false); + d1 = TAILQ_NEXT(d1, entries); + d2 = TAILQ_NEXT(d2, entries); + } + + if (d1 != NULL || d2 != NULL) + debug_return_bool(false); + debug_return_bool(true); +} + +enum cvtsudoers_conflict { + CONFLICT_NONE, + CONFLICT_RESOLVED, + CONFLICT_UNRESOLVED +}; + +/* + * Check for duplicate and conflicting Defaults entries in later sudoers files. + * Returns true if we find a conflict or duplicate, else false. + */ +static enum cvtsudoers_conflict +defaults_check_conflict(struct defaults *def, + struct sudoers_parse_tree *parse_tree0) +{ + struct sudoers_parse_tree *parse_tree = parse_tree0; + struct defaults *d; + debug_decl(defaults_check_conflict, SUDOERS_DEBUG_DEFAULTS); + + while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) { + TAILQ_FOREACH_REVERSE(d, &parse_tree->defaults, defaults_list, entries) { + bool mergeable = false; + + /* + * We currently only merge host-based Defaults but could do + * others as well. Lists in Defaults entries can be harder + * to read, especially command lists. + */ + if (!defaults_var_matches(def, d, &mergeable)) { + if (!mergeable || (def->type != DEFAULTS && def->type != DEFAULTS_HOST)) + continue; + } + if (defaults_val_matches(def, d)) { + /* Duplicate Defaults entry (may need to merge binding). */ + if (mergeable) { + if (d->type != def->type && + (d->type == DEFAULTS || def->type == DEFAULTS)) { + /* + * To be able to merge two Defaults, they both must + * have the same binding type. Convert a global + * Defaults to one bound to single "ALL" member. + */ + if (d->type == DEFAULTS) { + struct member *m = new_member(NULL, ALL); + TAILQ_INSERT_TAIL(&d->binding->members, m, entries); + d->type = def->type; + } + if (def->type == DEFAULTS) { + struct member *m = new_member(NULL, ALL); + TAILQ_INSERT_TAIL(&def->binding->members, m, entries); + def->type = d->type; + } + } + + /* Prepend def binding to d (hence double concat). */ + TAILQ_CONCAT(&def->binding->members, &d->binding->members, entries); + TAILQ_CONCAT(&d->binding->members, &def->binding->members, entries); + } + debug_return_int(CONFLICT_RESOLVED); + } + /* + * If the value doesn't match but the Defaults name did we don't + * consider that a conflict. + */ + if (!mergeable) { + log_warnx(U_("%s:%d:%d: conflicting Defaults entry \"%s\" host-specific in %s:%d:%d"), + def->file, def->line, def->column, def->var, + d->file, d->line, d->column); + debug_return_int(CONFLICT_UNRESOLVED); + } + } + } + + debug_return_int(CONFLICT_NONE); +} + +/* + * Merge Defaults entries in parse_trees and store the result in + * merged_tree. If a hostname was specified with the sudoers source, + * create a host-specific Defaults entry where possible. + * Returns true on success, else false. + */ +static bool +merge_defaults(struct sudoers_parse_tree_list *parse_trees, + struct sudoers_parse_tree *merged_tree, struct member_list *bound_hosts) +{ + struct sudoers_parse_tree *parse_tree; + struct defaults *def; + struct member *m; + debug_decl(merge_defaults, SUDOERS_DEBUG_DEFAULTS); + + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + /* + * If parse_tree has a host name associated with it, + * try to make the Defaults setting host-specific. + */ + TAILQ_FOREACH(def, &parse_tree->defaults, entries) { + if (parse_tree->lhost != NULL && def->type == DEFAULTS) { + m = new_member(parse_tree->lhost, WORD); + log_warnx(U_("%s:%d:%d: made Defaults \"%s\" specific to host %s"), + def->file, def->line, def->column, def->var, + parse_tree->lhost); + TAILQ_INSERT_TAIL(&def->binding->members, m, entries); + def->type = DEFAULTS_HOST; + } + } + } + + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + while ((def = TAILQ_FIRST(&parse_tree->defaults)) != NULL) { + /* + * Only add Defaults entry if not overridden by subsequent sudoers. + */ + TAILQ_REMOVE(&parse_tree->defaults, def, entries); + switch (defaults_check_conflict(def, parse_tree)) { + case CONFLICT_NONE: + if (def->type != DEFAULTS_HOST) { + log_warnx(U_("%s:%d:%d: unable to make Defaults \"%s\" host-specific"), + def->file, def->line, def->column, def->var); + } + TAILQ_INSERT_TAIL(&merged_tree->defaults, def, entries); + break; + case CONFLICT_RESOLVED: + /* Duplicate or merged into a subsequent Defaults setting. */ + free_default(def); + break; + case CONFLICT_UNRESOLVED: + log_warnx(U_("%s:%d:%d: removing Defaults \"%s\" overridden by subsequent entries"), + def->file, def->line, def->column, def->var); + free_default(def); + break; + } + } + } + + /* + * Simplify host lists in the merged Defaults. + */ + TAILQ_FOREACH(def, &merged_tree->defaults, entries) { + /* TODO: handle refcnt != 1 */ + if (def->type == DEFAULTS_HOST && def->binding->refcnt == 1) { + simplify_host_list(&def->binding->members, def->file, def->line, + def->column, bound_hosts); + m = TAILQ_FIRST(&def->binding->members); + if (m->type == ALL && !m->negated) { + if (TAILQ_NEXT(m, entries) == NULL) { + /* Convert Defaults@ALL -> Defaults */ + def->type = DEFAULTS; + free_members(&def->binding->members); + TAILQ_INIT(&def->binding->members); + } + } + } + } + + debug_return_bool(true); +} + +/* + * Returns true if cs1 is equivalent to cs2, else false. + */ +static bool +cmndspec_equivalent(struct cmndspec *cs1, struct cmndspec *cs2, bool check_negated) +{ + debug_decl(cmndspec_equivalent, SUDOERS_DEBUG_PARSER); + + if (cs1->runasuserlist != NULL && cs2->runasuserlist != NULL) { + if (!member_list_override(cs1->runasuserlist, cs2->runasuserlist, check_negated)) + debug_return_bool(false); + } else if (cs1->runasuserlist != cs2->runasuserlist) { + debug_return_bool(false); + } + if (cs1->runasgrouplist != NULL && cs2->runasgrouplist != NULL) { + if (!member_list_override(cs1->runasgrouplist, cs2->runasgrouplist, check_negated)) + debug_return_bool(false); + } else if (cs1->runasgrouplist != cs2->runasgrouplist) { + debug_return_bool(false); + } + if (!member_equivalent(cs1->cmnd, cs2->cmnd)) + debug_return_bool(false); + if (TAGS_CHANGED(cs1->tags, cs2->tags)) + debug_return_bool(false); + if (cs1->timeout != cs2->timeout) + debug_return_bool(false); + if (cs1->notbefore != cs2->notbefore) + debug_return_bool(false); + if (cs1->notafter != cs2->notafter) + debug_return_bool(false); + if (cs1->runcwd != NULL && cs2->runcwd != NULL) { + if (strcmp(cs1->runcwd, cs2->runcwd) != 0) + debug_return_bool(false); + } else if (cs1->runcwd != cs2->runcwd) { + debug_return_bool(false); + } + if (cs1->runchroot != NULL && cs2->runchroot != NULL) { + if (strcmp(cs1->runchroot, cs2->runchroot) != 0) + debug_return_bool(false); + } else if (cs1->runchroot != cs2->runchroot) { + debug_return_bool(false); + } +#ifdef HAVE_SELINUX + if (cs1->role != NULL && cs2->role != NULL) { + if (strcmp(cs1->role, cs2->role) != 0) + debug_return_bool(false); + } else if (cs1->role != cs2->role) { + debug_return_bool(false); + } + if (cs1->type != NULL && cs2->type != NULL) { + if (strcmp(cs1->type, cs2->type) != 0) + debug_return_bool(false); + } else if (cs1->type != cs2->type) { + debug_return_bool(false); + } +#endif +#ifdef HAVE_APPARMOR + if (cs1->apparmor_profile != NULL && cs2->apparmor_profile != NULL) { + if (strcmp(cs1->apparmor_profile, cs2->apparmor_profile) != 0) + debug_return_bool(false); + } else if (cs1->apparmor_profile != cs2->apparmor_profile) { + debug_return_bool(false); + } +#endif +#ifdef HAVE_PRIV_SET + if (cs1->privs != NULL && cs2->privs != NULL) { + if (strcmp(cs1->privs, cs2->privs) != 0) + debug_return_bool(false); + } else if (cs1->privs != cs2->privs) { + debug_return_bool(false); + } + if (cs1->limitprivs != NULL && cs2->limitprivs != NULL) { + if (strcmp(cs1->limitprivs, cs2->limitprivs) != 0) + debug_return_bool(false); + } else if (cs1->limitprivs != cs2->limitprivs) { + debug_return_bool(false); + } +#endif + + debug_return_bool(true); +} + +/* + * Returns true if csl1 is equivalent to csl2, else false. + */ +static bool +cmndspec_list_equivalent(struct cmndspec_list *csl1, struct cmndspec_list *csl2, + bool check_negated) +{ + struct cmndspec *cs1 = TAILQ_FIRST(csl1); + struct cmndspec *cs2 = TAILQ_FIRST(csl2); + debug_decl(cmndspec_list_equivalent, SUDOERS_DEBUG_PARSER); + + while (cs1 != NULL && cs2 != NULL) { + if (!cmndspec_equivalent(cs1, cs2, check_negated)) + debug_return_bool(false); + cs1 = TAILQ_NEXT(cs1, entries); + cs2 = TAILQ_NEXT(cs2, entries); + } + + if (cs1 != NULL || cs2 != NULL) + debug_return_bool(false); + debug_return_bool(true); +} + +/* + * Check whether userspec us1 is overridden by another sudoers file entry. + * If us1 and another userspec differ only in their host lists, merges + * the hosts from us1 into that userspec. + * Returns true if overridden, else false. + * TODO: merge privs + */ +static enum cvtsudoers_conflict +userspec_overridden(struct userspec *us1, + struct sudoers_parse_tree *parse_tree, bool check_negated) +{ + struct userspec *us2; + bool hosts_differ = false; + debug_decl(userspec_overridden, SUDOERS_DEBUG_PARSER); + + if (TAILQ_EMPTY(&parse_tree->userspecs)) + debug_return_int(CONFLICT_NONE); + + /* Sudoers rules are applied in reverse order (last match wins). */ + TAILQ_FOREACH_REVERSE(us2, &parse_tree->userspecs, userspec_list, entries) { + struct privilege *priv1, *priv2; + + if (!member_list_override(&us1->users, &us2->users, check_negated)) + continue; + + /* XXX - order should not matter */ + priv1 = TAILQ_LAST(&us1->privileges, privilege_list); + priv2 = TAILQ_LAST(&us2->privileges, privilege_list); + while (priv1 != NULL && priv2 != NULL) { + if (!defaults_list_equivalent(&priv1->defaults, &priv2->defaults)) + break; + if (!cmndspec_list_equivalent(&priv1->cmndlist, &priv2->cmndlist, check_negated)) + break; + + if (!member_list_override(&priv1->hostlist, &priv2->hostlist, check_negated)) + hosts_differ = true; + + priv1 = TAILQ_PREV(priv1, privilege_list, entries); + priv2 = TAILQ_PREV(priv2, privilege_list, entries); + } + if (priv1 != NULL || priv2 != NULL) { + /* mismatch */ + continue; + } + + /* + * If we have a match of everything except the host list, + * merge the differing host lists. + */ + if (hosts_differ) { + priv1 = TAILQ_LAST(&us1->privileges, privilege_list); + priv2 = TAILQ_LAST(&us2->privileges, privilege_list); + while (priv1 != NULL && priv2 != NULL) { + if (!member_list_override(&priv1->hostlist, &priv2->hostlist, check_negated)) { + /* + * Priv matches but hosts differ, prepend priv1 hostlist + * to into priv2 hostlist (hence the double concat). + */ + TAILQ_CONCAT(&priv1->hostlist, &priv2->hostlist, entries); + TAILQ_CONCAT(&priv2->hostlist, &priv1->hostlist, entries); + log_warnx(U_("%s:%d:%d: merging userspec into %s:%d:%d"), + us1->file, us1->line, us1->column, + us2->file, us2->line, us2->column); + } + priv1 = TAILQ_PREV(priv1, privilege_list, entries); + priv2 = TAILQ_PREV(priv2, privilege_list, entries); + } + debug_return_int(CONFLICT_RESOLVED); + } + debug_return_int(CONFLICT_UNRESOLVED); + } + + debug_return_int(CONFLICT_NONE); +} + +/* + * Check whether userspec us1 is overridden by another sudoers file entry. + * If us1 and another userspec differ only in their host lists, merges + * the hosts from us1 into that userspec. + * Returns true if overridden, else false. + */ +static enum cvtsudoers_conflict +userspec_check_conflict(struct userspec *us1, + struct sudoers_parse_tree *parse_tree0) +{ + struct sudoers_parse_tree *parse_tree = parse_tree0; + debug_decl(userspec_check_conflict, SUDOERS_DEBUG_PARSER); + + while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) { + enum cvtsudoers_conflict ret = + userspec_overridden(us1, parse_tree, false); + if (ret != CONFLICT_NONE) + debug_return_int(ret); + } + + debug_return_int(CONFLICT_NONE); +} + +/* + * Merge userspecs in parse_trees and store the result in merged_tree. + * If a hostname was specified with the sudoers source, make the + * privilege host-specific where possible. + * Returns true on success, else false. + */ +static bool +merge_userspecs(struct sudoers_parse_tree_list *parse_trees, + struct sudoers_parse_tree *merged_tree, struct member_list *bound_hosts) +{ + struct sudoers_parse_tree *parse_tree; + struct userspec *us; + struct privilege *priv; + struct member *m; + debug_decl(merge_userspecs, SUDOERS_DEBUG_DEFAULTS); + + /* + * If parse_tree has a host name associated with it, + * try to make the privilege host-specific. + */ + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + if (parse_tree->lhost == NULL) + continue; + TAILQ_FOREACH(us, &parse_tree->userspecs, entries) { + TAILQ_FOREACH(priv, &us->privileges, entries) { + TAILQ_FOREACH(m, &priv->hostlist, entries) { + /* We don't alter !ALL in a hostlist (XXX - should we?). */ + if (m->type == ALL && !m->negated) { + m->type = WORD; + m->name = strdup(parse_tree->lhost); + if (m->name == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + } + } + } + } + } + + /* + * Prune out duplicate userspecs after substituting hostname(s). + * Traverse the list in reverse order--in sudoers last match wins. + * XXX - do this at the privilege/cmndspec level instead. + */ + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + while ((us = TAILQ_LAST(&parse_tree->userspecs, userspec_list)) != NULL) { + TAILQ_REMOVE(&parse_tree->userspecs, us, entries); + switch (userspec_check_conflict(us, parse_tree)) { + case CONFLICT_NONE: + TAILQ_INSERT_HEAD(&merged_tree->userspecs, us, entries); + break; + case CONFLICT_RESOLVED: + free_userspec(us); + break; + case CONFLICT_UNRESOLVED: + log_warnx(U_("%s:%d:%d: removing userspec overridden by subsequent entries"), + us->file, us->line, us->column); + free_userspec(us); + break; + } + } + } + + /* + * Simplify member lists in the merged tree. + * Convert host lists with all hosts listed to "ALL" and + * collapse other entries around "ALL". + */ + TAILQ_FOREACH_REVERSE(us, &merged_tree->userspecs, userspec_list, entries) { + TAILQ_FOREACH_REVERSE(priv, &us->privileges, privilege_list, entries) { + /* TODO: simplify other lists? */ + simplify_host_list(&priv->hostlist, us->file, us->line, us->column, + bound_hosts); + } + } + + debug_return_bool(true); +} + +struct sudoers_parse_tree * +merge_sudoers(struct sudoers_parse_tree_list *parse_trees, + struct sudoers_parse_tree *merged_tree) +{ + struct member_list bound_hosts = TAILQ_HEAD_INITIALIZER(bound_hosts); + struct sudoers_parse_tree *parse_tree; + debug_decl(merge_sudoers, SUDOERS_DEBUG_UTIL); + + /* + * If all sudoers sources have a host associated with them, we + * can replace a list of those hosts with "ALL" in Defaults + * and userspecs. + */ + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + if (parse_tree->lhost == NULL) + break; + } + if (parse_tree == NULL) { + TAILQ_FOREACH(parse_tree, parse_trees, entries) { + struct member *m = new_member(parse_tree->lhost, WORD); + TAILQ_INSERT_TAIL(&bound_hosts, m, entries); + } + } + + if ((merged_tree->aliases = alloc_aliases()) == NULL) + sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + if (!merge_aliases(parse_trees, merged_tree)) + goto bad; + + if (!merge_defaults(parse_trees, merged_tree, &bound_hosts)) + goto bad; + + if (!merge_userspecs(parse_trees, merged_tree, &bound_hosts)) + goto bad; + + free_members(&bound_hosts); + debug_return_ptr(merged_tree); +bad: + free_members(&bound_hosts); + debug_return_ptr(NULL); +} |