When policy is loaded into a policy namespace, its raw data is stored
in the kernel even if an identical blob is already loaded elsewhere.
This is common with LXD containers, which get their own policy ns and
then load the same policy as the host (or as sibling containers),
duplicating the raw data for every container.

Add a global rhltable of loaddata that own their payload, keyed by the
policy hash. On load, look up an identical blob, verifying the match
with a full compare, and if found free the new loaddata's payload and
hash and borrow the existing ones instead, holding a count reference
on the owner. Otherwise the new loaddata becomes an owner in the
table. Only the payload is shared; each ns keeps its own loaddata with
its own aafs entries, revision and rawdata_list membership.

Lookups are done under RCU without taking any ns locks, so loaddata
is now freed after an RCU grace period. Sharing is only done when
policy hashing is enabled and raw data is exported.

Also make aa_rawdata_eq() compare hashes only when both loaddata have
one, since hash_policy can be toggled at runtime, and short-circuit
when both share the same payload.

Signed-off-by: Georgia Garcia <[email protected]>
---
 security/apparmor/include/policy_unpack.h |  35 +++++
 security/apparmor/lsm.c                   |   5 +
 security/apparmor/policy.c                |   7 +
 security/apparmor/policy_unpack.c         | 153 +++++++++++++++++++++-
 4 files changed, 196 insertions(+), 4 deletions(-)

diff --git a/security/apparmor/include/policy_unpack.h 
b/security/apparmor/include/policy_unpack.h
index c01f6885dbe3..a1efc02237de 100644
--- a/security/apparmor/include/policy_unpack.h
+++ b/security/apparmor/include/policy_unpack.h
@@ -14,6 +14,8 @@
 #include <linux/list.h>
 #include <linux/kref.h>
 #include <linux/dcache.h>
+#include <linux/rcupdate.h>
+#include <linux/rhashtable-types.h>
 #include <linux/workqueue.h>
 
 #include "lib.h"
