Unpack the optional "policyns" section the parser emits inside each profile (after rlimits) carrying a namespace's resource caps. unpack_policyns() mirrors sd_serialize_policyns().
Cap application is stage-then-commit: budgets apply to a tentative copy of the target ns's caps, the whole load set is admitted against that copy under ns->lock by aa_ns_admit_load_set(), and the caps are written back only at commit. A percentage cap on a children block resolves to that percent of the parent's own cap for the key. With the ns_quota sysctl off no caps are recorded, but the drop is audited. Constructs this kernel does not yet enforce - the descendants/root/:NAME: targets, subtree scope, and the criu/load_rate caps - are rejected with -EOPNOTSUPP rather than silently ignored, so policy using them fails to load until the follow-up series lands. Signed-off-by: Maxime Bélair <[email protected]> --- security/apparmor/include/policy_ns.h | 13 ++- security/apparmor/policy.c | 49 ++++++++++ security/apparmor/policy_ns.c | 128 +++++++++++++++++++++++--- security/apparmor/policy_unpack.c | 119 ++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 14 deletions(-) diff --git a/security/apparmor/include/policy_ns.h b/security/apparmor/include/policy_ns.h index c035ec2320b5..7ff28a550875 100644 --- a/security/apparmor/include/policy_ns.h +++ b/security/apparmor/include/policy_ns.h @@ -124,9 +124,16 @@ int aa_ns_admit_resident(struct aa_ns *ns, struct aa_ns_caps *limits, int aa_ns_admit_profile_size(struct aa_ns *ns, struct aa_ns_caps *limits, long bytes); int aa_ns_admit_count(struct aa_ns *ns, struct aa_ns_caps *limits, long delta); -/* apply one parsed "policyns limits" block to a (tentative) caps pair */ -void aa_ns_apply_budget(struct aa_ns_caps *limits, struct aa_ns_caps *child, - struct aa_ns_budget *budget); +/* whole replace-set admission, under ns->lock */ +struct aa_load_ent; +int aa_ns_admit_load_set(struct aa_ns *ns, struct list_head *lh, + struct aa_ns_caps *limits, struct aa_loaddata *udata, + struct aa_load_ent **fail_ent, const char **info); +/* apply one parsed "policyns limits" block to a (tentative) caps pair; + * returns -EOPNOTSUPP for a construct this kernel does not yet enforce + */ +int aa_ns_apply_budget(struct aa_ns_caps *limits, struct aa_ns_caps *child, + struct aa_ns_budget *budget); static inline struct aa_profile *aa_deref_parent(struct aa_profile *p) { diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c index 449d5ad1bcde..1b80f33aacd7 100644 --- a/security/apparmor/policy.c +++ b/security/apparmor/policy.c @@ -433,6 +433,10 @@ void aa_free_profile(struct aa_profile *profile) for (int i = 0; i < profile->n_rules; i++) free_ruleset(profile->label.rules[i]); + for (int i = 0; i < profile->n_budgets; i++) + kfree(profile->budgets[i].name); + kfree(profile->budgets); + kfree_sensitive(profile->dirname); if (profile->data) { @@ -1271,6 +1275,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, struct aa_ns *ns = NULL; struct aa_load_ent *ent, *tmp; struct aa_loaddata *rawdata_ent; + struct aa_ns_caps pend_limits, pend_child; const char *op; ssize_t count, error; LIST_HEAD(lh); @@ -1322,6 +1327,9 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, ns = aa_get_ns(policy_ns ? policy_ns : labels_ns(label)); mutex_lock_nested(&ns->lock, ns->level); + /* Tentative copies of the ns caps */ + pend_limits = ns->acct.limits; + pend_child = ns->acct.child; /* check for duplicate rawdata blobs: space and file dedup */ if (!list_empty(&ns->rawdata_list)) { list_for_each_entry(rawdata_ent, &ns->rawdata_list, list) { @@ -1404,6 +1412,44 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, rcu_assign_pointer(ent->new->parent, aa_get_profile(p)); } + /* + * Apply the load's "policyns limits" blocks to the tentative caps + * before admission. + */ + if (aa_g_policy_ns_quota) { + list_for_each_entry(ent, &lh, list) { + int b; + + for (b = 0; b < ent->new->n_budgets; b++) { + error = aa_ns_apply_budget(&pend_limits, + &pend_child, + &ent->new->budgets[b]); + if (error) { + info = "policyns limits: unsupported construct"; + goto fail_lock; + } + } + } + } else { + list_for_each_entry(ent, &lh, list) { + if (ent->new->n_budgets) { + audit_policy(label, op, ns_name, + ent->new->base.hname, + "policyns limits ignored: ns_quota disabled", + 0); + break; /* one record per load set */ + } + } + } + + /* + * Admission: check the whole load set against the tentative caps + * before installing anything, so a breach rejects the whole set. + */ + error = aa_ns_admit_load_set(ns, &lh, &pend_limits, udata, &ent, &info); + if (error) + goto fail_lock; + /* create new fs entries for introspection if needed */ if (!udata->dents[AAFS_LOADDATA_DIR] && aa_g_export_binary) { error = __aa_fs_create_rawdata(ns, udata); @@ -1432,6 +1478,9 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, } /* Done with checks that may fail - do actual replacement */ + /* commit the caps the load was admitted against */ + ns->acct.limits = pend_limits; + ns->acct.child = pend_child; __aa_bump_ns_revision(ns); if (aa_g_export_binary) __aa_loaddata_update(udata, ns->revision); diff --git a/security/apparmor/policy_ns.c b/security/apparmor/policy_ns.c index 48cf57637ffc..1b5d83c33c5f 100644 --- a/security/apparmor/policy_ns.c +++ b/security/apparmor/policy_ns.c @@ -191,6 +191,17 @@ static long cap_dec(long v) return v > 0 ? v - 1 : 0; } +/* @pct percent of @base; unlimited base stays unlimited, floored at 0 */ +static long cap_percent(long base, long pct) +{ + if (base == AA_NS_NOLIMIT) + return AA_NS_NOLIMIT; + if (pct <= 0) + return 0; + /* base and pct are both <= INT_MAX (parse-time), so u64 can't wrap */ + return (long)((u64)base * (u64)pct / 100); +} + void aa_ns_acct_init(struct aa_ns *ns) { struct aa_ns_acct *acct = &ns->acct; @@ -360,6 +371,85 @@ int aa_ns_admit_count(struct aa_ns *ns, struct aa_ns_caps *limits, long delta) &ns->acct.profile_count, delta, -EDQUOT); } +/** + * aa_ns_admit_load_set - admit a whole replace set against @ns's caps + * @ns: target namespace + * @lh: the load set, a list of struct aa_load_ent + * @limits: the tentative caps the set is admitted against + * @udata: the load's raw data (for the retained-rawdata memory term) + * @fail_ent: out - the profile that broke a per-profile cap, or NULL for a + * whole-set (memory/count) breach; only set when denying + * @info: out - audit cause string; only set when denying + * + * Sum the set's net resident bytes and profile count (new minus the dedup- + * skipped and replaced old), plus any newly retained rawdata, and check the + * per-profile, memory and count caps, so a breach rejects the set atomically + * before anything installs. Null profiles count for memory but not the count. + * + * Requires: @ns->lock held. + * + * Returns: 0 to admit the set, or a negative errno with *fail_ent and *info set. + */ +int aa_ns_admit_load_set(struct aa_ns *ns, struct list_head *lh, + struct aa_ns_caps *limits, struct aa_loaddata *udata, + struct aa_load_ent **fail_ent, const char **info) +{ + long new_bytes = 0, old_bytes = 0; + long new_count = 0, old_count = 0; + struct aa_load_ent *ent; + int error; + + if (!aa_g_policy_ns_quota) + return 0; + + list_for_each_entry(ent, lh, list) { + long bytes; + + if (ent->old && ent->new->rawdata && + ent->old->rawdata == ent->new->rawdata) + continue; /* dedup-skipped at install */ + + bytes = ent->new->resident_size; + if (!(ent->new->label.flags & FLAG_NULL)) { + error = aa_ns_admit_profile_size(ns, limits, bytes); + if (error) { + *fail_ent = ent; + *info = "profile exceeds max_profile cap"; + return error; + } + new_count++; + } + new_bytes += bytes; + if (ent->old) { + /* credit the exact bytes charged at @old's go-live, + * matching the eventual uncharge + */ + old_bytes += ent->old->acct_resident; + if (!(ent->old->label.flags & FLAG_NULL)) + old_count++; + } + } + /* a newly retained rawdata blob is charged too (same condition as + * the __aa_fs_create_rawdata call in aa_replace_profiles) + */ + if (!udata->dents[AAFS_LOADDATA_DIR] && aa_g_export_binary) + new_bytes += aa_loaddata_resident_size(udata); + + error = aa_ns_admit_resident(ns, limits, new_bytes - old_bytes); + if (error) { + *fail_ent = NULL; /* whole-set breach, not one profile */ + *info = "namespace memory cap exceeded"; + return error; + } + error = aa_ns_admit_count(ns, limits, new_count - old_count); + if (error) { + *fail_ent = NULL; + *info = "namespace profile cap exceeded"; + return error; + } + return 0; +} + /** * aa_ns_charge_profile - charge a profile's resident policy to its ns * @profile: the profile being made live (NOT NULL) @@ -431,15 +521,22 @@ void aa_ns_uncharge_profile(struct aa_profile *profile) * @tighten: cap_min() against the existing value (self) vs overwrite (children) */ static void apply_budget_keys(struct aa_ns_caps *dst, struct aa_ns_budget *b, - bool tighten) + bool tighten, const struct aa_ns_caps *pct_base) { long *cap = (long *)dst; int k; for (k = 0; k < AA_POLICYNS_KEY_MAX; k++) { - if (!(b->specified & (1u << k)) || (b->percent & (1u << k))) + long v; + + if (!(b->specified & (1u << k))) continue; - cap[k] = tighten ? cap_min(cap[k], b->values[k]) : b->values[k]; + if (b->percent & (1u << k)) + /* N% of the parent's own cap for key @k */ + v = cap_percent(((const long *)pct_base)[k], b->values[k]); + else + v = b->values[k]; + cap[k] = tighten ? cap_min(cap[k], v) : v; } } @@ -449,19 +546,28 @@ static void apply_budget_keys(struct aa_ns_caps *dst, struct aa_ns_budget *b, * @child: the (tentative) children template of that namespace * @b: one parsed budget block */ -void aa_ns_apply_budget(struct aa_ns_caps *limits, struct aa_ns_caps *child, - struct aa_ns_budget *b) +int aa_ns_apply_budget(struct aa_ns_caps *limits, struct aa_ns_caps *child, + struct aa_ns_budget *b) { + /* Some features remains to be implemented and are rejected with -EOPNOTSUPP. */ + if (b->scope == AA_POLICYNS_SCOPE_SUBTREE) + return -EOPNOTSUPP; + if (b->specified & ((1u << AA_POLICYNS_KEY_CRIU) | + (1u << AA_POLICYNS_KEY_LOAD_RATE))) + return -EOPNOTSUPP; + switch (b->target) { case AA_POLICYNS_TGT_SELF: - apply_budget_keys(limits, b, true); - break; + if (b->percent) /* % is a per-child ratio only */ + return -EOPNOTSUPP; + apply_budget_keys(limits, b, true, NULL); + return 0; case AA_POLICYNS_TGT_CHILDREN: aa_ns_caps_init_unset(child); - apply_budget_keys(child, b, false); - break; - default: - break; + apply_budget_keys(child, b, false, limits); + return 0; + default: /* descendants/root/:NAME: not yet enforced */ + return -EOPNOTSUPP; } } diff --git a/security/apparmor/policy_unpack.c b/security/apparmor/policy_unpack.c index 47360d4c5eb1..67df8167d32e 100644 --- a/security/apparmor/policy_unpack.c +++ b/security/apparmor/policy_unpack.c @@ -618,6 +618,118 @@ static bool unpack_xattrs(struct aa_ext *e, struct aa_profile *profile) return false; } +/* + * unpack_policyns_block - unpack one "policyns limits" block into @b + * + * Wire format, mirroring the parser's sd_serialize_policyns() + * + * policyns := AA_STRUCT "policyns" + * u32 target ; AA_POLICYNS_TGT_* + * u32 scope ; AA_POLICYNS_SCOPE_* + * u32 specified ; bitmask of keys present (bit k == key k) + * u32 percent ; bitmask of keys whose value is a percentage + * AA_ARRAY[AA_POLICYNS_KEY_MAX] of u64 ; cap values by key + * [ AA_STRING "name" ] ; only for the :NAME: target + * AA_STRUCTEND + * + * Returns: 1 if a block was consumed, 0 if none is present, or a negative + * errno on malformed input. + */ +static int unpack_policyns_block(struct aa_ext *e, struct aa_ns_budget *b) +{ + void *pos = e->pos; + char *name = NULL; + u16 size; + int k; + + if (!aa_unpack_nameX(e, AA_STRUCT, "policyns")) + return 0; /* no (more) policyns blocks */ + + memset(b, 0, sizeof(*b)); + if (!aa_unpack_u32(e, &b->target, NULL) || + !aa_unpack_u32(e, &b->scope, NULL) || + !aa_unpack_u32(e, &b->specified, NULL) || + !aa_unpack_u32(e, &b->percent, NULL)) + goto fail; + /* out-of-range target/scope (the parser never emits it) */ + if (b->target > AA_POLICYNS_TGT_NAME || + b->scope > AA_POLICYNS_SCOPE_SUBTREE) + goto fail; + + if (!aa_unpack_array(e, NULL, &size) || size != AA_POLICYNS_KEY_MAX) + goto fail; + for (k = 0; k < AA_POLICYNS_KEY_MAX; k++) { + u64 v; + + if (!aa_unpack_u64(e, &v, NULL)) + goto fail; + if (v > INT_MAX) /* parser bounds caps at INT_MAX */ + goto fail; /* out of range */ + b->values[k] = (long)v; + } + if (!aa_unpack_nameX(e, AA_ARRAYEND, NULL)) + goto fail; + + /* the literal ns name is present only for the :NAME: target */ + if (b->target == AA_POLICYNS_TGT_NAME) { + if (!aa_unpack_strdup(e, &name, "name")) + goto fail; + b->name = name; + } + + if (!aa_unpack_nameX(e, AA_STRUCTEND, NULL)) + goto fail; + + /* reject specified/percent bits outside the known key range */ + if ((b->specified | b->percent) & ~((1u << AA_POLICYNS_KEY_MAX) - 1)) + goto fail; + + return 1; + +fail: + kfree(name); + b->name = NULL; + e->pos = pos; + return -EPROTO; +} + +/* + * unpack_policyns - collect a profile's "policyns limits" blocks onto it + * + * Applied to the target ns at load time (aa_replace_profiles), under ns->lock. + * Returns 0 (with 0+ blocks recorded) or a negative errno. + */ +static int unpack_policyns(struct aa_ext *e, struct aa_profile *profile) +{ + struct aa_ns_budget *arr = NULL, blk; + int n = 0, i, ret; + + while ((ret = unpack_policyns_block(e, &blk)) > 0) { + struct aa_ns_budget *grown; + + grown = krealloc_array(arr, n + 1, sizeof(*arr), GFP_KERNEL); + if (!grown) { + ret = -ENOMEM; + goto fail; + } + arr = grown; + arr[n++] = blk; /* transfers blk.name ownership to @arr */ + } + if (ret < 0) + goto fail; + + profile->budgets = arr; + profile->n_budgets = n; + return 0; + +fail: + kfree(blk.name); /* unstored block on the -ENOMEM path */ + for (i = 0; i < n; i++) + kfree(arr[i].name); + kfree(arr); + return ret; +} + static bool unpack_secmark(struct aa_ext *e, struct aa_ruleset *rules) { void *pos = e->pos; @@ -1291,6 +1403,13 @@ static struct aa_profile *unpack_profile(struct aa_ext *e, char **ns_name) goto fail; } + /* optional policyns limits blocks, emitted by the parser after rlimits */ + error = unpack_policyns(e, profile); + if (error) { + info = "failed to unpack policyns limits"; + goto fail; + } + if (!unpack_secmark(e, rules)) { info = "failed to unpack profile secmark rules"; goto fail; -- 2.51.0
