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


Reply via email to