@@ -101,6 +103,15 @@ struct aa_ext {
  * @revision: unique revision count that this data was loaded as
  * @abi: the abi number the loaddata uses
  * @hash: a hash of the loaddata, used to help dedup data
+ * @shared: if non-NULL, @data and @hash are borrowed from this loaddata
+ *          (always the owning loaddata, never another borrower), which
+ *          is held via an i_loaddata (@count) reference. Used to share
+ *          identical raw policy across namespaces, e.g. a container's
+ *          ns loading the same policy as the host.
+ * @hnode: node in the global rawdata dedup table, keyed by @hash
+ * @hashed: whether @hnode is in the global rawdata dedup table
+ * @rcu: used to free the loaddata after an RCU grace period, as dedup
+ *       table lookups may still be comparing @hash
  *
  * There is no loaddata ref for being on ns->rawdata_list, so
  * @ns->lock must be held when walking the list. Dentries and
@@ -121,6 +132,10 @@ struct aa_loaddata {
        long revision;                  /* the ns policy revision this caused */
        int abi;
        unsigned char *hash;
+       struct aa_loaddata *shared;
+       struct rhlist_head hnode;
+       bool hashed;
+       struct rcu_head rcu;
 
        /* Pointer to payload. If @compressed_size > 0, then this is the
         * compressed version of the payload, else it is the uncompressed
@@ -149,6 +164,24 @@ aa_get_i_loaddata(struct aa_loaddata *data)
        return data;
 }
 
+/**
+ * aa_get_i_loaddata_not0 - get an inode/fs reference count if not zero
+ * @data: reference to get a count on
+ *
+ * Like aa_get_i_loaddata(), but safe to call on an entry whose last
+ * @count ref may already have been dropped.
+ *
+ * Returns: pointer to reference, or %NULL if @data is NULL or its
+ *          refcount has already reached zero.
+ */
+static inline struct aa_loaddata *
+aa_get_i_loaddata_not0(struct aa_loaddata *data)
+{
+       if (data && kref_get_unless_zero(&data->count.count))
+               return data;
+       return NULL;
+}
+
 
 /**
  * aa_get_profile_loaddata - get a profile reference count on loaddata
@@ -186,6 +219,8 @@ aa_get_profile_loaddata_not0(struct aa_loaddata *data)
 
 void __aa_loaddata_update(struct aa_loaddata *data, long revision);
 bool aa_rawdata_eq(struct aa_loaddata *l, struct aa_loaddata *r);
+void aa_loaddata_share(struct aa_loaddata *udata);
+int aa_rawdata_ht_init(void);
 void aa_loaddata_kref(struct kref *kref);
 void aa_ploaddata_kref(struct kref *kref);
 struct aa_loaddata *aa_loaddata_alloc(size_t size);
diff --git a/security/apparmor/lsm.c b/security/apparmor/lsm.c
index d502ad0ac26f..fdf19d0ce4c9 100644
--- a/security/apparmor/lsm.c
+++ b/security/apparmor/lsm.c
@@ -42,6 +42,7 @@
 #include "include/label.h"
 #include "include/policy.h"
 #include "include/policy_ns.h"
+#include "include/policy_unpack.h"
 #include "include/procattr.h"
 #include "include/mount.h"
 #include "include/secid.h"
@@ -2574,6 +2575,10 @@ static int __init apparmor_init(void)
                goto alloc_out;
        }
 
+       /* not fatal: cross-ns rawdata dedup is just disabled */
+       if (aa_rawdata_ht_init())
+               AA_ERROR("Unable to setup rawdata dedup table\n");
+
        error = aa_alloc_root_ns();
        if (error) {
                AA_ERROR("Unable to allocate default profile namespace\n");
diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c
index 8f23f99d2a58..15ac4a256354 100644
--- a/security/apparmor/policy.c
+++ b/security/apparmor/policy.c
@@ -1240,6 +1240,13 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, 
struct aa_label *label,
        } else
                ns = aa_get_ns(policy_ns ? policy_ns : labels_ns(label));
 
+       /*
+        * share raw data with an identical load in any ns (e.g. the host's
+        * policy loaded again inside a container). Requires policy hashing.
+        */
+       if (aa_g_export_binary)
+               aa_loaddata_share(udata);
+
        mutex_lock_nested(&ns->lock, ns->level);
        /* check for duplicate rawdata blobs: space and file dedup */
        if (!list_empty(&ns->rawdata_list)) {
diff --git a/security/apparmor/policy_unpack.c 
b/security/apparmor/policy_unpack.c
index 02e50bafa00a..b8d0f4d7a122 100644
--- a/security/apparmor/policy_unpack.c
+++ b/security/apparmor/policy_unpack.c
@@ -17,7 +17,10 @@
 #include <kunit/visibility.h>
 #include <linux/ctype.h>
 #include <linux/errno.h>
+#include <linux/jhash.h>
+#include <linux/rhashtable.h>
 #include <linux/zstd.h>
+#include <crypto/sha2.h>
 
 #include "include/apparmor.h"
 #include "include/audit.h"
@@ -105,19 +108,161 @@ bool aa_rawdata_eq(struct aa_loaddata *l, struct 
aa_loaddata *r)
                return false;
        if (l->compressed_size != r->compressed_size)
                return false;
-       if (aa_g_hash_policy && memcmp(l->hash, r->hash, aa_hash_size()) != 0)
+       if (l->hash && r->hash &&
+           memcmp(l->hash, r->hash, aa_hash_size()) != 0)
                return false;
+       /* shared payload, e.g. replacement after aa_loaddata_share() */
+       if (l->data == r->data)
+               return true;
        return memcmp(l->data, r->data, r->compressed_size ?: r->size) == 0;
 }
 
-static void do_loaddata_free(struct aa_loaddata *d)
+/*
+ * Global table of loaddata that own their payload, keyed by the policy
+ * hash, used to share identical raw policy between namespaces (e.g. a
+ * container's ns loading the same policy as the host, or sibling
+ * containers loading the same policy). Only populated when policy
+ * hashing is enabled. An rhltable is used as identical hashes can still
+ * have differing stored payloads (e.g. different compression levels).
+ */
+static struct rhltable aa_rawdata_ht;
+static bool aa_rawdata_ht_ready;
+
+static u32 rawdata_hashfn(const void *data, u32 len, u32 seed)
+{
+       return jhash(data, len, seed);
+}
+
+static u32 rawdata_obj_hashfn(const void *obj, u32 len, u32 seed)
+{
+       const struct aa_loaddata *d = obj;
+
+       return rawdata_hashfn(d->hash, len, seed);
+}
+
+static int rawdata_obj_cmpfn(struct rhashtable_compare_arg *arg,
+                            const void *obj)
+{
+       const struct aa_loaddata *d = obj;
+
+       return memcmp(arg->key, d->hash, arg->ht->p.key_len);
+}
+
+static const struct rhashtable_params rawdata_ht_params = {
+       .head_offset = offsetof(struct aa_loaddata, hnode),
+       .key_len = SHA256_DIGEST_SIZE,
+       .hashfn = rawdata_hashfn,
+       .obj_hashfn = rawdata_obj_hashfn,
+       .obj_cmpfn = rawdata_obj_cmpfn,
+       .automatic_shrinking = true,
+};
+
+int __init aa_rawdata_ht_init(void)
+{
+       int error = rhltable_init(&aa_rawdata_ht, &rawdata_ht_params);
+
+       if (!error)
+               aa_rawdata_ht_ready = true;
+       return error;
+}
+
+/**
+ * rawdata_ht_lookup - find an owner loaddata with the same payload as @udata
+ * @udata: loaddata to find a match for (NOT NULL, has a hash)
+ *
+ * Returns: owner loaddata with an i_loaddata (@count) reference held,
+ *          or %NULL if none was found
+ */
+static struct aa_loaddata *rawdata_ht_lookup(struct aa_loaddata *udata)
 {
-       kfree_sensitive(d->hash);
+       struct rhlist_head *list, *pos;
+       struct aa_loaddata *ent, *found = NULL;
+
+       rcu_read_lock();
+       list = rhltable_lookup(&aa_rawdata_ht, udata->hash, rawdata_ht_params);
+       rhl_for_each_entry_rcu(ent, pos, list, hnode) {
+               if (ent->size != udata->size ||
+                   ent->compressed_size != udata->compressed_size)
+                       continue;
+               /* entries may be mid-teardown; @data is only valid with a ref 
*/
+               if (!aa_get_i_loaddata_not0(ent))
+                       continue;
+               /* hash matched, make sure with a full compare */
+               if (aa_rawdata_eq(ent, udata)) {
+                       found = ent;
+                       break;
+               }
+               aa_put_i_loaddata(ent);
+       }
+       rcu_read_unlock();
+
+       return found;
+}
+
+/**
+ * aa_loaddata_share - share raw data with an identical load in any ns
+ * @udata: freshly unpacked loaddata (NOT NULL, not yet visible to others)
+ *
+ * If an identical rawdata payload is already loaded, free @udata's
+ * private copy of the payload and hash and borrow the existing one,
+ * otherwise make @udata available for future loads to share with.
+ *
+ * Only the payload is shared; @udata remains a distinct per-ns object
+ * with its own aafs entries, revision and list membership.
+ *
+ * Does nothing unless @udata was hashed (policy hashing enabled).
+ * Takes no ns locks.
+ */
+void aa_loaddata_share(struct aa_loaddata *udata)
+{
+       struct aa_loaddata *found;
+
+       AA_BUG(!udata);
+       AA_BUG(udata->shared);
+       AA_BUG(udata->hashed);
+
+       if (!aa_rawdata_ht_ready || !udata->hash)
+               return;
+
+       found = rawdata_ht_lookup(udata);
+       if (!found) {
+               /* become the owner others share with */
+               if (!rhltable_insert(&aa_rawdata_ht, &udata->hnode,
+                                    rawdata_ht_params))
+                       udata->hashed = true;
+               return;
+       }
+
+       kfree_sensitive(udata->hash);
+       kvfree(udata->data);
+       udata->hash = found->hash;
+       udata->data = found->data;
+       udata->shared = found;
+}
+
+static void do_loaddata_free_rcu(struct rcu_head *head)
+{
+       struct aa_loaddata *d = container_of(head, struct aa_loaddata, rcu);
+
+       if (!d->shared) {
+               kfree_sensitive(d->hash);
+               kvfree(d->data);
+       }
        kfree_sensitive(d->name);
-       kvfree(d->data);
        kfree_sensitive(d);
 }
 
+static void do_loaddata_free(struct aa_loaddata *d)
+{
+       if (d->hashed)
+               rhltable_remove(&aa_rawdata_ht, &d->hnode, rawdata_ht_params);
+       /* payload belongs to d->shared, which outlives this ref */
+       if (d->shared)
+               aa_put_i_loaddata(d->shared);
+       /* lookups may still be comparing d->hash */
+       call_rcu(&d->rcu, do_loaddata_free_rcu);
+}
+
 void aa_loaddata_kref(struct kref *kref)
 {
        struct aa_loaddata *d = container_of(kref, struct aa_loaddata,
-- 
2.43.0


Reply via email to