sudo/plugins/sudoers/cvtsudoers_merge.c

1300 lines
37 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2021-2024 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)
goto oom;
if (name != NULL) {
m->name = strdup(name);
if (m->name == NULL)
goto oom;
}
m->type = type;
debug_return_ptr(m);
oom:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
free(m);
debug_return_ptr(NULL);
}
/*
* 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 bool
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);
if (m == NULL)
debug_return_bool(false);
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_bool(true);
}
/*
* 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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_ptr(NULL);
}
/* 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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
free(m->name);
m->name = copy;
}
}
debug_return_int(0);
}
static bool
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_warnx(U_("%s: %s"), __func__,
U_("unable to allocate memory"));
debug_return_bool(false);
}
free(m->name);
m->name = copy;
}
}
}
debug_return_bool(true);
}
static bool
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_warnx(U_("%s: %s"), __func__,
U_("unable to allocate memory"));
debug_return_bool(false);
}
free(m->name);
m->name = copy;
}
debug_return_bool(true);
}
static bool
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) {
if (!alias_rename_member(old_name, new_name, m))
debug_return_bool(false);
}
debug_return_bool(true);
}
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;
debug_decl(alias_rename_userspecs, SUDOERS_DEBUG_ALIAS);
TAILQ_FOREACH(us, userspecs, entries) {
if (alias_type == USERALIAS) {
if (!alias_rename_member_list(old_name, new_name, &us->users)) {
debug_return_bool(false);
}
}
TAILQ_FOREACH(priv, &us->privileges, entries) {
if (!alias_rename_defaults(old_name, new_name, alias_type, &priv->defaults)) {
debug_return_bool(false);
}
if (alias_type == HOSTALIAS) {
if (!alias_rename_member_list(old_name, new_name, &priv->hostlist)) {
debug_return_bool(false);
}
continue;
}
TAILQ_FOREACH(cs, &priv->cmndlist, entries) {
if (alias_type == CMNDALIAS) {
if (!alias_rename_member(old_name, new_name, cs->cmnd)) {
debug_return_bool(false);
}
continue;
}
if (alias_type == RUNASALIAS) {
if (cs->runasuserlist != NULL) {
if (!alias_rename_member_list(old_name, new_name, cs->runasuserlist)) {
debug_return_bool(false);
}
}
if (cs->runasgrouplist != NULL) {
if (!alias_rename_member_list(old_name, new_name, cs->runasgrouplist)) {
debug_return_bool(false);
}
}
}
}
}
}
debug_return_bool(true);
}
/*
* Rename an alias in parse_tree and all the places where it is used.
* Takes ownership if new_name which must not be freed by the caller.
*/
static bool
alias_rename(const char *old_name, 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);
free(new_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 = new_name;
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);
alias_free(a);
debug_return_bool(false);
break;
default:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
alias_free(a);
debug_return_bool(false);
}
/* Rename it in the aliases tree itself (aliases can be nested). */
if (!alias_apply(parse_tree, alias_rename_members, &closure))
debug_return_bool(false);
/* Rename it in the Defaults list. */
if (!alias_rename_defaults(old_name, new_name, alias_type, &parse_tree->defaults))
debug_return_bool(false);
/* Rename it in the userspecs list. */
if (!alias_rename_userspecs(old_name, new_name, alias_type, &parse_tree->userspecs))
debug_return_bool(false);
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);
if (new_name == NULL)
debug_return_int(-1);
if (!alias_rename(a->name, new_name, a->type, parse_tree))
debug_return_int(-1);
}
/*
* 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);
debug_return_int(-1);
default:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
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.
*/
if (!alias_apply(parse_tree, alias_resolve_conflicts, merged_tree))
debug_return_bool(false);
/*
* 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,
CONFLICT_ERROR
};
/*
* 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);
if (m == NULL)
debug_return_int(CONFLICT_ERROR);
TAILQ_INSERT_TAIL(&d->binding->members, m, entries);
d->type = def->type;
}
if (def->type == DEFAULTS) {
struct member *m = new_member(NULL, ALL);
if (m == NULL)
debug_return_int(CONFLICT_ERROR);
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);
if (m == NULL)
debug_return_bool(false);
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;
default:
/* warning printed by defaults_check_conflict() */
free_default(def);
debug_return_bool(false);
}
}
}
/*
* 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) {
if (!simplify_host_list(&def->binding->members, def->file,
def->line, def->column, bound_hosts)) {
debug_return_bool(false);
}
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);
}
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);
}
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);
}
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);
}
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) {
char *copy = strdup(parse_tree->lhost);
if (copy == NULL) {
sudo_warnx(U_("%s: %s"), __func__,
U_("unable to allocate memory"));
debug_return_bool(false);
}
m->type = WORD;
m->name = copy;
}
}
}
}
}
/*
* 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;
default:
/* warning printed by defaults_check_conflict() */
free_userspec(us);
debug_return_bool(false);
}
}
}
/*
* 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? */
if (!simplify_host_list(&priv->hostlist, us->file, us->line,
us->column, bound_hosts)) {
debug_return_bool(false);
}
}
}
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);
if (m == NULL)
goto bad;
TAILQ_INSERT_TAIL(&bound_hosts, m, entries);
}
}
if ((merged_tree->aliases = alloc_aliases()) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
goto bad;
}
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);
}