diff options
Diffstat (limited to 'security/apparmor/domain.c')
-rw-r--r-- | security/apparmor/domain.c | 355 |
1 files changed, 256 insertions, 99 deletions
diff --git a/security/apparmor/domain.c b/security/apparmor/domain.c index 6a54d2ffa840..590b7e8cd21c 100644 --- a/security/apparmor/domain.c +++ b/security/apparmor/domain.c @@ -19,10 +19,11 @@ #include <linux/syscalls.h> #include <linux/tracehook.h> #include <linux/personality.h> +#include <linux/xattr.h> #include "include/audit.h" #include "include/apparmorfs.h" -#include "include/context.h" +#include "include/cred.h" #include "include/domain.h" #include "include/file.h" #include "include/ipc.h" @@ -302,7 +303,70 @@ static int change_profile_perms(struct aa_profile *profile, } /** + * aa_xattrs_match - check whether a file matches the xattrs defined in profile + * @bprm: binprm struct for the process to validate + * @profile: profile to match against (NOT NULL) + * @state: state to start match in + * + * Returns: number of extended attributes that matched, or < 0 on error + */ +static int aa_xattrs_match(const struct linux_binprm *bprm, + struct aa_profile *profile, unsigned int state) +{ + int i; + ssize_t size; + struct dentry *d; + char *value = NULL; + int value_size = 0, ret = profile->xattr_count; + + if (!bprm || !profile->xattr_count) + return 0; + + /* transition from exec match to xattr set */ + state = aa_dfa_null_transition(profile->xmatch, state); + + d = bprm->file->f_path.dentry; + + for (i = 0; i < profile->xattr_count; i++) { + size = vfs_getxattr_alloc(d, profile->xattrs[i], &value, + value_size, GFP_KERNEL); + if (size >= 0) { + u32 perm; + + /* Check the xattr value, not just presence */ + state = aa_dfa_match_len(profile->xmatch, state, value, + size); + perm = dfa_user_allow(profile->xmatch, state); + if (!(perm & MAY_EXEC)) { + ret = -EINVAL; + goto out; + } + } + /* transition to next element */ + state = aa_dfa_null_transition(profile->xmatch, state); + if (size < 0) { + /* + * No xattr match, so verify if transition to + * next element was valid. IFF so the xattr + * was optional. + */ + if (!state) { + ret = -EINVAL; + goto out; + } + /* don't count missing optional xattr as matched */ + ret--; + } + } + +out: + kfree(value); + return ret; +} + +/** * __attach_match_ - find an attachment match + * @bprm - binprm structure of transitioning task * @name - to match against (NOT NULL) * @head - profile list to walk (NOT NULL) * @info - info message if there was an error (NOT NULL) @@ -316,40 +380,80 @@ static int change_profile_perms(struct aa_profile *profile, * * Returns: profile or NULL if no match found */ -static struct aa_profile *__attach_match(const char *name, +static struct aa_profile *__attach_match(const struct linux_binprm *bprm, + const char *name, struct list_head *head, const char **info) { - int len = 0; + int candidate_len = 0, candidate_xattrs = 0; bool conflict = false; struct aa_profile *profile, *candidate = NULL; + AA_BUG(!name); + AA_BUG(!head); + list_for_each_entry_rcu(profile, head, base.list) { if (profile->label.flags & FLAG_NULL && &profile->label == ns_unconfined(profile->ns)) continue; + /* Find the "best" matching profile. Profiles must + * match the path and extended attributes (if any) + * associated with the file. A more specific path + * match will be preferred over a less specific one, + * and a match with more matching extended attributes + * will be preferred over one with fewer. If the best + * match has both the same level of path specificity + * and the same number of matching extended attributes + * as another profile, signal a conflict and refuse to + * match. + */ if (profile->xmatch) { - if (profile->xmatch_len >= len) { - unsigned int state; - u32 perm; - - state = aa_dfa_match(profile->xmatch, - DFA_START, name); - perm = dfa_user_allow(profile->xmatch, state); - /* any accepting state means a valid match. */ - if (perm & MAY_EXEC) { - if (profile->xmatch_len == len) { + unsigned int state, count; + u32 perm; + + state = aa_dfa_leftmatch(profile->xmatch, DFA_START, + name, &count); + perm = dfa_user_allow(profile->xmatch, state); + /* any accepting state means a valid match. */ + if (perm & MAY_EXEC) { + int ret; + + if (count < candidate_len) + continue; + + ret = aa_xattrs_match(bprm, profile, state); + /* Fail matching if the xattrs don't match */ + if (ret < 0) + continue; + + /* + * TODO: allow for more flexible best match + * + * The new match isn't more specific + * than the current best match + */ + if (count == candidate_len && + ret <= candidate_xattrs) { + /* Match is equivalent, so conflict */ + if (ret == candidate_xattrs) conflict = true; - continue; - } - candidate = profile; - len = profile->xmatch_len; - conflict = false; + continue; } + + /* Either the same length with more matching + * xattrs, or a longer match + */ + candidate = profile; + candidate_len = profile->xmatch_len; + candidate_xattrs = ret; + conflict = false; } } else if (!strcmp(profile->base.name, name)) - /* exact non-re match, no more searching required */ + /* + * old exact non-re match, without conditionals such + * as xattrs. no more searching required + */ return profile; } @@ -363,6 +467,7 @@ static struct aa_profile *__attach_match(const char *name, /** * find_attach - do attachment search for unconfined processes + * @bprm - binprm structure of transitioning task * @ns: the current namespace (NOT NULL) * @list: list to search (NOT NULL) * @name: the executable name to match against (NOT NULL) @@ -370,13 +475,14 @@ static struct aa_profile *__attach_match(const char *name, * * Returns: label or NULL if no match found */ -static struct aa_label *find_attach(struct aa_ns *ns, struct list_head *list, +static struct aa_label *find_attach(const struct linux_binprm *bprm, + struct aa_ns *ns, struct list_head *list, const char *name, const char **info) { struct aa_profile *profile; rcu_read_lock(); - profile = aa_get_profile(__attach_match(name, list, info)); + profile = aa_get_profile(__attach_match(bprm, name, list, info)); rcu_read_unlock(); return profile ? &profile->label : NULL; @@ -432,6 +538,7 @@ struct aa_label *x_table_lookup(struct aa_profile *profile, u32 xindex, /** * x_to_label - get target label for a given xindex * @profile: current profile (NOT NULL) + * @bprm: binprm structure of transitioning task * @name: name to lookup (NOT NULL) * @xindex: index into x transition table * @lookupname: returns: name used in lookup if one was specified (NOT NULL) @@ -441,6 +548,7 @@ struct aa_label *x_table_lookup(struct aa_profile *profile, u32 xindex, * Returns: refcounted label or NULL if not found available */ static struct aa_label *x_to_label(struct aa_profile *profile, + const struct linux_binprm *bprm, const char *name, u32 xindex, const char **lookupname, const char **info) @@ -468,11 +576,11 @@ static struct aa_label *x_to_label(struct aa_profile *profile, case AA_X_NAME: if (xindex & AA_X_CHILD) /* released by caller */ - new = find_attach(ns, &profile->base.profiles, + new = find_attach(bprm, ns, &profile->base.profiles, name, info); else /* released by caller */ - new = find_attach(ns, &ns->base.profiles, + new = find_attach(bprm, ns, &ns->base.profiles, name, info); *lookupname = name; break; @@ -512,6 +620,8 @@ static struct aa_label *profile_transition(struct aa_profile *profile, bool *secure_exec) { struct aa_label *new = NULL; + struct aa_profile *component; + struct label_it i; const char *info = NULL, *name = NULL, *target = NULL; unsigned int state = profile->file.start; struct aa_perms perms = {}; @@ -536,8 +646,8 @@ static struct aa_label *profile_transition(struct aa_profile *profile, } if (profile_unconfined(profile)) { - new = find_attach(profile->ns, &profile->ns->base.profiles, - name, &info); + new = find_attach(bprm, profile->ns, + &profile->ns->base.profiles, name, &info); if (new) { AA_DEBUG("unconfined attached to new label"); return new; @@ -550,7 +660,8 @@ static struct aa_label *profile_transition(struct aa_profile *profile, state = aa_str_perms(profile->file.dfa, state, name, cond, &perms); if (perms.allow & MAY_EXEC) { /* exec permission determine how to transition */ - new = x_to_label(profile, name, perms.xindex, &target, &info); + new = x_to_label(profile, bprm, name, perms.xindex, &target, + &info); if (new && new->proxy == profile->label.proxy && info) { /* hack ix fallback - improve how this is detected */ goto audit; @@ -559,6 +670,21 @@ static struct aa_label *profile_transition(struct aa_profile *profile, info = "profile transition not found"; /* remove MAY_EXEC to audit as failure */ perms.allow &= ~MAY_EXEC; + } else { + /* verify that each component's xattr requirements are + * met, and fail execution otherwise + */ + label_for_each(i, new, component) { + if (aa_xattrs_match(bprm, component, state) < + 0) { + error = -EACCES; + info = "required xattrs not present"; + perms.allow &= ~MAY_EXEC; + aa_put_label(new); + new = NULL; + goto audit; + } + } } } else if (COMPLAIN_MODE(profile)) { /* no exec permission - learning mode */ @@ -592,22 +718,6 @@ static struct aa_label *profile_transition(struct aa_profile *profile, if (!new) goto audit; - /* Policy has specified a domain transitions. if no_new_privs and - * confined and not transitioning to the current domain fail. - * - * NOTE: Domain transitions from unconfined and to stritly stacked - * subsets are allowed even when no_new_privs is set because this - * aways results in a further reduction of permissions. - */ - if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) && - !profile_unconfined(profile) && - !aa_label_is_subset(new, &profile->label)) { - error = -EPERM; - info = "no new privs"; - nonewprivs = true; - perms.allow &= ~MAY_EXEC; - goto audit; - } if (!(perms.xindex & AA_X_UNSAFE)) { if (DEBUG_ON) { @@ -684,21 +794,6 @@ static int profile_onexec(struct aa_profile *profile, struct aa_label *onexec, perms.allow &= ~AA_MAY_ONEXEC; goto audit; } - /* Policy has specified a domain transitions. if no_new_privs and - * confined and not transitioning to the current domain fail. - * - * NOTE: Domain transitions from unconfined and to stritly stacked - * subsets are allowed even when no_new_privs is set because this - * aways results in a further reduction of permissions. - */ - if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) && - !profile_unconfined(profile) && - !aa_label_is_subset(onexec, &profile->label)) { - error = -EPERM; - info = "no new privs"; - perms.allow &= ~AA_MAY_ONEXEC; - goto audit; - } if (!(perms.xindex & AA_X_UNSAFE)) { if (DEBUG_ON) { @@ -794,10 +889,22 @@ int apparmor_bprm_set_creds(struct linux_binprm *bprm) if (bprm->called_set_creds) return 0; - ctx = cred_ctx(bprm->cred); + ctx = task_ctx(current); + AA_BUG(!cred_label(bprm->cred)); AA_BUG(!ctx); - label = aa_get_newest_label(ctx->label); + label = aa_get_newest_label(cred_label(bprm->cred)); + + /* + * Detect no new privs being set, and store the label it + * occurred under. Ideally this would happen when nnp + * is set but there isn't a good way to do that yet. + * + * Testing for unconfined must be done before the subset test + */ + if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) && !unconfined(label) && + !ctx->nnp) + ctx->nnp = aa_get_label(label); /* buffer freed below, name is pointer into buffer */ get_buffers(buffer); @@ -819,7 +926,20 @@ int apparmor_bprm_set_creds(struct linux_binprm *bprm) goto done; } - /* TODO: Add ns level no_new_privs subset test */ + /* Policy has specified a domain transitions. If no_new_privs and + * confined ensure the transition is to confinement that is subset + * of the confinement when the task entered no new privs. + * + * NOTE: Domain transitions from unconfined and to stacked + * subsets are allowed even when no_new_privs is set because this + * aways results in a further reduction of permissions. + */ + if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) && + !unconfined(label) && !aa_label_is_subset(new, ctx->nnp)) { + error = -EPERM; + info = "no new privs"; + goto audit; + } if (bprm->unsafe & LSM_UNSAFE_SHARE) { /* FIXME: currently don't mediate shared state */ @@ -853,14 +973,11 @@ int apparmor_bprm_set_creds(struct linux_binprm *bprm) } bprm->per_clear |= PER_CLEAR_ON_SETID; } - aa_put_label(ctx->label); - /* transfer reference, released when ctx is freed */ - ctx->label = new; + aa_put_label(cred_label(bprm->cred)); + /* transfer reference, released when cred is freed */ + cred_label(bprm->cred) = new; done: - /* clear out temporary/transitional state from the context */ - aa_clear_task_ctx_trans(ctx); - aa_put_label(label); put_buffers(buffer); @@ -1049,30 +1166,28 @@ build: int aa_change_hat(const char *hats[], int count, u64 token, int flags) { const struct cred *cred; - struct aa_task_ctx *ctx; + struct aa_task_ctx *ctx = task_ctx(current); struct aa_label *label, *previous, *new = NULL, *target = NULL; struct aa_profile *profile; struct aa_perms perms = {}; const char *info = NULL; int error = 0; - /* - * Fail explicitly requested domain transitions if no_new_privs. - * There is no exception for unconfined as change_hat is not - * available. - */ - if (task_no_new_privs(current)) { - /* not an apparmor denial per se, so don't log it */ - AA_DEBUG("no_new_privs - change_hat denied"); - return -EPERM; - } - /* released below */ cred = get_current_cred(); - ctx = cred_ctx(cred); label = aa_get_newest_cred_label(cred); previous = aa_get_newest_label(ctx->previous); + /* + * Detect no new privs being set, and store the label it + * occurred under. Ideally this would happen when nnp + * is set but there isn't a good way to do that yet. + * + * Testing for unconfined must be done before the subset test + */ + if (task_no_new_privs(current) && !unconfined(label) && !ctx->nnp) + ctx->nnp = aa_get_label(label); + if (unconfined(label)) { info = "unconfined can not change_hat"; error = -EPERM; @@ -1093,6 +1208,18 @@ int aa_change_hat(const char *hats[], int count, u64 token, int flags) if (error) goto fail; + /* + * no new privs prevents domain transitions that would + * reduce restrictions. + */ + if (task_no_new_privs(current) && !unconfined(label) && + !aa_label_is_subset(new, ctx->nnp)) { + /* not an apparmor denial per se, so don't log it */ + AA_DEBUG("no_new_privs - change_hat denied"); + error = -EPERM; + goto out; + } + if (flags & AA_CHANGE_TEST) goto out; @@ -1102,6 +1229,18 @@ int aa_change_hat(const char *hats[], int count, u64 token, int flags) /* kill task in case of brute force attacks */ goto kill; } else if (previous && !(flags & AA_CHANGE_TEST)) { + /* + * no new privs prevents domain transitions that would + * reduce restrictions. + */ + if (task_no_new_privs(current) && !unconfined(label) && + !aa_label_is_subset(previous, ctx->nnp)) { + /* not an apparmor denial per se, so don't log it */ + AA_DEBUG("no_new_privs - change_hat denied"); + error = -EPERM; + goto out; + } + /* Return to saved label. Kill task if restore fails * to avoid brute force attacks */ @@ -1144,21 +1283,6 @@ static int change_profile_perms_wrapper(const char *op, const char *name, const char *info = NULL; int error = 0; - /* - * Fail explicitly requested domain transitions when no_new_privs - * and not unconfined OR the transition results in a stack on - * the current label. - * Stacking domain transitions and transitions from unconfined are - * allowed even when no_new_privs is set because this aways results - * in a reduction of permissions. - */ - if (task_no_new_privs(current) && !stack && - !profile_unconfined(profile) && - !aa_label_is_subset(target, &profile->label)) { - info = "no new privs"; - error = -EPERM; - } - if (!error) error = change_profile_perms(profile, target, stack, request, profile->file.start, perms); @@ -1192,10 +1316,23 @@ int aa_change_profile(const char *fqname, int flags) const char *info = NULL; const char *auditname = fqname; /* retain leading & if stack */ bool stack = flags & AA_CHANGE_STACK; + struct aa_task_ctx *ctx = task_ctx(current); int error = 0; char *op; u32 request; + label = aa_get_current_label(); + + /* + * Detect no new privs being set, and store the label it + * occurred under. Ideally this would happen when nnp + * is set but there isn't a good way to do that yet. + * + * Testing for unconfined must be done before the subset test + */ + if (task_no_new_privs(current) && !unconfined(label) && !ctx->nnp) + ctx->nnp = aa_get_label(label); + if (!fqname || !*fqname) { AA_DEBUG("no profile name"); return -EINVAL; @@ -1283,14 +1420,28 @@ check: if (flags & AA_CHANGE_TEST) goto out; + /* stacking is always a subset, so only check the nonstack case */ + if (!stack) { + new = fn_label_build_in_ns(label, profile, GFP_KERNEL, + aa_get_label(target), + aa_get_label(&profile->label)); + /* + * no new privs prevents domain transitions that would + * reduce restrictions. + */ + if (task_no_new_privs(current) && !unconfined(label) && + !aa_label_is_subset(new, ctx->nnp)) { + /* not an apparmor denial per se, so don't log it */ + AA_DEBUG("no_new_privs - change_hat denied"); + error = -EPERM; + goto out; + } + } + if (!(flags & AA_CHANGE_ONEXEC)) { /* only transition profiles in the current ns */ if (stack) new = aa_label_merge(label, target, GFP_KERNEL); - else - new = fn_label_build_in_ns(label, profile, GFP_KERNEL, - aa_get_label(target), - aa_get_label(&profile->label)); if (IS_ERR_OR_NULL(new)) { info = "failed to build target label"; error = PTR_ERR(new); @@ -1299,9 +1450,15 @@ check: goto audit; } error = aa_replace_current_label(new); - } else + } else { + if (new) { + aa_put_label(new); + new = NULL; + } + /* full transition will be built in exec path */ error = aa_set_current_onexec(target, stack); + } audit: error = fn_for_each_in_ns(label, profile, |