From 9d0709650f192fc010e9c22a7e243976d41047ae Mon Sep 17 00:00:00 2001 From: Saikrishna Arcot Date: Mon, 6 Apr 2026 17:25:04 -0700 Subject: [PATCH] Fix apparmor vulnerabilities (QID-45097) CVEs: CVE-2026-23268, CVE-2026-23269, CVE-2026-23403, CVE-2026-23404, CVE-2026-23405, CVE-2026-23406, CVE-2026-23407, CVE-2026-23408, CVE-2026-23409, CVE-2026-23410, CVE-2026-23411 Signed-off-by: Saikrishna Arcot --- ...1-apparmor-fix-kernel-doc-complaints.patch | 66 ++ ...nel-doc-warnings-in-apparmor-policy..patch | 87 +++ ...e-DFA-start-states-are-in-bounds-in-.patch | 64 ++ ...mor-fix-memory-leak-in-verify_header.patch | 40 + ...-recursive-profile-removal-with-iter.patch | 86 +++ ...it-the-number-of-levels-of-policy-na.patch | 52 ++ ...e-effect-bug-in-match_char-macro-usa.patch | 124 +++ ...sing-bounds-check-on-DEFAULT-table-i.patch | 92 +++ ...ble-free-of-ns_name-in-aa_replace_pr.patch | 48 ++ ...rivileged-local-user-can-do-privileg.patch | 190 +++++ ...x-differential-encoding-verification.patch | 92 +++ ...rmor-fix-race-on-rawdata-dereference.patch | 447 +++++++++++ ...e-between-freeing-data-and-fs-access.patch | 721 ++++++++++++++++++ patch/series | 15 + 14 files changed, 2124 insertions(+) create mode 100644 patch/qsa-2026-apparmor/0001-apparmor-fix-kernel-doc-complaints.patch create mode 100644 patch/qsa-2026-apparmor/0002-apparmor-Fix-kernel-doc-warnings-in-apparmor-policy..patch create mode 100644 patch/qsa-2026-apparmor/0003-apparmor-validate-DFA-start-states-are-in-bounds-in-.patch create mode 100644 patch/qsa-2026-apparmor/0004-apparmor-fix-memory-leak-in-verify_header.patch create mode 100644 patch/qsa-2026-apparmor/0005-apparmor-replace-recursive-profile-removal-with-iter.patch create mode 100644 patch/qsa-2026-apparmor/0006-apparmor-fix-limit-the-number-of-levels-of-policy-na.patch create mode 100644 patch/qsa-2026-apparmor/0007-apparmor-fix-side-effect-bug-in-match_char-macro-usa.patch create mode 100644 patch/qsa-2026-apparmor/0008-apparmor-fix-missing-bounds-check-on-DEFAULT-table-i.patch create mode 100644 patch/qsa-2026-apparmor/0009-apparmor-Fix-double-free-of-ns_name-in-aa_replace_pr.patch create mode 100644 patch/qsa-2026-apparmor/0010-apparmor-fix-unprivileged-local-user-can-do-privileg.patch create mode 100644 patch/qsa-2026-apparmor/0011-apparmor-fix-differential-encoding-verification.patch create mode 100644 patch/qsa-2026-apparmor/0012-apparmor-fix-race-on-rawdata-dereference.patch create mode 100644 patch/qsa-2026-apparmor/0013-apparmor-fix-race-between-freeing-data-and-fs-access.patch diff --git a/patch/qsa-2026-apparmor/0001-apparmor-fix-kernel-doc-complaints.patch b/patch/qsa-2026-apparmor/0001-apparmor-fix-kernel-doc-complaints.patch new file mode 100644 index 000000000..ac76e9a80 --- /dev/null +++ b/patch/qsa-2026-apparmor/0001-apparmor-fix-kernel-doc-complaints.patch @@ -0,0 +1,66 @@ +From 931da57ad18798616be0a085957cc9d345a79ddc Mon Sep 17 00:00:00 2001 +From: Randy Dunlap +Date: Mon, 2 Jan 2023 12:45:12 -0800 +Subject: [PATCH 01/13] apparmor: fix kernel-doc complaints + +commit 76862af5d1add618f0cc99868bc729925f9551d2 upstream. + +Correct kernel-doc notation to placate kernel-doc W=1 warnings: + +security/apparmor/policy.c:439: warning: duplicate section name 'Return' +security/apparmor/secid.c:57: warning: Cannot understand * +security/apparmor/file.c:174: warning: cannot understand function prototype: 'struct aa_perms default_perms = ' + +Signed-off-by: Randy Dunlap +Cc: John Johansen +Cc: John Johansen +Cc: apparmor@lists.ubuntu.com +Cc: Paul Moore +Cc: James Morris +Cc: "Serge E. Hallyn" +Signed-off-by: John Johansen +[bwh: Backported to 6.1: drop inapplicable changes] +Signed-off-by: Ben Hutchings +--- + security/apparmor/policy.c | 3 +-- + security/apparmor/secid.c | 3 +-- + 2 files changed, 2 insertions(+), 4 deletions(-) + +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index 4ee5a450d118..a49d76192d4c 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -715,7 +715,7 @@ bool aa_current_policy_admin_capable(struct aa_ns *ns) + /** + * aa_may_manage_policy - can the current task manage policy + * @label: label to check if it can manage policy +- * @op: the policy manipulation operation being done ++ * @mask: contains the policy manipulation operation being done + * + * Returns: 0 if the task is allowed to manipulate policy else error + */ +@@ -770,7 +770,6 @@ static struct aa_profile *__list_lookup_parent(struct list_head *lh, + * __replace_profile - replace @old with @new on a list + * @old: profile to be replaced (NOT NULL) + * @new: profile to replace @old with (NOT NULL) +- * @share_proxy: transfer @old->proxy to @new + * + * Will duplicate and refcount elements that @new inherits from @old + * and will inherit @old children. +diff --git a/security/apparmor/secid.c b/security/apparmor/secid.c +index 24a0e23f1b2b..83d3d1e6d9dc 100644 +--- a/security/apparmor/secid.c ++++ b/security/apparmor/secid.c +@@ -53,8 +53,7 @@ void aa_secid_update(u32 secid, struct aa_label *label) + xa_unlock_irqrestore(&aa_secids, flags); + } + +-/** +- * ++/* + * see label for inverse aa_label_to_secid + */ + struct aa_label *aa_secid_to_label(u32 secid) +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0002-apparmor-Fix-kernel-doc-warnings-in-apparmor-policy..patch b/patch/qsa-2026-apparmor/0002-apparmor-Fix-kernel-doc-warnings-in-apparmor-policy..patch new file mode 100644 index 000000000..795f0c591 --- /dev/null +++ b/patch/qsa-2026-apparmor/0002-apparmor-Fix-kernel-doc-warnings-in-apparmor-policy..patch @@ -0,0 +1,87 @@ +From 609c779bbec3bf8cfb91535b065495a8ec91d1a9 Mon Sep 17 00:00:00 2001 +From: Gaosheng Cui +Date: Sun, 25 Jun 2023 09:13:49 +0800 +Subject: [PATCH 02/13] apparmor: Fix kernel-doc warnings in apparmor/policy.c + +commit 25ff0ff2d6286928dc516c74b879809c691c2dd8 upstream. + +Fix kernel-doc warnings: + +security/apparmor/policy.c:294: warning: Function parameter or +member 'proxy' not described in 'aa_alloc_profile' +security/apparmor/policy.c:785: warning: Function parameter or +member 'label' not described in 'aa_policy_view_capable' +security/apparmor/policy.c:785: warning: Function parameter or +member 'ns' not described in 'aa_policy_view_capable' +security/apparmor/policy.c:847: warning: Function parameter or +member 'ns' not described in 'aa_may_manage_policy' +security/apparmor/policy.c:964: warning: Function parameter or +member 'hname' not described in '__lookup_replace' +security/apparmor/policy.c:964: warning: Function parameter or +member 'info' not described in '__lookup_replace' +security/apparmor/policy.c:964: warning: Function parameter or +member 'noreplace' not described in '__lookup_replace' +security/apparmor/policy.c:964: warning: Function parameter or +member 'ns' not described in '__lookup_replace' +security/apparmor/policy.c:964: warning: Function parameter or +member 'p' not described in '__lookup_replace' + +Signed-off-by: Gaosheng Cui +Signed-off-by: John Johansen +Signed-off-by: Ben Hutchings +--- + security/apparmor/policy.c | 17 ++++++++++------- + 1 file changed, 10 insertions(+), 7 deletions(-) + +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index a49d76192d4c..e51af2017888 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -251,6 +251,7 @@ void aa_free_profile(struct aa_profile *profile) + /** + * aa_alloc_profile - allocate, initialize and return a new profile + * @hname: name of the profile (NOT NULL) ++ * @proxy: proxy to use OR null if to allocate a new one + * @gfp: allocation type + * + * Returns: refcount profile or NULL on failure +@@ -650,8 +651,9 @@ static int policy_ns_capable(struct aa_label *label, + + /** + * aa_policy_view_capable - check if viewing policy in at @ns is allowed +- * label: label that is trying to view policy in ns +- * ns: namespace being viewed by @label (may be NULL if @label's ns) ++ * @label: label that is trying to view policy in ns ++ * @ns: namespace being viewed by @label (may be NULL if @label's ns) ++ * + * Returns: true if viewing policy is allowed + * + * If @ns is NULL then the namespace being viewed is assumed to be the +@@ -715,6 +717,7 @@ bool aa_current_policy_admin_capable(struct aa_ns *ns) + /** + * aa_may_manage_policy - can the current task manage policy + * @label: label to check if it can manage policy ++ * @ns: namespace being managed by @label (may be NULL if @label's ns) + * @mask: contains the policy manipulation operation being done + * + * Returns: 0 if the task is allowed to manipulate policy else error +@@ -826,11 +829,11 @@ static void __replace_profile(struct aa_profile *old, struct aa_profile *new) + + /** + * __lookup_replace - lookup replacement information for a profile +- * @ns - namespace the lookup occurs in +- * @hname - name of profile to lookup +- * @noreplace - true if not replacing an existing profile +- * @p - Returns: profile to be replaced +- * @info - Returns: info string on why lookup failed ++ * @ns: namespace the lookup occurs in ++ * @hname: name of profile to lookup ++ * @noreplace: true if not replacing an existing profile ++ * @p: Returns - profile to be replaced ++ * @info: Returns - info string on why lookup failed + * + * Returns: profile to replace (no ref) on success else ptr error + */ +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0003-apparmor-validate-DFA-start-states-are-in-bounds-in-.patch b/patch/qsa-2026-apparmor/0003-apparmor-validate-DFA-start-states-are-in-bounds-in-.patch new file mode 100644 index 000000000..87860ba6f --- /dev/null +++ b/patch/qsa-2026-apparmor/0003-apparmor-validate-DFA-start-states-are-in-bounds-in-.patch @@ -0,0 +1,64 @@ +From 05c8275824c0397544a6ac7c90ccda8eff4cef60 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 15 Jan 2026 15:30:50 +0100 +Subject: [PATCH 03/13] apparmor: validate DFA start states are in bounds in + unpack_pdb + +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. + +Reported-by: Qualys Security Advisory +Fixes: ad5ff3db53c6 ("AppArmor: Add ability to load extended policy") +Signed-off-by: Massimiliano Pellizzer +Signed-off-by: John Johansen +[ Salvatore Bonaccorso: Apply backported change as proposed by Qualys +Security Advisory Team for 6.1.y series without ad596ea74e74 ("apparmor: +group dfa policydb unpacking") introduced in v6.2-rc1. ] +--- + security/apparmor/policy_unpack.c | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/security/apparmor/policy_unpack.c b/security/apparmor/policy_unpack.c +index 17601235ff98..9fd5b76893d3 100644 +--- a/security/apparmor/policy_unpack.c ++++ b/security/apparmor/policy_unpack.c +@@ -827,6 +827,13 @@ static struct aa_profile *unpack_profile(struct aa_ext *e, char **ns_name) + if (!aa_unpack_u32(e, &profile->policy.start[0], "start")) + /* default start state */ + profile->policy.start[0] = DFA_START; ++ ++ const size_t state_count = profile->policy.dfa->tables[YYTD_ID_BASE]->td_lolen; ++ if (profile->policy.start[0] >= state_count) { ++ info = "invalid policy dfa start state"; ++ goto fail; ++ } ++ + /* setup class index */ + for (i = AA_CLASS_FILE; i <= AA_CLASS_LAST; i++) { + profile->policy.start[i] = +@@ -850,6 +857,12 @@ static struct aa_profile *unpack_profile(struct aa_ext *e, char **ns_name) + if (!aa_unpack_u32(e, &profile->file.start, "dfa_start")) + /* default start state */ + profile->file.start = DFA_START; ++ ++ const size_t state_count = profile->file.dfa->tables[YYTD_ID_BASE]->td_lolen; ++ if (profile->file.start >= state_count) { ++ info = "invalid file dfa start state"; ++ goto fail; ++ } + } else if (profile->policy.dfa && + profile->policy.start[AA_CLASS_FILE]) { + profile->file.dfa = aa_get_dfa(profile->policy.dfa); +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0004-apparmor-fix-memory-leak-in-verify_header.patch b/patch/qsa-2026-apparmor/0004-apparmor-fix-memory-leak-in-verify_header.patch new file mode 100644 index 000000000..5f1a5c932 --- /dev/null +++ b/patch/qsa-2026-apparmor/0004-apparmor-fix-memory-leak-in-verify_header.patch @@ -0,0 +1,40 @@ +From 1b595f1453b0229bb0e5784c72e36310bebd6069 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Tue, 20 Jan 2026 15:24:04 +0100 +Subject: [PATCH 04/13] apparmor: fix memory leak in verify_header + +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 +--- + security/apparmor/policy_unpack.c | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/security/apparmor/policy_unpack.c b/security/apparmor/policy_unpack.c +index 9fd5b76893d3..b855522c729d 100644 +--- a/security/apparmor/policy_unpack.c ++++ b/security/apparmor/policy_unpack.c +@@ -955,7 +955,6 @@ static int verify_header(struct aa_ext *e, int required, const char **ns) + { + int error = -EPROTONOSUPPORT; + const char *name = NULL; +- *ns = NULL; + + /* get the interface version */ + if (!aa_unpack_u32(e, &e->version, "version")) { +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0005-apparmor-replace-recursive-profile-removal-with-iter.patch b/patch/qsa-2026-apparmor/0005-apparmor-replace-recursive-profile-removal-with-iter.patch new file mode 100644 index 000000000..993e17d5c --- /dev/null +++ b/patch/qsa-2026-apparmor/0005-apparmor-replace-recursive-profile-removal-with-iter.patch @@ -0,0 +1,86 @@ +From 07dd632e17a7204094a3041a722f3c01c70fba65 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Tue, 13 Jan 2026 09:09:43 +0100 +Subject: [PATCH 05/13] apparmor: replace recursive profile removal with + iterative approach + +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 +--- + security/apparmor/policy.c | 30 +++++++++++++++++++++++++++--- + 1 file changed, 27 insertions(+), 3 deletions(-) + +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index e51af2017888..27708f7ff33a 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -146,19 +146,43 @@ static void __list_remove_profile(struct aa_profile *profile) + } + + /** +- * __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); +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0006-apparmor-fix-limit-the-number-of-levels-of-policy-na.patch b/patch/qsa-2026-apparmor/0006-apparmor-fix-limit-the-number-of-levels-of-policy-na.patch new file mode 100644 index 000000000..655a8d3d3 --- /dev/null +++ b/patch/qsa-2026-apparmor/0006-apparmor-fix-limit-the-number-of-levels-of-policy-na.patch @@ -0,0 +1,52 @@ +From 3955c80396688e4f3d0eeee04884bed375cbd733 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Tue, 3 Mar 2026 11:08:02 -0800 +Subject: [PATCH 06/13] apparmor: fix: limit the number of levels of policy + namespaces + +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 +--- + security/apparmor/include/policy_ns.h | 2 ++ + security/apparmor/policy_ns.c | 2 ++ + 2 files changed, 4 insertions(+) + +diff --git a/security/apparmor/include/policy_ns.h b/security/apparmor/include/policy_ns.h +index 33d665516fc1..dabb69bc87e0 100644 +--- 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 +diff --git a/security/apparmor/policy_ns.c b/security/apparmor/policy_ns.c +index 78700d94b453..b7d9d5376aac 100644 +--- a/security/apparmor/policy_ns.c ++++ b/security/apparmor/policy_ns.c +@@ -262,6 +262,8 @@ static struct aa_ns *__aa_create_ns(struct aa_ns *parent, const char *name, + 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); +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0007-apparmor-fix-side-effect-bug-in-match_char-macro-usa.patch b/patch/qsa-2026-apparmor/0007-apparmor-fix-side-effect-bug-in-match_char-macro-usa.patch new file mode 100644 index 000000000..822db7ef6 --- /dev/null +++ b/patch/qsa-2026-apparmor/0007-apparmor-fix-side-effect-bug-in-match_char-macro-usa.patch @@ -0,0 +1,124 @@ +From bfb484fe91951894e6872e61bb8e85656fb2ada5 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 29 Jan 2026 17:08:25 +0100 +Subject: [PATCH 07/13] apparmor: fix side-effect bug in match_char() macro + usage + +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 +--- + security/apparmor/match.c | 30 ++++++++++++++++++++---------- + 1 file changed, 20 insertions(+), 10 deletions(-) + +diff --git a/security/apparmor/match.c b/security/apparmor/match.c +index 0e683ee323e3..ae07fe81b47f 100644 +--- a/security/apparmor/match.c ++++ b/security/apparmor/match.c +@@ -452,13 +452,18 @@ unsigned int aa_dfa_match_len(struct aa_dfa *dfa, unsigned int start, + 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; +@@ -493,13 +498,18 @@ unsigned int aa_dfa_match(struct aa_dfa *dfa, unsigned int start, + /* 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; +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0008-apparmor-fix-missing-bounds-check-on-DEFAULT-table-i.patch b/patch/qsa-2026-apparmor/0008-apparmor-fix-missing-bounds-check-on-DEFAULT-table-i.patch new file mode 100644 index 000000000..24544025f --- /dev/null +++ b/patch/qsa-2026-apparmor/0008-apparmor-fix-missing-bounds-check-on-DEFAULT-table-i.patch @@ -0,0 +1,92 @@ +From 4642a6ee16ee77b9a2608b3b58d0bb088d5fa7b6 Mon Sep 17 00:00:00 2001 +From: Massimiliano Pellizzer +Date: Thu, 29 Jan 2026 16:51:11 +0100 +Subject: [PATCH 08/13] apparmor: fix missing bounds check on DEFAULT table in + verify_dfa() + +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 +--- + security/apparmor/match.c | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/security/apparmor/match.c b/security/apparmor/match.c +index ae07fe81b47f..f70e5f769ef0 100644 +--- a/security/apparmor/match.c ++++ b/security/apparmor/match.c +@@ -204,9 +204,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; +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0009-apparmor-Fix-double-free-of-ns_name-in-aa_replace_pr.patch b/patch/qsa-2026-apparmor/0009-apparmor-Fix-double-free-of-ns_name-in-aa_replace_pr.patch new file mode 100644 index 000000000..676ff7136 --- /dev/null +++ b/patch/qsa-2026-apparmor/0009-apparmor-Fix-double-free-of-ns_name-in-aa_replace_pr.patch @@ -0,0 +1,48 @@ +From 08e860d626176663f5f53e839fc61e0e11b32728 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Wed, 10 Sep 2025 06:22:17 -0700 +Subject: [PATCH 09/13] apparmor: Fix double free of ns_name in + aa_replace_profiles() + +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: 04dc715e24d08 ("apparmor: audit policy ns specified in policy load") +Reported-by: Qualys Security Advisory +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +--- + security/apparmor/policy.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index 27708f7ff33a..c99da66422c2 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -960,6 +960,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, + goto fail; + } + ns_name = ent->ns_name; ++ ent->ns_name = NULL; + } else + count++; + } +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0010-apparmor-fix-unprivileged-local-user-can-do-privileg.patch b/patch/qsa-2026-apparmor/0010-apparmor-fix-unprivileged-local-user-can-do-privileg.patch new file mode 100644 index 000000000..5c4eb5965 --- /dev/null +++ b/patch/qsa-2026-apparmor/0010-apparmor-fix-unprivileged-local-user-can-do-privileg.patch @@ -0,0 +1,190 @@ +From ef65a63af390089bf9e2e40ba37aa27cccec51f8 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Fri, 7 Nov 2025 08:36:04 -0800 +Subject: [PATCH 10/13] apparmor: fix unprivileged local user can do privileged + policy management + +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: c88d4c7b049e8 ("AppArmor: core policy routines") +Reported-by: Qualys Security Advisory +Tested-by: Salvatore Bonaccorso +Reviewed-by: Georgia Garcia +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +[bwh: Backported to 6.1: aa_may_manage_policy() does not take a subj_cred + parameter. Do not add one but use current_cred(), which is what all its + callers pass in later kernel versions.] +Signed-off-by: Ben Hutchings +--- + security/apparmor/apparmorfs.c | 16 ++++++++------ + security/apparmor/include/policy.h | 2 +- + security/apparmor/policy.c | 35 +++++++++++++++++++++++++++++- + 3 files changed, 44 insertions(+), 9 deletions(-) + +diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c +index ce7b2f43c319..82297eef57ea 100644 +--- a/security/apparmor/apparmorfs.c ++++ b/security/apparmor/apparmorfs.c +@@ -412,7 +412,8 @@ static struct aa_loaddata *aa_simple_write_to_buffer(const char __user *userbuf, + } + + 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, const char __user *buf, size_t size, + /* high level check about policy management - fine grained in + * below after unpack + */ +- error = aa_may_manage_policy(label, ns, mask); ++ error = aa_may_manage_policy(label, ns, ocred, mask); + if (error) + goto end_section; + +@@ -444,7 +445,8 @@ 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); +- 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 file *f, const char __user *buf, + { + 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; +@@ -486,7 +488,7 @@ static ssize_t profile_remove(struct file *f, const char __user *buf, + /* high level check about policy management - fine grained in + * below after unpack + */ +- error = aa_may_manage_policy(label, ns, AA_MAY_REMOVE_POLICY); ++ error = aa_may_manage_policy(label, ns, f->f_cred, AA_MAY_REMOVE_POLICY); + if (error) + goto out; + +@@ -1799,7 +1801,7 @@ static int ns_mkdir_op(struct user_namespace *mnt_userns, struct inode *dir, + int error; + + label = begin_current_label_crit_section(); +- error = aa_may_manage_policy(label, NULL, AA_MAY_LOAD_POLICY); ++ error = aa_may_manage_policy(label, NULL, NULL, AA_MAY_LOAD_POLICY); + end_current_label_crit_section(label); + if (error) + return error; +@@ -1848,7 +1850,7 @@ static int ns_rmdir_op(struct inode *dir, struct dentry *dentry) + int error; + + label = begin_current_label_crit_section(); +- error = aa_may_manage_policy(label, NULL, AA_MAY_LOAD_POLICY); ++ error = aa_may_manage_policy(label, NULL, NULL, AA_MAY_LOAD_POLICY); + end_current_label_crit_section(label); + if (error) + return error; +diff --git a/security/apparmor/include/policy.h b/security/apparmor/include/policy.h +index 639b5b248e63..3f776f5e8de4 100644 +--- a/security/apparmor/include/policy.h ++++ b/security/apparmor/include/policy.h +@@ -308,7 +308,7 @@ static inline int AUDIT_MODE(struct aa_profile *profile) + bool aa_policy_view_capable(struct aa_label *label, struct aa_ns *ns); + bool aa_policy_admin_capable(struct aa_label *label, struct aa_ns *ns); + int aa_may_manage_policy(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); + +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index c99da66422c2..a23c374ca88f 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -738,15 +738,43 @@ bool aa_current_policy_admin_capable(struct aa_ns *ns) + 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 + * @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(struct aa_label *label, struct aa_ns *ns, u32 mask) ++int aa_may_manage_policy(struct aa_label *label, struct aa_ns *ns, const struct cred *ocred, ++ u32 mask) + { + const char *op; + +@@ -762,6 +790,11 @@ int aa_may_manage_policy(struct aa_label *label, struct aa_ns *ns, u32 mask) + return audit_policy(label, op, NULL, NULL, "policy_locked", + -EACCES); + ++ if (ocred && !is_subset_of_obj_privilege(current_cred(), label, ocred)) ++ return audit_policy(label, op, NULL, NULL, ++ "not privileged for target profile", ++ -EACCES); ++ + if (!aa_policy_admin_capable(label, ns)) + return audit_policy(label, op, NULL, NULL, "not policy admin", + -EACCES); +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0011-apparmor-fix-differential-encoding-verification.patch b/patch/qsa-2026-apparmor/0011-apparmor-fix-differential-encoding-verification.patch new file mode 100644 index 000000000..a9e745444 --- /dev/null +++ b/patch/qsa-2026-apparmor/0011-apparmor-fix-differential-encoding-verification.patch @@ -0,0 +1,92 @@ +From 396e512aaf72adb6634d54fb7d1a11d9f0f5976e Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Fri, 17 Oct 2025 01:53:00 -0700 +Subject: [PATCH 11/13] apparmor: fix differential encoding verification + +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 +--- + security/apparmor/include/match.h | 1 + + security/apparmor/match.c | 23 +++++++++++++++++++---- + 2 files changed, 20 insertions(+), 4 deletions(-) + +diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h +index 29306ec87fd1..611ae908469b 100644 +--- a/security/apparmor/include/match.h ++++ b/security/apparmor/include/match.h +@@ -190,6 +190,7 @@ static inline void aa_put_dfa(struct aa_dfa *dfa) + #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) +diff --git a/security/apparmor/match.c b/security/apparmor/match.c +index f70e5f769ef0..8972d1b57b7a 100644 +--- a/security/apparmor/match.c ++++ b/security/apparmor/match.c +@@ -246,16 +246,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; + +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0012-apparmor-fix-race-on-rawdata-dereference.patch b/patch/qsa-2026-apparmor/0012-apparmor-fix-race-on-rawdata-dereference.patch new file mode 100644 index 000000000..e1ed2e50e --- /dev/null +++ b/patch/qsa-2026-apparmor/0012-apparmor-fix-race-on-rawdata-dereference.patch @@ -0,0 +1,447 @@ +From c7685c9d7bcb138c869043d6846ff83bd538b18e Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Tue, 24 Feb 2026 10:20:02 -0800 +Subject: [PATCH 12/13] apparmor: fix race on rawdata dereference +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +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: Ben Hutchings +--- + 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(-) + +diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c +index 82297eef57ea..ee5ba542bc9c 100644 +--- a/security/apparmor/apparmorfs.c ++++ b/security/apparmor/apparmorfs.c +@@ -79,7 +79,7 @@ static void rawdata_f_data_free(struct rawdata_f_data *private) + 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_write_to_buffer(const char __user *userbuf, + + 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, const char __user *buf, size_t size, + 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); +@@ -502,7 +506,7 @@ static ssize_t profile_remove(struct file *f, const char __user *buf, + 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); +@@ -1226,18 +1230,17 @@ static const struct file_operations seq_rawdata_ ##NAME ##_fops = { \ + 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; +@@ -1248,7 +1251,7 @@ static int seq_rawdata_release(struct inode *inode, struct file *file) + 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); + } +@@ -1370,9 +1373,8 @@ static int rawdata_open(struct inode *inode, struct file *file) + 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); +@@ -1397,7 +1399,7 @@ static int rawdata_open(struct inode *inode, struct file *file) + return error; + + fail_private_alloc: +- aa_put_loaddata(loaddata); ++ aa_put_i_loaddata(loaddata); + return error; + } + +@@ -1414,9 +1416,9 @@ static void remove_rawdata_dents(struct aa_loaddata *rawdata) + + 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); + } + } + } +@@ -1455,18 +1457,21 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata) + 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) { +@@ -1474,6 +1479,7 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata) + rawdata, &seq_rawdata_hash_fops); + if (IS_ERR(dent)) + goto fail; ++ aa_get_i_loaddata(rawdata); + rawdata->dents[AAFS_LOADDATA_HASH] = dent; + } + +@@ -1482,24 +1488,25 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata) + &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 */ +diff --git a/security/apparmor/include/policy_unpack.h b/security/apparmor/include/policy_unpack.h +index e89b701447bc..e06031c6d42e 100644 +--- a/security/apparmor/include/policy_unpack.h ++++ b/security/apparmor/include/policy_unpack.h +@@ -85,17 +85,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]; +@@ -117,52 +129,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); +diff --git a/security/apparmor/policy.c b/security/apparmor/policy.c +index a23c374ca88f..08ee70b61f74 100644 +--- a/security/apparmor/policy.c ++++ b/security/apparmor/policy.c +@@ -266,7 +266,7 @@ void aa_free_profile(struct aa_profile *profile) + } + + kfree_sensitive(profile->hash); +- aa_put_loaddata(profile->rawdata); ++ aa_put_profile_loaddata(profile->rawdata); + aa_label_destroy(&profile->label); + + kfree_sensitive(profile); +@@ -966,7 +966,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, + 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) +@@ -1018,10 +1018,10 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, + 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; + } +@@ -1033,7 +1033,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, + struct aa_policy *policy; + + 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); +@@ -1149,7 +1149,7 @@ ssize_t aa_replace_profiles(struct aa_ns *policy_ns, struct aa_label *label, + + out: + aa_put_ns(ns); +- aa_put_loaddata(udata); ++ aa_put_profile_loaddata(udata); + kfree(ns_name); + + if (error) +diff --git a/security/apparmor/policy_unpack.c b/security/apparmor/policy_unpack.c +index b855522c729d..ea387dc38794 100644 +--- a/security/apparmor/policy_unpack.c ++++ b/security/apparmor/policy_unpack.c +@@ -112,34 +112,47 @@ bool aa_rawdata_eq(struct aa_loaddata *l, struct aa_loaddata *r) + 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); + } + } +@@ -157,6 +170,7 @@ struct aa_loaddata *aa_loaddata_alloc(size_t size) + return ERR_PTR(-ENOMEM); + } + kref_init(&d->count); ++ kref_init(&d->pcount); + INIT_LIST_HEAD(&d->list); + + return d; +-- +2.53.0 + diff --git a/patch/qsa-2026-apparmor/0013-apparmor-fix-race-between-freeing-data-and-fs-access.patch b/patch/qsa-2026-apparmor/0013-apparmor-fix-race-between-freeing-data-and-fs-access.patch new file mode 100644 index 000000000..793604e6e --- /dev/null +++ b/patch/qsa-2026-apparmor/0013-apparmor-fix-race-between-freeing-data-and-fs-access.patch @@ -0,0 +1,721 @@ +From 4cb51685a1e99bd559f4e96fac35cba2218e0211 Mon Sep 17 00:00:00 2001 +From: John Johansen +Date: Sun, 1 Mar 2026 16:10:51 -0800 +Subject: [PATCH 13/13] apparmor: fix race between freeing data and fs + accessing it +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +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 +Reviewed-by: Georgia Garcia +Reviewed-by: Maxime Bélair +Reviewed-by: Cengiz Can +Signed-off-by: John Johansen +[Salvatore Bonaccorso: Backported to 6.1: Fix context for version not +having e44a4dc4b36c ("apparmor: switch SECURITY_APPARMOR_HASH from sha1 +to sha256")] +Signed-off-by: Salvatore Bonaccorso +--- + 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(-) + +diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c +index ee5ba542bc9c..a146f18a5277 100644 +--- 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_file *seq, struct dentry *dentry) + 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 *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 inode *dir, struct dentry *dentry, + * 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 char *name, umode_t mode, + goto fail_dentry; + inode_unlock(dir); + ++ if (data) ++ aa_get_common_ref(data); ++ + return dentry; + + fail_dentry: +@@ -323,7 +395,8 @@ static struct dentry *aafs_create(const char *name, umode_t mode, + * 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 @@ static ssize_t policy_update(u32 mask, const char __user *buf, size_t size, + 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_fs_profile_load = { + 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 file *f, const char __user *buf, + 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 +@@ -575,7 +648,7 @@ static int ns_revision_open(struct inode *inode, struct file *file) + 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; +@@ -1052,7 +1125,7 @@ static const struct file_operations seq_profile_ ##NAME ##_fops = { \ + 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) { +@@ -1230,7 +1303,7 @@ static const struct file_operations seq_rawdata_ ##NAME ##_fops = { \ + 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) +@@ -1373,7 +1446,7 @@ static int rawdata_open(struct inode *inode, struct file *file) + 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; + +@@ -1418,7 +1491,6 @@ static void remove_rawdata_dents(struct aa_loaddata *rawdata) + if (!IS_ERR_OR_NULL(rawdata->dents[i])) { + aafs_remove(rawdata->dents[i]); + rawdata->dents[i] = NULL; +- aa_put_i_loaddata(rawdata); + } + } + } +@@ -1457,45 +1529,41 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata) + 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; + +@@ -1506,7 +1574,6 @@ int __aa_fs_create_rawdata(struct aa_ns *ns, struct aa_loaddata *rawdata) + + fail: + remove_rawdata_dents(rawdata); +- aa_put_i_loaddata(rawdata); + return PTR_ERR(dent); + } + #endif /* CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */ +@@ -1530,13 +1597,10 @@ void __aafs_profile_rmdir(struct aa_profile *profile) + __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; + } + } +@@ -1566,14 +1630,7 @@ static struct dentry *create_profile_file(struct dentry *dir, const char *name, + 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 +@@ -1624,7 +1681,8 @@ static const char *rawdata_get_link_base(struct dentry *dentry, + 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; +@@ -1757,27 +1815,24 @@ int __aafs_profile_mkdir(struct aa_profile *profile, struct dentry *parent) + 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 */ +@@ -1813,7 +1868,7 @@ static int ns_mkdir_op(struct user_namespace *mnt_userns, struct inode *dir, + 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 +@@ -1862,7 +1917,7 @@ static int ns_rmdir_op(struct inode *dir, struct dentry *dentry) + 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. +@@ -1932,27 +1987,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; +@@ -1977,40 +2011,40 @@ static int __aafs_ns_mkdir_entries(struct aa_ns *ns, struct dentry *dir) + 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; +diff --git a/security/apparmor/include/label.h b/security/apparmor/include/label.h +index 860484c6f99a..9887bfbab0f3 100644 +--- 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; +@@ -372,7 +372,7 @@ int aa_label_match(struct aa_profile *profile, struct aa_label *label, + */ + 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; +@@ -381,7 +381,7 @@ static inline struct aa_label *__aa_get_label(struct aa_label *l) + 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; + } +@@ -401,7 +401,7 @@ static inline struct aa_label *aa_get_label_rcu(struct aa_label __rcu **l) + 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; +@@ -441,7 +441,7 @@ static inline struct aa_label *aa_get_newest_label(struct aa_label *l) + 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); + } + + +@@ -451,7 +451,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; + } +@@ -459,7 +459,7 @@ static inline struct aa_proxy *aa_get_proxy(struct aa_proxy *proxy) + 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); +diff --git a/security/apparmor/include/lib.h b/security/apparmor/include/lib.h +index fd57e9ffc139..8901822224a4 100644 +--- a/security/apparmor/include/lib.h ++++ b/security/apparmor/include/lib.h +@@ -69,6 +69,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 +diff --git a/security/apparmor/include/policy.h b/security/apparmor/include/policy.h +index 3f776f5e8de4..f7e069baa7c3 100644 +--- a/security/apparmor/include/policy.h ++++ b/security/apparmor/include/policy.h +@@ -247,7 +247,7 @@ static inline unsigned int PROFILE_MEDIATES_AF(struct aa_profile *profile, + 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; + } +@@ -261,7 +261,7 @@ static inline struct aa_profile *aa_get_profile(struct aa_profile *p) + */ + 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; +@@ -281,7 +281,7 @@ static inline struct aa_profile *aa_get_profile_rcu(struct aa_profile __rcu **p) + 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; +@@ -294,7 +294,7 @@ static inline struct aa_profile *aa_get_profile_rcu(struct aa_profile __rcu **p) + 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) +diff --git a/security/apparmor/include/policy_unpack.h b/security/apparmor/include/policy_unpack.h +index e06031c6d42e..4d7be953724e 100644 +--- a/security/apparmor/include/policy_unpack.h ++++ b/security/apparmor/include/policy_unpack.h +@@ -106,7 +106,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; +@@ -141,7 +141,7 @@ aa_get_i_loaddata(struct aa_loaddata *data) + { + + if (data) +- kref_get(&(data->count)); ++ kref_get(&(data->count.count)); + return data; + } + +@@ -169,7 +169,7 @@ struct aa_loaddata *aa_loaddata_alloc(size_t size); + 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) +diff --git a/security/apparmor/label.c b/security/apparmor/label.c +index a67c5897ee25..c57c5d296b51 100644 +--- a/security/apparmor/label.c ++++ b/security/apparmor/label.c +@@ -52,7 +52,8 @@ static void free_proxy(struct aa_proxy *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 aa_label *label, gfp_t gfp) + + 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_head *head) + + 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 *label, int size, gfp_t gfp) + + 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; +diff --git a/security/apparmor/policy_unpack.c b/security/apparmor/policy_unpack.c +index ea387dc38794..0c4f6771ac8a 100644 +--- a/security/apparmor/policy_unpack.c ++++ b/security/apparmor/policy_unpack.c +@@ -122,7 +122,8 @@ static void do_loaddata_free(struct aa_loaddata *d) + + 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); + } +@@ -169,7 +170,8 @@ struct aa_loaddata *aa_loaddata_alloc(size_t size) + 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); + +-- +2.53.0 + diff --git a/patch/series b/patch/series index ebc3574e6..6cbe6ac82 100755 --- a/patch/series +++ b/patch/series @@ -237,6 +237,21 @@ cisco-npu-disable-other-bars.patch # https://github.com/sonic-net/sonic-buildimage/issues/20901 PCI-ASPM-Fix-link-state-exit-during-switch-upstream.patch +# QID-45097: Crackarmor fixes +qsa-2026-apparmor/0001-apparmor-fix-kernel-doc-complaints.patch +qsa-2026-apparmor/0002-apparmor-Fix-kernel-doc-warnings-in-apparmor-policy..patch +qsa-2026-apparmor/0003-apparmor-validate-DFA-start-states-are-in-bounds-in-.patch +qsa-2026-apparmor/0004-apparmor-fix-memory-leak-in-verify_header.patch +qsa-2026-apparmor/0005-apparmor-replace-recursive-profile-removal-with-iter.patch +qsa-2026-apparmor/0006-apparmor-fix-limit-the-number-of-levels-of-policy-na.patch +qsa-2026-apparmor/0007-apparmor-fix-side-effect-bug-in-match_char-macro-usa.patch +qsa-2026-apparmor/0008-apparmor-fix-missing-bounds-check-on-DEFAULT-table-i.patch +qsa-2026-apparmor/0009-apparmor-Fix-double-free-of-ns_name-in-aa_replace_pr.patch +qsa-2026-apparmor/0010-apparmor-fix-unprivileged-local-user-can-do-privileg.patch +qsa-2026-apparmor/0011-apparmor-fix-differential-encoding-verification.patch +qsa-2026-apparmor/0012-apparmor-fix-race-on-rawdata-dereference.patch +qsa-2026-apparmor/0013-apparmor-fix-race-between-freeing-data-and-fs-access.patch + # # ############################################################