From: Greg Kroah-Hartman Date: Thu, 12 Mar 2026 19:58:45 +0000 (+0100) Subject: 6.12-stable patches X-Git-Tag: v6.12.77~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=18fc6f87205a439a6996f5808fcfbbd46eff40ac;p=thirdparty%2Fkernel%2Fstable-queue.git 6.12-stable patches added patches: apparmor-fix-differential-encoding-verification.patch apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch apparmor-fix-memory-leak-in-verify_header.patch apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch apparmor-fix-race-on-rawdata-dereference.patch apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch apparmor-replace-recursive-profile-removal-with-iterative-approach.patch apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch --- diff --git a/queue-6.12/apparmor-fix-differential-encoding-verification.patch b/queue-6.12/apparmor-fix-differential-encoding-verification.patch new file mode 100644 index 0000000000..646aba1d4d --- /dev/null +++ b/queue-6.12/apparmor-fix-differential-encoding-verification.patch @@ -0,0 +1,90 @@ +From 29d8ddb441b69f905bf6ceca0964078e422351d6 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Fri, 17 Oct 2025 01:53:00 -0700 +Subject: apparmor: fix differential encoding verification + +From: John Johansen + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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 +@@ -183,6 +183,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; + diff --git a/queue-6.12/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch b/queue-6.12/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch new file mode 100644 index 0000000000..1e0663255c --- /dev/null +++ b/queue-6.12/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch @@ -0,0 +1,47 @@ +From a37bc9af4cd6d98e3250e151af87aa9e96238dee Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Wed, 10 Sep 2025 06:22:17 -0700 +Subject: apparmor: Fix double free of ns_name in aa_replace_profiles() + +From: John Johansen + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + security/apparmor/policy.c | 1 + + 1 file changed, 1 insertion(+) + +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -1118,6 +1118,7 @@ ssize_t aa_replace_profiles(struct aa_ns + goto fail; + } + ns_name = ent->ns_name; ++ ent->ns_name = NULL; + } else + count++; + } diff --git a/queue-6.12/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch b/queue-6.12/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch new file mode 100644 index 0000000000..7b9164f50e --- /dev/null +++ b/queue-6.12/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch @@ -0,0 +1,49 @@ +From 2374abaa6cc62b1266836469b7e57a3fcd83c364 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Tue, 3 Mar 2026 11:08:02 -0800 +Subject: apparmor: fix: limit the number of levels of policy namespaces + +From: John Johansen + +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 +Reviewed-by: Ryan Lee +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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 +@@ -223,6 +223,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); diff --git a/queue-6.12/apparmor-fix-memory-leak-in-verify_header.patch b/queue-6.12/apparmor-fix-memory-leak-in-verify_header.patch new file mode 100644 index 0000000000..08b9e5fceb --- /dev/null +++ b/queue-6.12/apparmor-fix-memory-leak-in-verify_header.patch @@ -0,0 +1,40 @@ +From e632ffa273865b3cccabe01e2f69c8c5077fe8c9 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Tue, 20 Jan 2026 15:24:04 +0100 +Subject: apparmor: fix memory leak in verify_header + +From: Massimiliano Pellizzer + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + security/apparmor/policy_unpack.c | 1 - + 1 file changed, 1 deletion(-) + +--- a/security/apparmor/policy_unpack.c ++++ b/security/apparmor/policy_unpack.c +@@ -1142,7 +1142,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")) { diff --git a/queue-6.12/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch b/queue-6.12/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch new file mode 100644 index 0000000000..cbc21208c2 --- /dev/null +++ b/queue-6.12/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch @@ -0,0 +1,91 @@ +From 5c4ed07c11f3ac20a7be83905b9f73f0fa720462 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 29 Jan 2026 16:51:11 +0100 +Subject: apparmor: fix missing bounds check on DEFAULT table in verify_dfa() + +From: Massimiliano Pellizzer + +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] +[ 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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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; diff --git a/queue-6.12/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch b/queue-6.12/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch new file mode 100644 index 0000000000..9343a39211 --- /dev/null +++ b/queue-6.12/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch @@ -0,0 +1,701 @@ +From 3e9ed3d9ae03527e1cf3d01ab7c801df3c4844d1 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Sun, 1 Mar 2026 16:10:51 -0800 +Subject: apparmor: fix race between freeing data and fs accessing it + +From: John Johansen + +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 aand 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 +Reviewed-by: Georgia Garcia +Reviewed-by: Maxime Bélair +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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) +@@ -1379,7 +1452,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; + +@@ -1424,7 +1497,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); + } + } + } +@@ -1463,45 +1535,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("sha256", 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; + +@@ -1512,7 +1580,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 */ +@@ -1536,13 +1603,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; + } + } +@@ -1576,14 +1640,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 +@@ -1629,7 +1686,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; +@@ -1771,27 +1829,24 @@ int __aafs_profile_mkdir(struct aa_profi + if (profile->rawdata) { + if (aa_g_hash_policy) { + dent = aafs_create("raw_sha256", S_IFLNK | 0444, dir, +- profile->label.proxy, NULL, NULL, +- &rawdata_link_sha256_iops); ++ &profile->label.proxy->count, NULL, ++ NULL, &rawdata_link_sha256_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 */ +@@ -1828,7 +1883,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 +@@ -1878,7 +1933,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. +@@ -1948,27 +2003,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; +@@ -1993,40 +2027,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 +@@ -329,7 +329,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; + } +@@ -343,7 +343,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; +@@ -363,7 +363,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; +@@ -376,7 +376,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; +@@ -371,7 +373,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) { +@@ -408,7 +411,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); + diff --git a/queue-6.12/apparmor-fix-race-on-rawdata-dereference.patch b/queue-6.12/apparmor-fix-race-on-rawdata-dereference.patch new file mode 100644 index 0000000000..52c9dfd150 --- /dev/null +++ b/queue-6.12/apparmor-fix-race-on-rawdata-dereference.patch @@ -0,0 +1,437 @@ +From 33a2cf13a67bed7fe0d84b73664fa795d8acfbde Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Tue, 24 Feb 2026 10:20:02 -0800 +Subject: apparmor: fix race on rawdata dereference + +From: John Johansen + +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 +Reviewed-by: Georgia Garcia +Reviewed-by: Maxime Bélair +Reviewed-by: Cengiz Can +Tested-by: Salvatore Bonaccorso +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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); + } +@@ -1376,9 +1379,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); +@@ -1403,7 +1405,7 @@ fail_decompress: + return error; + + fail_private_alloc: +- aa_put_loaddata(loaddata); ++ aa_put_i_loaddata(loaddata); + return error; + } + +@@ -1420,9 +1422,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); + } + } + } +@@ -1461,18 +1463,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) { +@@ -1480,6 +1485,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; + } + +@@ -1488,24 +1494,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 +@@ -338,7 +338,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); +@@ -1123,7 +1123,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) +@@ -1175,10 +1175,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; + } +@@ -1191,7 +1191,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); +@@ -1324,7 +1324,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; diff --git a/queue-6.12/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch b/queue-6.12/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch new file mode 100644 index 0000000000..fa373a36dd --- /dev/null +++ b/queue-6.12/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch @@ -0,0 +1,123 @@ +From 5a037446353b8c131e6d7d3174f8ae2c4e8117d7 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 29 Jan 2026 17:08:25 +0100 +Subject: apparmor: fix side-effect bug in match_char() macro usage + +From: Massimiliano Pellizzer + +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] +[ 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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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; diff --git a/queue-6.12/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch b/queue-6.12/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch new file mode 100644 index 0000000000..f76e634add --- /dev/null +++ b/queue-6.12/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch @@ -0,0 +1,182 @@ +From 86d539efdbc2c586c27a115fd3d95e526c6ebb55 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Fri, 7 Nov 2025 08:36:04 -0800 +Subject: apparmor: fix unprivileged local user can do privileged policy management + +From: John Johansen + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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; + +@@ -1813,7 +1815,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) +@@ -1863,7 +1865,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 +@@ -393,7 +393,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 +@@ -894,17 +894,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; + +@@ -920,6 +947,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); diff --git a/queue-6.12/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch b/queue-6.12/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch new file mode 100644 index 0000000000..54bcc1e551 --- /dev/null +++ b/queue-6.12/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch @@ -0,0 +1,85 @@ +From 0947da14e2e2709b1c4c97baa1c6b9ded3c09549 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Tue, 13 Jan 2026 09:09:43 +0100 +Subject: apparmor: replace recursive profile removal with iterative approach + +From: Massimiliano Pellizzer + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + security/apparmor/policy.c | 30 +++++++++++++++++++++++++++--- + 1 file changed, 27 insertions(+), 3 deletions(-) + +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -184,19 +184,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); diff --git a/queue-6.12/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch b/queue-6.12/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch new file mode 100644 index 0000000000..725a957387 --- /dev/null +++ b/queue-6.12/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch @@ -0,0 +1,55 @@ +From 2a16417f26895c90c84e8da4bbbb75ef3a93b892 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 15 Jan 2026 15:30:50 +0100 +Subject: apparmor: validate DFA start states are in bounds in unpack_pdb + +From: Massimiliano Pellizzer + +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 +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +Signed-off-by: Greg Kroah-Hartman +--- + 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 +@@ -762,7 +762,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); diff --git a/queue-6.12/series b/queue-6.12/series index 319197bac6..9d4feddaf5 100644 --- a/queue-6.12/series +++ b/queue-6.12/series @@ -247,3 +247,14 @@ xdp-produce-a-warning-when-calculated-tailroom-is-ne.patch selftest-arm64-fix-sve2p1_sigill-to-hwcap-test.patch 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 +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