--- /dev/null
+From 0b5538ee638c650d9fefada5aa335a5682ca1c48 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Fri, 17 Oct 2025 01:53:00 -0700
+Subject: apparmor: fix differential encoding verification
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 39440b137546a3aa383cfdabc605fb73811b6093 upstream.
+
+Differential encoding allows loops to be created if it is abused. To
+prevent this the unpack should verify that a diff-encode chain
+terminates.
+
+Unfortunately the differential encode verification had two bugs.
+
+1. it conflated states that had gone through check and already been
+ marked, with states that were currently being checked and marked.
+ This means that loops in the current chain being verified are treated
+ as a chain that has already been verified.
+
+2. the order bailout on already checked states compared current chain
+ check iterators j,k instead of using the outer loop iterator i.
+ Meaning a step backwards in states in the current chain verification
+ was being mistaken for moving to an already verified state.
+
+Move to a double mark scheme where already verified states get a
+different mark, than the current chain being kept. This enables us
+to also drop the backwards verification check that was the cause of
+the second error as any already verified state is already marked.
+
+Fixes: 031dcc8f4e84 ("apparmor: dfa add support for state differential encoding")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/include/match.h | 1 +
+ security/apparmor/match.c | 23 +++++++++++++++++++----
+ 2 files changed, 20 insertions(+), 4 deletions(-)
+
+--- a/security/apparmor/include/match.h
++++ b/security/apparmor/include/match.h
+@@ -181,6 +181,7 @@ static inline void aa_put_dfa(struct aa_
+ #define MATCH_FLAG_DIFF_ENCODE 0x80000000
+ #define MARK_DIFF_ENCODE 0x40000000
+ #define MATCH_FLAG_OOB_TRANSITION 0x20000000
++#define MARK_DIFF_ENCODE_VERIFIED 0x10000000
+ #define MATCH_FLAGS_MASK 0xff000000
+ #define MATCH_FLAGS_VALID (MATCH_FLAG_DIFF_ENCODE | MATCH_FLAG_OOB_TRANSITION)
+ #define MATCH_FLAGS_INVALID (MATCH_FLAGS_MASK & ~MATCH_FLAGS_VALID)
+--- a/security/apparmor/match.c
++++ b/security/apparmor/match.c
+@@ -202,16 +202,31 @@ static int verify_dfa(struct aa_dfa *dfa
+ size_t j, k;
+
+ for (j = i;
+- (BASE_TABLE(dfa)[j] & MATCH_FLAG_DIFF_ENCODE) &&
+- !(BASE_TABLE(dfa)[j] & MARK_DIFF_ENCODE);
++ ((BASE_TABLE(dfa)[j] & MATCH_FLAG_DIFF_ENCODE) &&
++ !(BASE_TABLE(dfa)[j] & MARK_DIFF_ENCODE_VERIFIED));
+ j = k) {
++ if (BASE_TABLE(dfa)[j] & MARK_DIFF_ENCODE)
++ /* loop in current chain */
++ goto out;
+ k = DEFAULT_TABLE(dfa)[j];
+ if (j == k)
++ /* self loop */
+ goto out;
+- if (k < j)
+- break; /* already verified */
+ BASE_TABLE(dfa)[j] |= MARK_DIFF_ENCODE;
+ }
++ /* move mark to verified */
++ for (j = i;
++ (BASE_TABLE(dfa)[j] & MATCH_FLAG_DIFF_ENCODE);
++ j = k) {
++ k = DEFAULT_TABLE(dfa)[j];
++ if (j < i)
++ /* jumps to state/chain that has been
++ * verified
++ */
++ break;
++ BASE_TABLE(dfa)[j] &= ~MARK_DIFF_ENCODE;
++ BASE_TABLE(dfa)[j] |= MARK_DIFF_ENCODE_VERIFIED;
++ }
+ }
+ error = 0;
+
--- /dev/null
+From acc4c331e4881c32364ac99b16715e6342adf611 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Wed, 10 Sep 2025 06:22:17 -0700
+Subject: apparmor: Fix double free of ns_name in aa_replace_profiles()
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 5df0c44e8f5f619d3beb871207aded7c78414502 upstream.
+
+if ns_name is NULL after
+1071 error = aa_unpack(udata, &lh, &ns_name);
+
+and if ent->ns_name contains an ns_name in
+1089 } else if (ent->ns_name) {
+
+then ns_name is assigned the ent->ns_name
+1095 ns_name = ent->ns_name;
+
+however ent->ns_name is freed at
+1262 aa_load_ent_free(ent);
+
+and then again when freeing ns_name at
+1270 kfree(ns_name);
+
+Fix this by NULLing out ent->ns_name after it is transferred to ns_name
+
+Fixes: 145a0ef21c8e9 ("apparmor: fix blob compression when ns is forced on a policy load")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -1115,6 +1115,7 @@ ssize_t aa_replace_profiles(struct aa_ns
+ goto fail;
+ }
+ ns_name = ent->ns_name;
++ ent->ns_name = NULL;
+ } else
+ count++;
+ }
--- /dev/null
+From 30fd33360c1a9dfeb9d0220d2f832416f5b439fc Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Tue, 3 Mar 2026 11:08:02 -0800
+Subject: apparmor: fix: limit the number of levels of policy namespaces
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 306039414932c80f8420695a24d4fe10c84ccfb2 upstream.
+
+Currently the number of policy namespaces is not bounded relying on
+the user namespace limit. However policy namespaces aren't strictly
+tied to user namespaces and it is possible to create them and nest
+them arbitrarily deep which can be used to exhaust system resource.
+
+Hard cap policy namespaces to the same depth as user namespaces.
+
+Fixes: c88d4c7b049e8 ("AppArmor: core policy routines")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Reviewed-by: Ryan Lee <ryan.lee@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/include/policy_ns.h | 2 ++
+ security/apparmor/policy_ns.c | 2 ++
+ 2 files changed, 4 insertions(+)
+
+--- a/security/apparmor/include/policy_ns.h
++++ b/security/apparmor/include/policy_ns.h
+@@ -18,6 +18,8 @@
+ #include "label.h"
+ #include "policy.h"
+
++/* Match max depth of user namespaces */
++#define MAX_NS_DEPTH 32
+
+ /* struct aa_ns_acct - accounting of profiles in namespace
+ * @max_size: maximum space allowed for all profiles in namespace
+--- a/security/apparmor/policy_ns.c
++++ b/security/apparmor/policy_ns.c
+@@ -260,6 +260,8 @@ static struct aa_ns *__aa_create_ns(stru
+ AA_BUG(!name);
+ AA_BUG(!mutex_is_locked(&parent->lock));
+
++ if (parent->level > MAX_NS_DEPTH)
++ return ERR_PTR(-ENOSPC);
+ ns = alloc_ns(parent->base.hname, name);
+ if (!ns)
+ return ERR_PTR(-ENOMEM);
--- /dev/null
+From f29ec10efe7f164c8ab54d82ab4a8d696be95a85 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Tue, 20 Jan 2026 15:24:04 +0100
+Subject: apparmor: fix memory leak in verify_header
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit e38c55d9f834e5b848bfed0f5c586aaf45acb825 upstream.
+
+The function sets `*ns = NULL` on every call, leaking the namespace
+string allocated in previous iterations when multiple profiles are
+unpacked. This also breaks namespace consistency checking since *ns
+is always NULL when the comparison is made.
+
+Remove the incorrect assignment.
+The caller (aa_unpack) initializes *ns to NULL once before the loop,
+which is sufficient.
+
+Fixes: dd51c8485763 ("apparmor: provide base for multiple profiles to be replaced at once")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy_unpack.c | 1 -
+ 1 file changed, 1 deletion(-)
+
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -1132,7 +1132,6 @@ static int verify_header(struct aa_ext *
+ {
+ int error = -EPROTONOSUPPORT;
+ const char *name = NULL;
+- *ns = NULL;
+
+ /* get the interface version */
+ if (!aa_unpack_u32(e, &e->version, "version")) {
--- /dev/null
+From 7be9ea7488a2e246abe5fead007f9014f8addb69 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 29 Jan 2026 16:51:11 +0100
+Subject: apparmor: fix missing bounds check on DEFAULT table in verify_dfa()
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit d352873bbefa7eb39995239d0b44ccdf8aaa79a4 upstream.
+
+The verify_dfa() function only checks DEFAULT_TABLE bounds when the state
+is not differentially encoded.
+
+When the verification loop traverses the differential encoding chain,
+it reads k = DEFAULT_TABLE[j] and uses k as an array index without
+validation. A malformed DFA with DEFAULT_TABLE[j] >= state_count,
+therefore, causes both out-of-bounds reads and writes.
+
+[ 57.179855] ==================================================================
+[ 57.180549] BUG: KASAN: slab-out-of-bounds in verify_dfa+0x59a/0x660
+[ 57.180904] Read of size 4 at addr ffff888100eadec4 by task su/993
+
+[ 57.181554] CPU: 1 UID: 0 PID: 993 Comm: su Not tainted 6.19.0-rc7-next-20260127 #1 PREEMPT(lazy)
+[ 57.181558] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
+[ 57.181563] Call Trace:
+[ 57.181572] <TASK>
+[ 57.181577] dump_stack_lvl+0x5e/0x80
+[ 57.181596] print_report+0xc8/0x270
+[ 57.181605] ? verify_dfa+0x59a/0x660
+[ 57.181608] kasan_report+0x118/0x150
+[ 57.181620] ? verify_dfa+0x59a/0x660
+[ 57.181623] verify_dfa+0x59a/0x660
+[ 57.181627] aa_dfa_unpack+0x1610/0x1740
+[ 57.181629] ? __kmalloc_cache_noprof+0x1d0/0x470
+[ 57.181640] unpack_pdb+0x86d/0x46b0
+[ 57.181647] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 57.181653] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 57.181656] ? aa_unpack_nameX+0x1a8/0x300
+[ 57.181659] aa_unpack+0x20b0/0x4c30
+[ 57.181662] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 57.181664] ? stack_depot_save_flags+0x33/0x700
+[ 57.181681] ? kasan_save_track+0x4f/0x80
+[ 57.181683] ? kasan_save_track+0x3e/0x80
+[ 57.181686] ? __kasan_kmalloc+0x93/0xb0
+[ 57.181688] ? __kvmalloc_node_noprof+0x44a/0x780
+[ 57.181693] ? aa_simple_write_to_buffer+0x54/0x130
+[ 57.181697] ? policy_update+0x154/0x330
+[ 57.181704] aa_replace_profiles+0x15a/0x1dd0
+[ 57.181707] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 57.181710] ? __kvmalloc_node_noprof+0x44a/0x780
+[ 57.181712] ? aa_loaddata_alloc+0x77/0x140
+[ 57.181715] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 57.181717] ? _copy_from_user+0x2a/0x70
+[ 57.181730] policy_update+0x17a/0x330
+[ 57.181733] profile_replace+0x153/0x1a0
+[ 57.181735] ? rw_verify_area+0x93/0x2d0
+[ 57.181740] vfs_write+0x235/0xab0
+[ 57.181745] ksys_write+0xb0/0x170
+[ 57.181748] do_syscall_64+0x8e/0x660
+[ 57.181762] entry_SYSCALL_64_after_hwframe+0x76/0x7e
+[ 57.181765] RIP: 0033:0x7f6192792eb2
+
+Remove the MATCH_FLAG_DIFF_ENCODE condition to validate all DEFAULT_TABLE
+entries unconditionally.
+
+Fixes: 031dcc8f4e84 ("apparmor: dfa add support for state differential encoding")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/match.c | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+--- a/security/apparmor/match.c
++++ b/security/apparmor/match.c
+@@ -160,9 +160,10 @@ static int verify_dfa(struct aa_dfa *dfa
+ if (state_count == 0)
+ goto out;
+ for (i = 0; i < state_count; i++) {
+- if (!(BASE_TABLE(dfa)[i] & MATCH_FLAG_DIFF_ENCODE) &&
+- (DEFAULT_TABLE(dfa)[i] >= state_count))
++ if (DEFAULT_TABLE(dfa)[i] >= state_count) {
++ pr_err("AppArmor DFA default state out of bounds");
+ goto out;
++ }
+ if (BASE_TABLE(dfa)[i] & MATCH_FLAGS_INVALID) {
+ pr_err("AppArmor DFA state with invalid match flags");
+ goto out;
--- /dev/null
+From 33c685fe4eda0033ee9a60fa69b5dd50be3c3459 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Sun, 1 Mar 2026 16:10:51 -0800
+Subject: apparmor: fix race between freeing data and fs accessing it
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 8e135b8aee5a06c52a4347a5a6d51223c6f36ba3 upstream.
+
+AppArmor was putting the reference to i_private data on its end after
+removing the original entry from the file system. However the inode
+can and does live beyond that point and it is possible that some of
+the fs call back functions will be invoked after the reference has
+been put, which results in a race between freeing the data and
+accessing it through the fs.
+
+While the rawdata/loaddata is the most likely candidate to fail the
+race, as it has the fewest references. If properly crafted it might be
+possible to trigger a race for the other types stored in i_private.
+
+Fix this by moving the put of i_private referenced data to the correct
+place which is during inode eviction.
+
+Fixes: c961ee5f21b20 ("apparmor: convert from securityfs to apparmorfs for policy ns files")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Maxime Bélair <maxime.belair@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c | 194 +++++++++++++++++-------------
+ security/apparmor/include/label.h | 16 +-
+ security/apparmor/include/lib.h | 12 +
+ security/apparmor/include/policy.h | 8 -
+ security/apparmor/include/policy_unpack.h | 6
+ security/apparmor/label.c | 12 +
+ security/apparmor/policy_unpack.c | 6
+ 7 files changed, 153 insertions(+), 101 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -32,6 +32,7 @@
+ #include "include/crypto.h"
+ #include "include/ipc.h"
+ #include "include/label.h"
++#include "include/lib.h"
+ #include "include/policy.h"
+ #include "include/policy_ns.h"
+ #include "include/resource.h"
+@@ -62,6 +63,7 @@
+ * securityfs and apparmorfs filesystems.
+ */
+
++#define IREF_POISON 101
+
+ /*
+ * support fns
+@@ -153,6 +155,71 @@ static int aafs_show_path(struct seq_fil
+ return 0;
+ }
+
++static struct aa_ns *get_ns_common_ref(struct aa_common_ref *ref)
++{
++ if (ref) {
++ struct aa_label *reflabel = container_of(ref, struct aa_label,
++ count);
++ return aa_get_ns(labels_ns(reflabel));
++ }
++
++ return NULL;
++}
++
++static struct aa_proxy *get_proxy_common_ref(struct aa_common_ref *ref)
++{
++ if (ref)
++ return aa_get_proxy(container_of(ref, struct aa_proxy, count));
++
++ return NULL;
++}
++
++static struct aa_loaddata *get_loaddata_common_ref(struct aa_common_ref *ref)
++{
++ if (ref)
++ return aa_get_i_loaddata(container_of(ref, struct aa_loaddata,
++ count));
++ return NULL;
++}
++
++static void aa_put_common_ref(struct aa_common_ref *ref)
++{
++ if (!ref)
++ return;
++
++ switch (ref->reftype) {
++ case REF_RAWDATA:
++ aa_put_i_loaddata(container_of(ref, struct aa_loaddata,
++ count));
++ break;
++ case REF_PROXY:
++ aa_put_proxy(container_of(ref, struct aa_proxy,
++ count));
++ break;
++ case REF_NS:
++ /* ns count is held on its unconfined label */
++ aa_put_ns(labels_ns(container_of(ref, struct aa_label, count)));
++ break;
++ default:
++ AA_BUG(true, "unknown refcount type");
++ break;
++ }
++}
++
++static void aa_get_common_ref(struct aa_common_ref *ref)
++{
++ kref_get(&ref->count);
++}
++
++static void aafs_evict(struct inode *inode)
++{
++ struct aa_common_ref *ref = inode->i_private;
++
++ clear_inode(inode);
++ aa_put_common_ref(ref);
++ inode->i_private = (void *) IREF_POISON;
++}
++
+ static void aafs_free_inode(struct inode *inode)
+ {
+ if (S_ISLNK(inode->i_mode))
+@@ -162,6 +229,7 @@ static void aafs_free_inode(struct inode
+
+ static const struct super_operations aafs_super_ops = {
+ .statfs = simple_statfs,
++ .evict_inode = aafs_evict,
+ .free_inode = aafs_free_inode,
+ .show_path = aafs_show_path,
+ };
+@@ -262,7 +330,8 @@ static int __aafs_setup_d_inode(struct i
+ * aafs_remove(). Will return ERR_PTR on failure.
+ */
+ static struct dentry *aafs_create(const char *name, umode_t mode,
+- struct dentry *parent, void *data, void *link,
++ struct dentry *parent,
++ struct aa_common_ref *data, void *link,
+ const struct file_operations *fops,
+ const struct inode_operations *iops)
+ {
+@@ -299,6 +368,9 @@ static struct dentry *aafs_create(const
+ goto fail_dentry;
+ inode_unlock(dir);
+
++ if (data)
++ aa_get_common_ref(data);
++
+ return dentry;
+
+ fail_dentry:
+@@ -323,7 +395,8 @@ fail_lock:
+ * see aafs_create
+ */
+ static struct dentry *aafs_create_file(const char *name, umode_t mode,
+- struct dentry *parent, void *data,
++ struct dentry *parent,
++ struct aa_common_ref *data,
+ const struct file_operations *fops)
+ {
+ return aafs_create(name, mode, parent, data, NULL, fops, NULL);
+@@ -448,7 +521,7 @@ end_section:
+ static ssize_t profile_load(struct file *f, const char __user *buf, size_t size,
+ loff_t *pos)
+ {
+- struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++ struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+ int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns,
+ f->f_cred);
+
+@@ -466,7 +539,7 @@ static const struct file_operations aa_f
+ static ssize_t profile_replace(struct file *f, const char __user *buf,
+ size_t size, loff_t *pos)
+ {
+- struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++ struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+ int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY,
+ buf, size, pos, ns, f->f_cred);
+ aa_put_ns(ns);
+@@ -486,7 +559,7 @@ static ssize_t profile_remove(struct fil
+ struct aa_loaddata *data;
+ struct aa_label *label;
+ ssize_t error;
+- struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++ struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+
+ label = begin_current_label_crit_section();
+ /* high level check about policy management - fine grained in
+@@ -576,7 +649,7 @@ static int ns_revision_open(struct inode
+ if (!rev)
+ return -ENOMEM;
+
+- rev->ns = aa_get_ns(inode->i_private);
++ rev->ns = get_ns_common_ref(inode->i_private);
+ if (!rev->ns)
+ rev->ns = aa_get_current_ns();
+ file->private_data = rev;
+@@ -1054,7 +1127,7 @@ static const struct file_operations seq_
+ static int seq_profile_open(struct inode *inode, struct file *file,
+ int (*show)(struct seq_file *, void *))
+ {
+- struct aa_proxy *proxy = aa_get_proxy(inode->i_private);
++ struct aa_proxy *proxy = get_proxy_common_ref(inode->i_private);
+ int error = single_open(file, show, proxy);
+
+ if (error) {
+@@ -1246,7 +1319,7 @@ static const struct file_operations seq_
+ static int seq_rawdata_open(struct inode *inode, struct file *file,
+ int (*show)(struct seq_file *, void *))
+ {
+- struct aa_loaddata *data = aa_get_i_loaddata(inode->i_private);
++ struct aa_loaddata *data = get_loaddata_common_ref(inode->i_private);
+ int error;
+
+ if (!data)
+@@ -1381,7 +1454,7 @@ static int rawdata_open(struct inode *in
+ if (!aa_current_policy_view_capable(NULL))
+ return -EACCES;
+
+- loaddata = aa_get_i_loaddata(inode->i_private);
++ loaddata = get_loaddata_common_ref(inode->i_private);
+ if (!loaddata)
+ return -ENOENT;
+
+@@ -1426,7 +1499,6 @@ static void remove_rawdata_dents(struct
+ if (!IS_ERR_OR_NULL(rawdata->dents[i])) {
+ aafs_remove(rawdata->dents[i]);
+ rawdata->dents[i] = NULL;
+- aa_put_i_loaddata(rawdata);
+ }
+ }
+ }
+@@ -1465,45 +1537,41 @@ int __aa_fs_create_rawdata(struct aa_ns
+ if (IS_ERR(dir))
+ /* ->name freed when rawdata freed */
+ return PTR_ERR(dir);
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_DIR] = dir;
+
+- dent = aafs_create_file("abi", S_IFREG | 0444, dir, rawdata,
++ dent = aafs_create_file("abi", S_IFREG | 0444, dir, &rawdata->count,
+ &seq_rawdata_abi_fops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_ABI] = dent;
+
+- dent = aafs_create_file("revision", S_IFREG | 0444, dir, rawdata,
+- &seq_rawdata_revision_fops);
++ dent = aafs_create_file("revision", S_IFREG | 0444, dir,
++ &rawdata->count,
++ &seq_rawdata_revision_fops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_REVISION] = dent;
+
+ if (aa_g_hash_policy) {
+ dent = aafs_create_file("sha1", S_IFREG | 0444, dir,
+- rawdata, &seq_rawdata_hash_fops);
++ &rawdata->count,
++ &seq_rawdata_hash_fops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_HASH] = dent;
+ }
+
+ dent = aafs_create_file("compressed_size", S_IFREG | 0444, dir,
+- rawdata,
++ &rawdata->count,
+ &seq_rawdata_compressed_size_fops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_COMPRESSED_SIZE] = dent;
+
+- dent = aafs_create_file("raw_data", S_IFREG | 0444,
+- dir, rawdata, &rawdata_fops);
++ dent = aafs_create_file("raw_data", S_IFREG | 0444, dir,
++ &rawdata->count, &rawdata_fops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_DATA] = dent;
+ d_inode(dent)->i_size = rawdata->size;
+
+@@ -1514,7 +1582,6 @@ int __aa_fs_create_rawdata(struct aa_ns
+
+ fail:
+ remove_rawdata_dents(rawdata);
+- aa_put_i_loaddata(rawdata);
+ return PTR_ERR(dent);
+ }
+ #endif /* CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
+@@ -1538,13 +1605,10 @@ void __aafs_profile_rmdir(struct aa_prof
+ __aafs_profile_rmdir(child);
+
+ for (i = AAFS_PROF_SIZEOF - 1; i >= 0; --i) {
+- struct aa_proxy *proxy;
+ if (!profile->dents[i])
+ continue;
+
+- proxy = d_inode(profile->dents[i])->i_private;
+ aafs_remove(profile->dents[i]);
+- aa_put_proxy(proxy);
+ profile->dents[i] = NULL;
+ }
+ }
+@@ -1577,14 +1641,7 @@ static struct dentry *create_profile_fil
+ struct aa_profile *profile,
+ const struct file_operations *fops)
+ {
+- struct aa_proxy *proxy = aa_get_proxy(profile->label.proxy);
+- struct dentry *dent;
+-
+- dent = aafs_create_file(name, S_IFREG | 0444, dir, proxy, fops);
+- if (IS_ERR(dent))
+- aa_put_proxy(proxy);
+-
+- return dent;
++ return aafs_create_file(name, S_IFREG | 0444, dir, &profile->label.proxy->count, fops);
+ }
+
+ #ifdef CONFIG_SECURITY_APPARMOR_EXPORT_BINARY
+@@ -1635,7 +1692,8 @@ static const char *rawdata_get_link_base
+ struct delayed_call *done,
+ const char *name)
+ {
+- struct aa_proxy *proxy = inode->i_private;
++ struct aa_common_ref *ref = inode->i_private;
++ struct aa_proxy *proxy = container_of(ref, struct aa_proxy, count);
+ struct aa_label *label;
+ struct aa_profile *profile;
+ char *target;
+@@ -1777,27 +1835,24 @@ int __aafs_profile_mkdir(struct aa_profi
+ if (profile->rawdata) {
+ if (aa_g_hash_policy) {
+ dent = aafs_create("raw_sha1", S_IFLNK | 0444, dir,
+- profile->label.proxy, NULL, NULL,
+- &rawdata_link_sha1_iops);
++ &profile->label.proxy->count, NULL,
++ NULL, &rawdata_link_sha1_iops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_proxy(profile->label.proxy);
+ profile->dents[AAFS_PROF_RAW_HASH] = dent;
+ }
+ dent = aafs_create("raw_abi", S_IFLNK | 0444, dir,
+- profile->label.proxy, NULL, NULL,
++ &profile->label.proxy->count, NULL, NULL,
+ &rawdata_link_abi_iops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_proxy(profile->label.proxy);
+ profile->dents[AAFS_PROF_RAW_ABI] = dent;
+
+ dent = aafs_create("raw_data", S_IFLNK | 0444, dir,
+- profile->label.proxy, NULL, NULL,
++ &profile->label.proxy->count, NULL, NULL,
+ &rawdata_link_data_iops);
+ if (IS_ERR(dent))
+ goto fail;
+- aa_get_proxy(profile->label.proxy);
+ profile->dents[AAFS_PROF_RAW_DATA] = dent;
+ }
+ #endif /*CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
+@@ -1834,7 +1889,7 @@ static int ns_mkdir_op(struct mnt_idmap
+ if (error)
+ return error;
+
+- parent = aa_get_ns(dir->i_private);
++ parent = get_ns_common_ref(dir->i_private);
+ AA_BUG(d_inode(ns_subns_dir(parent)) != dir);
+
+ /* we have to unlock and then relock to get locking order right
+@@ -1884,7 +1939,7 @@ static int ns_rmdir_op(struct inode *dir
+ if (error)
+ return error;
+
+- parent = aa_get_ns(dir->i_private);
++ parent = get_ns_common_ref(dir->i_private);
+ /* rmdir calls the generic securityfs functions to remove files
+ * from the apparmor dir. It is up to the apparmor ns locking
+ * to avoid races.
+@@ -1954,27 +2009,6 @@ void __aafs_ns_rmdir(struct aa_ns *ns)
+
+ __aa_fs_list_remove_rawdata(ns);
+
+- if (ns_subns_dir(ns)) {
+- sub = d_inode(ns_subns_dir(ns))->i_private;
+- aa_put_ns(sub);
+- }
+- if (ns_subload(ns)) {
+- sub = d_inode(ns_subload(ns))->i_private;
+- aa_put_ns(sub);
+- }
+- if (ns_subreplace(ns)) {
+- sub = d_inode(ns_subreplace(ns))->i_private;
+- aa_put_ns(sub);
+- }
+- if (ns_subremove(ns)) {
+- sub = d_inode(ns_subremove(ns))->i_private;
+- aa_put_ns(sub);
+- }
+- if (ns_subrevision(ns)) {
+- sub = d_inode(ns_subrevision(ns))->i_private;
+- aa_put_ns(sub);
+- }
+-
+ for (i = AAFS_NS_SIZEOF - 1; i >= 0; --i) {
+ aafs_remove(ns->dents[i]);
+ ns->dents[i] = NULL;
+@@ -1999,40 +2033,40 @@ static int __aafs_ns_mkdir_entries(struc
+ return PTR_ERR(dent);
+ ns_subdata_dir(ns) = dent;
+
+- dent = aafs_create_file("revision", 0444, dir, ns,
++ dent = aafs_create_file("revision", 0444, dir,
++ &ns->unconfined->label.count,
+ &aa_fs_ns_revision_fops);
+ if (IS_ERR(dent))
+ return PTR_ERR(dent);
+- aa_get_ns(ns);
+ ns_subrevision(ns) = dent;
+
+- dent = aafs_create_file(".load", 0640, dir, ns,
+- &aa_fs_profile_load);
++ dent = aafs_create_file(".load", 0640, dir,
++ &ns->unconfined->label.count,
++ &aa_fs_profile_load);
+ if (IS_ERR(dent))
+ return PTR_ERR(dent);
+- aa_get_ns(ns);
+ ns_subload(ns) = dent;
+
+- dent = aafs_create_file(".replace", 0640, dir, ns,
+- &aa_fs_profile_replace);
++ dent = aafs_create_file(".replace", 0640, dir,
++ &ns->unconfined->label.count,
++ &aa_fs_profile_replace);
+ if (IS_ERR(dent))
+ return PTR_ERR(dent);
+- aa_get_ns(ns);
+ ns_subreplace(ns) = dent;
+
+- dent = aafs_create_file(".remove", 0640, dir, ns,
+- &aa_fs_profile_remove);
++ dent = aafs_create_file(".remove", 0640, dir,
++ &ns->unconfined->label.count,
++ &aa_fs_profile_remove);
+ if (IS_ERR(dent))
+ return PTR_ERR(dent);
+- aa_get_ns(ns);
+ ns_subremove(ns) = dent;
+
+ /* use create_dentry so we can supply private data */
+- dent = aafs_create("namespaces", S_IFDIR | 0755, dir, ns, NULL, NULL,
+- &ns_dir_inode_operations);
++ dent = aafs_create("namespaces", S_IFDIR | 0755, dir,
++ &ns->unconfined->label.count,
++ NULL, NULL, &ns_dir_inode_operations);
+ if (IS_ERR(dent))
+ return PTR_ERR(dent);
+- aa_get_ns(ns);
+ ns_subns_dir(ns) = dent;
+
+ return 0;
+--- a/security/apparmor/include/label.h
++++ b/security/apparmor/include/label.h
+@@ -101,7 +101,7 @@ enum label_flags {
+
+ struct aa_label;
+ struct aa_proxy {
+- struct kref count;
++ struct aa_common_ref count;
+ struct aa_label __rcu *label;
+ };
+
+@@ -121,7 +121,7 @@ struct label_it {
+ * @ent: set of profiles for label, actual size determined by @size
+ */
+ struct aa_label {
+- struct kref count;
++ struct aa_common_ref count;
+ struct rb_node node;
+ struct rcu_head rcu;
+ struct aa_proxy *proxy;
+@@ -373,7 +373,7 @@ int aa_label_match(struct aa_profile *pr
+ */
+ static inline struct aa_label *__aa_get_label(struct aa_label *l)
+ {
+- if (l && kref_get_unless_zero(&l->count))
++ if (l && kref_get_unless_zero(&l->count.count))
+ return l;
+
+ return NULL;
+@@ -382,7 +382,7 @@ static inline struct aa_label *__aa_get_
+ static inline struct aa_label *aa_get_label(struct aa_label *l)
+ {
+ if (l)
+- kref_get(&(l->count));
++ kref_get(&(l->count.count));
+
+ return l;
+ }
+@@ -402,7 +402,7 @@ static inline struct aa_label *aa_get_la
+ rcu_read_lock();
+ do {
+ c = rcu_dereference(*l);
+- } while (c && !kref_get_unless_zero(&c->count));
++ } while (c && !kref_get_unless_zero(&c->count.count));
+ rcu_read_unlock();
+
+ return c;
+@@ -442,7 +442,7 @@ static inline struct aa_label *aa_get_ne
+ static inline void aa_put_label(struct aa_label *l)
+ {
+ if (l)
+- kref_put(&l->count, aa_label_kref);
++ kref_put(&l->count.count, aa_label_kref);
+ }
+
+
+@@ -452,7 +452,7 @@ void aa_proxy_kref(struct kref *kref);
+ static inline struct aa_proxy *aa_get_proxy(struct aa_proxy *proxy)
+ {
+ if (proxy)
+- kref_get(&(proxy->count));
++ kref_get(&(proxy->count.count));
+
+ return proxy;
+ }
+@@ -460,7 +460,7 @@ static inline struct aa_proxy *aa_get_pr
+ static inline void aa_put_proxy(struct aa_proxy *proxy)
+ {
+ if (proxy)
+- kref_put(&proxy->count, aa_proxy_kref);
++ kref_put(&proxy->count.count, aa_proxy_kref);
+ }
+
+ void __aa_proxy_redirect(struct aa_label *orig, struct aa_label *new);
+--- a/security/apparmor/include/lib.h
++++ b/security/apparmor/include/lib.h
+@@ -71,6 +71,18 @@ void aa_info_message(const char *str);
+ /* Security blob offsets */
+ extern struct lsm_blob_sizes apparmor_blob_sizes;
+
++enum reftype {
++ REF_NS,
++ REF_PROXY,
++ REF_RAWDATA,
++};
++
++/* common reference count used by data the shows up in aafs */
++struct aa_common_ref {
++ struct kref count;
++ enum reftype reftype;
++};
++
+ /**
+ * aa_strneq - compare null terminated @str to a non null terminated substring
+ * @str: a null terminated string
+--- a/security/apparmor/include/policy.h
++++ b/security/apparmor/include/policy.h
+@@ -337,7 +337,7 @@ static inline aa_state_t ANY_RULE_MEDIAT
+ static inline struct aa_profile *aa_get_profile(struct aa_profile *p)
+ {
+ if (p)
+- kref_get(&(p->label.count));
++ kref_get(&(p->label.count.count));
+
+ return p;
+ }
+@@ -351,7 +351,7 @@ static inline struct aa_profile *aa_get_
+ */
+ static inline struct aa_profile *aa_get_profile_not0(struct aa_profile *p)
+ {
+- if (p && kref_get_unless_zero(&p->label.count))
++ if (p && kref_get_unless_zero(&p->label.count.count))
+ return p;
+
+ return NULL;
+@@ -371,7 +371,7 @@ static inline struct aa_profile *aa_get_
+ rcu_read_lock();
+ do {
+ c = rcu_dereference(*p);
+- } while (c && !kref_get_unless_zero(&c->label.count));
++ } while (c && !kref_get_unless_zero(&c->label.count.count));
+ rcu_read_unlock();
+
+ return c;
+@@ -384,7 +384,7 @@ static inline struct aa_profile *aa_get_
+ static inline void aa_put_profile(struct aa_profile *p)
+ {
+ if (p)
+- kref_put(&p->label.count, aa_label_kref);
++ kref_put(&p->label.count.count, aa_label_kref);
+ }
+
+ static inline int AUDIT_MODE(struct aa_profile *profile)
+--- a/security/apparmor/include/policy_unpack.h
++++ b/security/apparmor/include/policy_unpack.h
+@@ -108,7 +108,7 @@ struct aa_ext {
+ * fs entries and drops the associated @count ref.
+ */
+ struct aa_loaddata {
+- struct kref count;
++ struct aa_common_ref count;
+ struct kref pcount;
+ struct list_head list;
+ struct work_struct work;
+@@ -143,7 +143,7 @@ aa_get_i_loaddata(struct aa_loaddata *da
+ {
+
+ if (data)
+- kref_get(&(data->count));
++ kref_get(&(data->count.count));
+ return data;
+ }
+
+@@ -171,7 +171,7 @@ struct aa_loaddata *aa_loaddata_alloc(si
+ static inline void aa_put_i_loaddata(struct aa_loaddata *data)
+ {
+ if (data)
+- kref_put(&data->count, aa_loaddata_kref);
++ kref_put(&data->count.count, aa_loaddata_kref);
+ }
+
+ static inline void aa_put_profile_loaddata(struct aa_loaddata *data)
+--- a/security/apparmor/label.c
++++ b/security/apparmor/label.c
+@@ -52,7 +52,8 @@ static void free_proxy(struct aa_proxy *
+
+ void aa_proxy_kref(struct kref *kref)
+ {
+- struct aa_proxy *proxy = container_of(kref, struct aa_proxy, count);
++ struct aa_proxy *proxy = container_of(kref, struct aa_proxy,
++ count.count);
+
+ free_proxy(proxy);
+ }
+@@ -63,7 +64,8 @@ struct aa_proxy *aa_alloc_proxy(struct a
+
+ new = kzalloc(sizeof(struct aa_proxy), gfp);
+ if (new) {
+- kref_init(&new->count);
++ kref_init(&new->count.count);
++ new->count.reftype = REF_PROXY;
+ rcu_assign_pointer(new->label, aa_get_label(label));
+ }
+ return new;
+@@ -369,7 +371,8 @@ static void label_free_rcu(struct rcu_he
+
+ void aa_label_kref(struct kref *kref)
+ {
+- struct aa_label *label = container_of(kref, struct aa_label, count);
++ struct aa_label *label = container_of(kref, struct aa_label,
++ count.count);
+ struct aa_ns *ns = labels_ns(label);
+
+ if (!ns) {
+@@ -406,7 +409,8 @@ bool aa_label_init(struct aa_label *labe
+
+ label->size = size; /* doesn't include null */
+ label->vec[size] = NULL; /* null terminate */
+- kref_init(&label->count);
++ kref_init(&label->count.count);
++ label->count.reftype = REF_NS; /* for aafs purposes */
+ RB_CLEAR_NODE(&label->node);
+
+ return true;
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -118,7 +118,8 @@ static void do_loaddata_free(struct aa_l
+
+ void aa_loaddata_kref(struct kref *kref)
+ {
+- struct aa_loaddata *d = container_of(kref, struct aa_loaddata, count);
++ struct aa_loaddata *d = container_of(kref, struct aa_loaddata,
++ count.count);
+
+ do_loaddata_free(d);
+ }
+@@ -165,7 +166,8 @@ struct aa_loaddata *aa_loaddata_alloc(si
+ kfree(d);
+ return ERR_PTR(-ENOMEM);
+ }
+- kref_init(&d->count);
++ kref_init(&d->count.count);
++ d->count.reftype = REF_RAWDATA;
+ kref_init(&d->pcount);
+ INIT_LIST_HEAD(&d->list);
+
--- /dev/null
+From 4a904ea1b4a28be89eeaeb0a534e01bcb1226359 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Tue, 24 Feb 2026 10:20:02 -0800
+Subject: apparmor: fix race on rawdata dereference
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit a0b7091c4de45a7325c8780e6934a894f92ac86b upstream.
+
+There is a race condition that leads to a use-after-free situation:
+because the rawdata inodes are not refcounted, an attacker can start
+open()ing one of the rawdata files, and at the same time remove the
+last reference to this rawdata (by removing the corresponding profile,
+for example), which frees its struct aa_loaddata; as a result, when
+seq_rawdata_open() is reached, i_private is a dangling pointer and
+freed memory is accessed.
+
+The rawdata inodes weren't refcounted to avoid a circular refcount and
+were supposed to be held by the profile rawdata reference. However
+during profile removal there is a window where the vfs and profile
+destruction race, resulting in the use after free.
+
+Fix this by moving to a double refcount scheme. Where the profile
+refcount on rawdata is used to break the circular dependency. Allowing
+for freeing of the rawdata once all inode references to the rawdata
+are put.
+
+Fixes: 5d5182cae401 ("apparmor: move to per loaddata files, instead of replicating in profiles")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Maxime Bélair <maxime.belair@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c | 35 ++++++++------
+ security/apparmor/include/policy_unpack.h | 71 ++++++++++++++++++------------
+ security/apparmor/policy.c | 12 ++---
+ security/apparmor/policy_unpack.c | 32 +++++++++----
+ 4 files changed, 93 insertions(+), 57 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -79,7 +79,7 @@ static void rawdata_f_data_free(struct r
+ if (!private)
+ return;
+
+- aa_put_loaddata(private->loaddata);
++ aa_put_i_loaddata(private->loaddata);
+ kvfree(private);
+ }
+
+@@ -404,7 +404,8 @@ static struct aa_loaddata *aa_simple_wri
+
+ data->size = copy_size;
+ if (copy_from_user(data->data, userbuf, copy_size)) {
+- aa_put_loaddata(data);
++ /* trigger free - don't need to put pcount */
++ aa_put_i_loaddata(data);
+ return ERR_PTR(-EFAULT);
+ }
+
+@@ -432,7 +433,10 @@ static ssize_t policy_update(u32 mask, c
+ error = PTR_ERR(data);
+ if (!IS_ERR(data)) {
+ error = aa_replace_profiles(ns, label, mask, data);
+- aa_put_loaddata(data);
++ /* put pcount, which will put count and free if no
++ * profiles referencing it.
++ */
++ aa_put_profile_loaddata(data);
+ }
+ end_section:
+ end_current_label_crit_section(label);
+@@ -503,7 +507,7 @@ static ssize_t profile_remove(struct fil
+ if (!IS_ERR(data)) {
+ data->data[size] = 0;
+ error = aa_remove_profiles(ns, label, data->data, size);
+- aa_put_loaddata(data);
++ aa_put_profile_loaddata(data);
+ }
+ out:
+ end_current_label_crit_section(label);
+@@ -1242,18 +1246,17 @@ static const struct file_operations seq_
+ static int seq_rawdata_open(struct inode *inode, struct file *file,
+ int (*show)(struct seq_file *, void *))
+ {
+- struct aa_loaddata *data = __aa_get_loaddata(inode->i_private);
++ struct aa_loaddata *data = aa_get_i_loaddata(inode->i_private);
+ int error;
+
+ if (!data)
+- /* lost race this ent is being reaped */
+ return -ENOENT;
+
+ error = single_open(file, show, data);
+ if (error) {
+ AA_BUG(file->private_data &&
+ ((struct seq_file *)file->private_data)->private);
+- aa_put_loaddata(data);
++ aa_put_i_loaddata(data);
+ }
+
+ return error;
+@@ -1264,7 +1267,7 @@ static int seq_rawdata_release(struct in
+ struct seq_file *seq = (struct seq_file *) file->private_data;
+
+ if (seq)
+- aa_put_loaddata(seq->private);
++ aa_put_i_loaddata(seq->private);
+
+ return single_release(inode, file);
+ }
+@@ -1378,9 +1381,8 @@ static int rawdata_open(struct inode *in
+ if (!aa_current_policy_view_capable(NULL))
+ return -EACCES;
+
+- loaddata = __aa_get_loaddata(inode->i_private);
++ loaddata = aa_get_i_loaddata(inode->i_private);
+ if (!loaddata)
+- /* lost race: this entry is being reaped */
+ return -ENOENT;
+
+ private = rawdata_f_data_alloc(loaddata->size);
+@@ -1405,7 +1407,7 @@ fail_decompress:
+ return error;
+
+ fail_private_alloc:
+- aa_put_loaddata(loaddata);
++ aa_put_i_loaddata(loaddata);
+ return error;
+ }
+
+@@ -1422,9 +1424,9 @@ static void remove_rawdata_dents(struct
+
+ for (i = 0; i < AAFS_LOADDATA_NDENTS; i++) {
+ if (!IS_ERR_OR_NULL(rawdata->dents[i])) {
+- /* no refcounts on i_private */
+ aafs_remove(rawdata->dents[i]);
+ rawdata->dents[i] = NULL;
++ aa_put_i_loaddata(rawdata);
+ }
+ }
+ }
+@@ -1463,18 +1465,21 @@ int __aa_fs_create_rawdata(struct aa_ns
+ if (IS_ERR(dir))
+ /* ->name freed when rawdata freed */
+ return PTR_ERR(dir);
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_DIR] = dir;
+
+ dent = aafs_create_file("abi", S_IFREG | 0444, dir, rawdata,
+ &seq_rawdata_abi_fops);
+ if (IS_ERR(dent))
+ goto fail;
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_ABI] = dent;
+
+ dent = aafs_create_file("revision", S_IFREG | 0444, dir, rawdata,
+ &seq_rawdata_revision_fops);
+ if (IS_ERR(dent))
+ goto fail;
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_REVISION] = dent;
+
+ if (aa_g_hash_policy) {
+@@ -1482,6 +1487,7 @@ int __aa_fs_create_rawdata(struct aa_ns
+ rawdata, &seq_rawdata_hash_fops);
+ if (IS_ERR(dent))
+ goto fail;
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_HASH] = dent;
+ }
+
+@@ -1490,24 +1496,25 @@ int __aa_fs_create_rawdata(struct aa_ns
+ &seq_rawdata_compressed_size_fops);
+ if (IS_ERR(dent))
+ goto fail;
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_COMPRESSED_SIZE] = dent;
+
+ dent = aafs_create_file("raw_data", S_IFREG | 0444,
+ dir, rawdata, &rawdata_fops);
+ if (IS_ERR(dent))
+ goto fail;
++ aa_get_i_loaddata(rawdata);
+ rawdata->dents[AAFS_LOADDATA_DATA] = dent;
+ d_inode(dent)->i_size = rawdata->size;
+
+ rawdata->ns = aa_get_ns(ns);
+ list_add(&rawdata->list, &ns->rawdata_list);
+- /* no refcount on inode rawdata */
+
+ return 0;
+
+ fail:
+ remove_rawdata_dents(rawdata);
+-
++ aa_put_i_loaddata(rawdata);
+ return PTR_ERR(dent);
+ }
+ #endif /* CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
+--- a/security/apparmor/include/policy_unpack.h
++++ b/security/apparmor/include/policy_unpack.h
+@@ -87,17 +87,29 @@ struct aa_ext {
+ u32 version;
+ };
+
+-/*
+- * struct aa_loaddata - buffer of policy raw_data set
++/* struct aa_loaddata - buffer of policy raw_data set
++ * @count: inode/filesystem refcount - use aa_get_i_loaddata()
++ * @pcount: profile refcount - use aa_get_profile_loaddata()
++ * @list: list the loaddata is on
++ * @work: used to do a delayed cleanup
++ * @dents: refs to dents created in aafs
++ * @ns: the namespace this loaddata was loaded into
++ * @name:
++ * @size: the size of the data that was loaded
++ * @compressed_size: the size of the data when it is compressed
++ * @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
+ *
+- * there is no loaddata ref for being on ns list, nor a ref from
+- * d_inode(@dentry) when grab a ref from these, @ns->lock must be held
+- * && __aa_get_loaddata() needs to be used, and the return value
+- * checked, if NULL the loaddata is already being reaped and should be
+- * considered dead.
++ * There is no loaddata ref for being on ns->rawdata_list, so
++ * @ns->lock must be held when walking the list. Dentries and
++ * inode opens hold refs on @count; profiles hold refs on @pcount.
++ * When the last @pcount drops, do_ploaddata_rmfs() removes the
++ * fs entries and drops the associated @count ref.
+ */
+ struct aa_loaddata {
+ struct kref count;
++ struct kref pcount;
+ struct list_head list;
+ struct work_struct work;
+ struct dentry *dents[AAFS_LOADDATA_NDENTS];
+@@ -119,52 +131,55 @@ struct aa_loaddata {
+ int aa_unpack(struct aa_loaddata *udata, struct list_head *lh, const char **ns);
+
+ /**
+- * __aa_get_loaddata - get a reference count to uncounted data reference
++ * aa_get_loaddata - get a reference count from a counted data reference
+ * @data: reference to get a count on
+ *
+- * Returns: pointer to reference OR NULL if race is lost and reference is
+- * being repeated.
+- * Requires: @data->ns->lock held, and the return code MUST be checked
+- *
+- * Use only from inode->i_private and @data->list found references
++ * Returns: pointer to reference
++ * Requires: @data to have a valid reference count on it. It is a bug
++ * if the race to reap can be encountered when it is used.
+ */
+ static inline struct aa_loaddata *
+-__aa_get_loaddata(struct aa_loaddata *data)
++aa_get_i_loaddata(struct aa_loaddata *data)
+ {
+- if (data && kref_get_unless_zero(&(data->count)))
+- return data;
+
+- return NULL;
++ if (data)
++ kref_get(&(data->count));
++ return data;
+ }
+
++
+ /**
+- * aa_get_loaddata - get a reference count from a counted data reference
++ * aa_get_profile_loaddata - get a profile reference count on loaddata
+ * @data: reference to get a count on
+ *
+- * Returns: point to reference
+- * Requires: @data to have a valid reference count on it. It is a bug
+- * if the race to reap can be encountered when it is used.
++ * Returns: pointer to reference
++ * Requires: @data to have a valid reference count on it.
+ */
+ static inline struct aa_loaddata *
+-aa_get_loaddata(struct aa_loaddata *data)
++aa_get_profile_loaddata(struct aa_loaddata *data)
+ {
+- struct aa_loaddata *tmp = __aa_get_loaddata(data);
+-
+- AA_BUG(data && !tmp);
+-
+- return tmp;
++ if (data)
++ kref_get(&(data->pcount));
++ return 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_kref(struct kref *kref);
++void aa_ploaddata_kref(struct kref *kref);
+ struct aa_loaddata *aa_loaddata_alloc(size_t size);
+-static inline void aa_put_loaddata(struct aa_loaddata *data)
++static inline void aa_put_i_loaddata(struct aa_loaddata *data)
+ {
+ if (data)
+ kref_put(&data->count, aa_loaddata_kref);
+ }
+
++static inline void aa_put_profile_loaddata(struct aa_loaddata *data)
++{
++ if (data)
++ kref_put(&data->pcount, aa_ploaddata_kref);
++}
++
+ #if IS_ENABLED(CONFIG_KUNIT)
+ bool aa_inbounds(struct aa_ext *e, size_t size);
+ size_t aa_unpack_u16_chunk(struct aa_ext *e, char **chunk);
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -336,7 +336,7 @@ void aa_free_profile(struct aa_profile *
+ }
+
+ kfree_sensitive(profile->hash);
+- aa_put_loaddata(profile->rawdata);
++ aa_put_profile_loaddata(profile->rawdata);
+ aa_label_destroy(&profile->label);
+
+ kfree_sensitive(profile);
+@@ -1120,7 +1120,7 @@ ssize_t aa_replace_profiles(struct aa_ns
+ LIST_HEAD(lh);
+
+ op = mask & AA_MAY_REPLACE_POLICY ? OP_PROF_REPL : OP_PROF_LOAD;
+- aa_get_loaddata(udata);
++ aa_get_profile_loaddata(udata);
+ /* released below */
+ error = aa_unpack(udata, &lh, &ns_name);
+ if (error)
+@@ -1172,10 +1172,10 @@ ssize_t aa_replace_profiles(struct aa_ns
+ if (aa_rawdata_eq(rawdata_ent, udata)) {
+ struct aa_loaddata *tmp;
+
+- tmp = __aa_get_loaddata(rawdata_ent);
++ tmp = aa_get_profile_loaddata(rawdata_ent);
+ /* check we didn't fail the race */
+ if (tmp) {
+- aa_put_loaddata(udata);
++ aa_put_profile_loaddata(udata);
+ udata = tmp;
+ break;
+ }
+@@ -1188,7 +1188,7 @@ ssize_t aa_replace_profiles(struct aa_ns
+ struct aa_profile *p;
+
+ if (aa_g_export_binary)
+- ent->new->rawdata = aa_get_loaddata(udata);
++ ent->new->rawdata = aa_get_profile_loaddata(udata);
+ error = __lookup_replace(ns, ent->new->base.hname,
+ !(mask & AA_MAY_REPLACE_POLICY),
+ &ent->old, &info);
+@@ -1321,7 +1321,7 @@ ssize_t aa_replace_profiles(struct aa_ns
+
+ out:
+ aa_put_ns(ns);
+- aa_put_loaddata(udata);
++ aa_put_profile_loaddata(udata);
+ kfree(ns_name);
+
+ if (error)
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -108,34 +108,47 @@ bool aa_rawdata_eq(struct aa_loaddata *l
+ return memcmp(l->data, r->data, r->compressed_size ?: r->size) == 0;
+ }
+
++static void do_loaddata_free(struct aa_loaddata *d)
++{
++ kfree_sensitive(d->hash);
++ kfree_sensitive(d->name);
++ kvfree(d->data);
++ kfree_sensitive(d);
++}
++
++void aa_loaddata_kref(struct kref *kref)
++{
++ struct aa_loaddata *d = container_of(kref, struct aa_loaddata, count);
++
++ do_loaddata_free(d);
++}
++
+ /*
+ * need to take the ns mutex lock which is NOT safe most places that
+ * put_loaddata is called, so we have to delay freeing it
+ */
+-static void do_loaddata_free(struct work_struct *work)
++static void do_ploaddata_rmfs(struct work_struct *work)
+ {
+ struct aa_loaddata *d = container_of(work, struct aa_loaddata, work);
+ struct aa_ns *ns = aa_get_ns(d->ns);
+
+ if (ns) {
+ mutex_lock_nested(&ns->lock, ns->level);
++ /* remove fs ref to loaddata */
+ __aa_fs_remove_rawdata(d);
+ mutex_unlock(&ns->lock);
+ aa_put_ns(ns);
+ }
+-
+- kfree_sensitive(d->hash);
+- kfree_sensitive(d->name);
+- kvfree(d->data);
+- kfree_sensitive(d);
++ /* called by dropping last pcount, so drop its associated icount */
++ aa_put_i_loaddata(d);
+ }
+
+-void aa_loaddata_kref(struct kref *kref)
++void aa_ploaddata_kref(struct kref *kref)
+ {
+- struct aa_loaddata *d = container_of(kref, struct aa_loaddata, count);
++ struct aa_loaddata *d = container_of(kref, struct aa_loaddata, pcount);
+
+ if (d) {
+- INIT_WORK(&d->work, do_loaddata_free);
++ INIT_WORK(&d->work, do_ploaddata_rmfs);
+ schedule_work(&d->work);
+ }
+ }
+@@ -153,6 +166,7 @@ struct aa_loaddata *aa_loaddata_alloc(si
+ return ERR_PTR(-ENOMEM);
+ }
+ kref_init(&d->count);
++ kref_init(&d->pcount);
+ INIT_LIST_HEAD(&d->list);
+
+ return d;
--- /dev/null
+From d778afe7716788427d3d70e0dd809fb97cdfc619 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 29 Jan 2026 17:08:25 +0100
+Subject: apparmor: fix side-effect bug in match_char() macro usage
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit 8756b68edae37ff546c02091989a4ceab3f20abd upstream.
+
+The match_char() macro evaluates its character parameter multiple
+times when traversing differential encoding chains. When invoked
+with *str++, the string pointer advances on each iteration of the
+inner do-while loop, causing the DFA to check different characters
+at each iteration and therefore skip input characters.
+This results in out-of-bounds reads when the pointer advances past
+the input buffer boundary.
+
+[ 94.984676] ==================================================================
+[ 94.985301] BUG: KASAN: slab-out-of-bounds in aa_dfa_match+0x5ae/0x760
+[ 94.985655] Read of size 1 at addr ffff888100342000 by task file/976
+
+[ 94.986319] CPU: 7 UID: 1000 PID: 976 Comm: file Not tainted 6.19.0-rc7-next-20260127 #1 PREEMPT(lazy)
+[ 94.986322] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
+[ 94.986329] Call Trace:
+[ 94.986341] <TASK>
+[ 94.986347] dump_stack_lvl+0x5e/0x80
+[ 94.986374] print_report+0xc8/0x270
+[ 94.986384] ? aa_dfa_match+0x5ae/0x760
+[ 94.986388] kasan_report+0x118/0x150
+[ 94.986401] ? aa_dfa_match+0x5ae/0x760
+[ 94.986405] aa_dfa_match+0x5ae/0x760
+[ 94.986408] __aa_path_perm+0x131/0x400
+[ 94.986418] aa_path_perm+0x219/0x2f0
+[ 94.986424] apparmor_file_open+0x345/0x570
+[ 94.986431] security_file_open+0x5c/0x140
+[ 94.986442] do_dentry_open+0x2f6/0x1120
+[ 94.986450] vfs_open+0x38/0x2b0
+[ 94.986453] ? may_open+0x1e2/0x2b0
+[ 94.986466] path_openat+0x231b/0x2b30
+[ 94.986469] ? __x64_sys_openat+0xf8/0x130
+[ 94.986477] do_file_open+0x19d/0x360
+[ 94.986487] do_sys_openat2+0x98/0x100
+[ 94.986491] __x64_sys_openat+0xf8/0x130
+[ 94.986499] do_syscall_64+0x8e/0x660
+[ 94.986515] ? count_memcg_events+0x15f/0x3c0
+[ 94.986526] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 94.986540] ? handle_mm_fault+0x1639/0x1ef0
+[ 94.986551] ? vma_start_read+0xf0/0x320
+[ 94.986558] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 94.986561] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 94.986563] ? fpregs_assert_state_consistent+0x50/0xe0
+[ 94.986572] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 94.986574] ? arch_exit_to_user_mode_prepare+0x9/0xb0
+[ 94.986587] ? srso_alias_return_thunk+0x5/0xfbef5
+[ 94.986588] ? irqentry_exit+0x3c/0x590
+[ 94.986595] entry_SYSCALL_64_after_hwframe+0x76/0x7e
+[ 94.986597] RIP: 0033:0x7fda4a79c3ea
+
+Fix by extracting the character value before invoking match_char,
+ensuring single evaluation per outer loop.
+
+Fixes: 074c1cd798cb ("apparmor: dfa move character match into a macro")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/match.c | 30 ++++++++++++++++++++----------
+ 1 file changed, 20 insertions(+), 10 deletions(-)
+
+--- a/security/apparmor/match.c
++++ b/security/apparmor/match.c
+@@ -408,13 +408,18 @@ aa_state_t aa_dfa_match_len(struct aa_df
+ if (dfa->tables[YYTD_ID_EC]) {
+ /* Equivalence class table defined */
+ u8 *equiv = EQUIV_TABLE(dfa);
+- for (; len; len--)
+- match_char(state, def, base, next, check,
+- equiv[(u8) *str++]);
++ for (; len; len--) {
++ u8 c = equiv[(u8) *str];
++
++ match_char(state, def, base, next, check, c);
++ str++;
++ }
+ } else {
+ /* default is direct to next state */
+- for (; len; len--)
+- match_char(state, def, base, next, check, (u8) *str++);
++ for (; len; len--) {
++ match_char(state, def, base, next, check, (u8) *str);
++ str++;
++ }
+ }
+
+ return state;
+@@ -448,13 +453,18 @@ aa_state_t aa_dfa_match(struct aa_dfa *d
+ /* Equivalence class table defined */
+ u8 *equiv = EQUIV_TABLE(dfa);
+ /* default is direct to next state */
+- while (*str)
+- match_char(state, def, base, next, check,
+- equiv[(u8) *str++]);
++ while (*str) {
++ u8 c = equiv[(u8) *str];
++
++ match_char(state, def, base, next, check, c);
++ str++;
++ }
+ } else {
+ /* default is direct to next state */
+- while (*str)
+- match_char(state, def, base, next, check, (u8) *str++);
++ while (*str) {
++ match_char(state, def, base, next, check, (u8) *str);
++ str++;
++ }
+ }
+
+ return state;
--- /dev/null
+From 7b639636b39c89236a213b2312f5d1dea351365f Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Fri, 7 Nov 2025 08:36:04 -0800
+Subject: apparmor: fix unprivileged local user can do privileged policy management
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 6601e13e82841879406bf9f369032656f441a425 upstream.
+
+An unprivileged local user can load, replace, and remove profiles by
+opening the apparmorfs interfaces, via a confused deputy attack, by
+passing the opened fd to a privileged process, and getting the
+privileged process to write to the interface.
+
+This does require a privileged target that can be manipulated to do
+the write for the unprivileged process, but once such access is
+achieved full policy management is possible and all the possible
+implications that implies: removing confinement, DoS of system or
+target applications by denying all execution, by-passing the
+unprivileged user namespace restriction, to exploiting kernel bugs for
+a local privilege escalation.
+
+The policy management interface can not have its permissions simply
+changed from 0666 to 0600 because non-root processes need to be able
+to load policy to different policy namespaces.
+
+Instead ensure the task writing the interface has privileges that
+are a subset of the task that opened the interface. This is already
+done via policy for confined processes, but unconfined can delegate
+access to the opened fd, by-passing the usual policy check.
+
+Fixes: b7fd2c0340eac ("apparmor: add per policy ns .load, .replace, .remove interface files")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c | 16 +++++++++-------
+ security/apparmor/include/policy.h | 2 +-
+ security/apparmor/policy.c | 34 +++++++++++++++++++++++++++++++++-
+ 3 files changed, 43 insertions(+), 9 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -412,7 +412,8 @@ static struct aa_loaddata *aa_simple_wri
+ }
+
+ static ssize_t policy_update(u32 mask, const char __user *buf, size_t size,
+- loff_t *pos, struct aa_ns *ns)
++ loff_t *pos, struct aa_ns *ns,
++ const struct cred *ocred)
+ {
+ struct aa_loaddata *data;
+ struct aa_label *label;
+@@ -423,7 +424,7 @@ static ssize_t policy_update(u32 mask, c
+ /* high level check about policy management - fine grained in
+ * below after unpack
+ */
+- error = aa_may_manage_policy(current_cred(), label, ns, mask);
++ error = aa_may_manage_policy(current_cred(), label, ns, ocred, mask);
+ if (error)
+ goto end_section;
+
+@@ -444,7 +445,8 @@ static ssize_t profile_load(struct file
+ loff_t *pos)
+ {
+ struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
+- int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns);
++ int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns,
++ f->f_cred);
+
+ aa_put_ns(ns);
+
+@@ -462,7 +464,7 @@ static ssize_t profile_replace(struct fi
+ {
+ struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
+ int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY,
+- buf, size, pos, ns);
++ buf, size, pos, ns, f->f_cred);
+ aa_put_ns(ns);
+
+ return error;
+@@ -487,7 +489,7 @@ static ssize_t profile_remove(struct fil
+ * below after unpack
+ */
+ error = aa_may_manage_policy(current_cred(), label, ns,
+- AA_MAY_REMOVE_POLICY);
++ f->f_cred, AA_MAY_REMOVE_POLICY);
+ if (error)
+ goto out;
+
+@@ -1819,7 +1821,7 @@ static int ns_mkdir_op(struct mnt_idmap
+ int error;
+
+ label = begin_current_label_crit_section();
+- error = aa_may_manage_policy(current_cred(), label, NULL,
++ error = aa_may_manage_policy(current_cred(), label, NULL, NULL,
+ AA_MAY_LOAD_POLICY);
+ end_current_label_crit_section(label);
+ if (error)
+@@ -1869,7 +1871,7 @@ static int ns_rmdir_op(struct inode *dir
+ int error;
+
+ label = begin_current_label_crit_section();
+- error = aa_may_manage_policy(current_cred(), label, NULL,
++ error = aa_may_manage_policy(current_cred(), label, NULL, NULL,
+ AA_MAY_LOAD_POLICY);
+ end_current_label_crit_section(label);
+ if (error)
+--- a/security/apparmor/include/policy.h
++++ b/security/apparmor/include/policy.h
+@@ -401,7 +401,7 @@ bool aa_policy_admin_capable(const struc
+ struct aa_label *label, struct aa_ns *ns);
+ int aa_may_manage_policy(const struct cred *subj_cred,
+ struct aa_label *label, struct aa_ns *ns,
+- u32 mask);
++ const struct cred *ocred, u32 mask);
+ bool aa_current_policy_view_capable(struct aa_ns *ns);
+ bool aa_current_policy_admin_capable(struct aa_ns *ns);
+
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -891,17 +891,44 @@ bool aa_current_policy_admin_capable(str
+ return res;
+ }
+
++static bool is_subset_of_obj_privilege(const struct cred *cred,
++ struct aa_label *label,
++ const struct cred *ocred)
++{
++ if (cred == ocred)
++ return true;
++
++ if (!aa_label_is_subset(label, cred_label(ocred)))
++ return false;
++ /* don't allow crossing userns for now */
++ if (cred->user_ns != ocred->user_ns)
++ return false;
++ if (!cap_issubset(cred->cap_inheritable, ocred->cap_inheritable))
++ return false;
++ if (!cap_issubset(cred->cap_permitted, ocred->cap_permitted))
++ return false;
++ if (!cap_issubset(cred->cap_effective, ocred->cap_effective))
++ return false;
++ if (!cap_issubset(cred->cap_bset, ocred->cap_bset))
++ return false;
++ if (!cap_issubset(cred->cap_ambient, ocred->cap_ambient))
++ return false;
++ return true;
++}
++
++
+ /**
+ * aa_may_manage_policy - can the current task manage policy
+ * @subj_cred; subjects cred
+ * @label: label to check if it can manage policy
+ * @ns: namespace being managed by @label (may be NULL if @label's ns)
++ * @ocred: object cred if request is coming from an open object
+ * @mask: contains the policy manipulation operation being done
+ *
+ * Returns: 0 if the task is allowed to manipulate policy else error
+ */
+ int aa_may_manage_policy(const struct cred *subj_cred, struct aa_label *label,
+- struct aa_ns *ns, u32 mask)
++ struct aa_ns *ns, const struct cred *ocred, u32 mask)
+ {
+ const char *op;
+
+@@ -917,6 +944,11 @@ int aa_may_manage_policy(const struct cr
+ return audit_policy(label, op, NULL, NULL, "policy_locked",
+ -EACCES);
+
++ if (ocred && !is_subset_of_obj_privilege(subj_cred, label, ocred))
++ return audit_policy(label, op, NULL, NULL,
++ "not privileged for target profile",
++ -EACCES);
++
+ if (!aa_policy_admin_capable(subj_cred, label, ns))
+ return audit_policy(label, op, NULL, NULL, "not policy admin",
+ -EACCES);
--- /dev/null
+From 6b6edff204f4d9b72ef23e40acc68d457d3bc4c9 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Tue, 13 Jan 2026 09:09:43 +0100
+Subject: apparmor: replace recursive profile removal with iterative approach
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit ab09264660f9de5d05d1ef4e225aa447c63a8747 upstream.
+
+The profile removal code uses recursion when removing nested profiles,
+which can lead to kernel stack exhaustion and system crashes.
+
+Reproducer:
+ $ pf='a'; for ((i=0; i<1024; i++)); do
+ echo -e "profile $pf { \n }" | apparmor_parser -K -a;
+ pf="$pf//x";
+ done
+ $ echo -n a > /sys/kernel/security/apparmor/.remove
+
+Replace the recursive __aa_profile_list_release() approach with an
+iterative approach in __remove_profile(). The function repeatedly
+finds and removes leaf profiles until the entire subtree is removed,
+maintaining the same removal semantic without recursion.
+
+Fixes: c88d4c7b049e ("AppArmor: core policy routines")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy.c | 30 +++++++++++++++++++++++++++---
+ 1 file changed, 27 insertions(+), 3 deletions(-)
+
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -182,19 +182,43 @@ static void __list_remove_profile(struct
+ }
+
+ /**
+- * __remove_profile - remove old profile, and children
+- * @profile: profile to be replaced (NOT NULL)
++ * __remove_profile - remove profile, and children
++ * @profile: profile to be removed (NOT NULL)
+ *
+ * Requires: namespace list lock be held, or list not be shared
+ */
+ static void __remove_profile(struct aa_profile *profile)
+ {
++ struct aa_profile *curr, *to_remove;
++
+ AA_BUG(!profile);
+ AA_BUG(!profile->ns);
+ AA_BUG(!mutex_is_locked(&profile->ns->lock));
+
+ /* release any children lists first */
+- __aa_profile_list_release(&profile->base.profiles);
++ if (!list_empty(&profile->base.profiles)) {
++ curr = list_first_entry(&profile->base.profiles, struct aa_profile, base.list);
++
++ while (curr != profile) {
++
++ while (!list_empty(&curr->base.profiles))
++ curr = list_first_entry(&curr->base.profiles,
++ struct aa_profile, base.list);
++
++ to_remove = curr;
++ if (!list_is_last(&to_remove->base.list,
++ &aa_deref_parent(curr)->base.profiles))
++ curr = list_next_entry(to_remove, base.list);
++ else
++ curr = aa_deref_parent(curr);
++
++ /* released by free_profile */
++ aa_label_remove(&to_remove->label);
++ __aafs_profile_rmdir(to_remove);
++ __list_remove_profile(to_remove);
++ }
++ }
++
+ /* released by free_profile */
+ aa_label_remove(&profile->label);
+ __aafs_profile_rmdir(profile);
--- /dev/null
+From 7a12faaab0a4c112408f011742dcef03d2a832c1 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 15 Jan 2026 15:30:50 +0100
+Subject: apparmor: validate DFA start states are in bounds in unpack_pdb
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit 9063d7e2615f4a7ab321de6b520e23d370e58816 upstream.
+
+Start states are read from untrusted data and used as indexes into the
+DFA state tables. The aa_dfa_next() function call in unpack_pdb() will
+access dfa->tables[YYTD_ID_BASE][start], and if the start state exceeds
+the number of states in the DFA, this results in an out-of-bound read.
+
+==================================================================
+ BUG: KASAN: slab-out-of-bounds in aa_dfa_next+0x2a1/0x360
+ Read of size 4 at addr ffff88811956fb90 by task su/1097
+ ...
+
+Reject policies with out-of-bounds start states during unpacking
+to prevent the issue.
+
+Fixes: ad5ff3db53c6 ("AppArmor: Add ability to load extended policy")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy_unpack.c | 12 +++++++++++-
+ 1 file changed, 11 insertions(+), 1 deletion(-)
+
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -764,7 +764,17 @@ static int unpack_pdb(struct aa_ext *e,
+ if (!aa_unpack_u32(e, &pdb->start[AA_CLASS_FILE], "dfa_start")) {
+ /* default start state for xmatch and file dfa */
+ pdb->start[AA_CLASS_FILE] = DFA_START;
+- } /* setup class index */
++ }
++
++ size_t state_count = pdb->dfa->tables[YYTD_ID_BASE]->td_lolen;
++
++ if (pdb->start[0] >= state_count ||
++ pdb->start[AA_CLASS_FILE] >= state_count) {
++ *info = "invalid dfa start state";
++ goto fail;
++ }
++
++ /* setup class index */
+ for (i = AA_CLASS_FILE + 1; i <= AA_CLASS_LAST; i++) {
+ pdb->start[i] = aa_dfa_next(pdb->dfa, pdb->start[0],
+ i);
tracing-add-null-pointer-check-to-trigger_data_free.patch
net-sched-only-allow-act_ct-to-bind-to-clsact-ingress-qdiscs-and-shared-blocks.patch
net-tcp-accept-old-ack-during-closing.patch
+apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch
+apparmor-fix-memory-leak-in-verify_header.patch
+apparmor-replace-recursive-profile-removal-with-iterative-approach.patch
+apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch
+apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch
+apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch
+apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch
+apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch
+apparmor-fix-differential-encoding-verification.patch
+apparmor-fix-race-on-rawdata-dereference.patch
+apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch