]> git.ipfire.org Git - thirdparty/kernel/stable-queue.git/commitdiff
6.18-stable patches
authorGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 12 Mar 2026 19:58:58 +0000 (20:58 +0100)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 12 Mar 2026 19:58:58 +0000 (20:58 +0100)
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

12 files changed:
queue-6.18/apparmor-fix-differential-encoding-verification.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-memory-leak-in-verify_header.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-race-on-rawdata-dereference.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch [new file with mode: 0644]
queue-6.18/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch [new file with mode: 0644]
queue-6.18/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch [new file with mode: 0644]
queue-6.18/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch [new file with mode: 0644]
queue-6.18/series

diff --git a/queue-6.18/apparmor-fix-differential-encoding-verification.patch b/queue-6.18/apparmor-fix-differential-encoding-verification.patch
new file mode 100644 (file)
index 0000000..ecabe81
--- /dev/null
@@ -0,0 +1,90 @@
+From 39440b137546a3aa383cfdabc605fb73811b6093 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Fri, 17 Oct 2025 01:53:00 -0700
+Subject: apparmor: fix differential encoding verification
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 39440b137546a3aa383cfdabc605fb73811b6093 upstream.
+
+Differential encoding allows loops to be created if it is abused. To
+prevent this the unpack should verify that a diff-encode chain
+terminates.
+
+Unfortunately the differential encode verification had two bugs.
+
+1. it conflated states that had gone through check and already been
+   marked, with states that were currently being checked and marked.
+   This means that loops in the current chain being verified are treated
+   as a chain that has already been verified.
+
+2. the order bailout on already checked states compared current chain
+   check iterators j,k instead of using the outer loop iterator i.
+   Meaning a step backwards in states in the current chain verification
+   was being mistaken for moving to an already verified state.
+
+Move to a double mark scheme where already verified states get a
+different mark, than the current chain being kept. This enables us
+to also drop the backwards verification check that was the cause of
+the second error as any already verified state is already marked.
+
+Fixes: 031dcc8f4e84 ("apparmor: dfa add support for state differential encoding")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/include/match.h |    1 +
+ security/apparmor/match.c         |   23 +++++++++++++++++++----
+ 2 files changed, 20 insertions(+), 4 deletions(-)
+
+--- a/security/apparmor/include/match.h
++++ b/security/apparmor/include/match.h
+@@ -185,6 +185,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.18/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch b/queue-6.18/apparmor-fix-double-free-of-ns_name-in-aa_replace_profiles.patch
new file mode 100644 (file)
index 0000000..a51ab39
--- /dev/null
@@ -0,0 +1,48 @@
+From 5df0c44e8f5f619d3beb871207aded7c78414502 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Wed, 10 Sep 2025 06:22:17 -0700
+Subject: apparmor: Fix double free of ns_name in aa_replace_profiles()
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 5df0c44e8f5f619d3beb871207aded7c78414502 upstream.
+
+if ns_name is NULL after
+1071         error = aa_unpack(udata, &lh, &ns_name);
+
+and if ent->ns_name contains an ns_name in
+1089                 } else if (ent->ns_name) {
+
+then ns_name is assigned the ent->ns_name
+1095                         ns_name = ent->ns_name;
+
+however ent->ns_name is freed at
+1262                 aa_load_ent_free(ent);
+
+and then again when freeing ns_name at
+1270         kfree(ns_name);
+
+Fix this by NULLing out ent->ns_name after it is transferred to ns_name
+
+Fixes: 145a0ef21c8e9 ("apparmor: fix blob compression when ns is forced on a policy load
+")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy.c |    1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -1149,6 +1149,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.18/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch b/queue-6.18/apparmor-fix-limit-the-number-of-levels-of-policy-namespaces.patch
new file mode 100644 (file)
index 0000000..1976ed5
--- /dev/null
@@ -0,0 +1,49 @@
+From 306039414932c80f8420695a24d4fe10c84ccfb2 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Tue, 3 Mar 2026 11:08:02 -0800
+Subject: apparmor: fix: limit the number of levels of policy namespaces
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 306039414932c80f8420695a24d4fe10c84ccfb2 upstream.
+
+Currently the number of policy namespaces is not bounded relying on
+the user namespace limit. However policy namespaces aren't strictly
+tied to user namespaces and it is possible to create them and nest
+them arbitrarily deep which can be used to exhaust system resource.
+
+Hard cap policy namespaces to the same depth as user namespaces.
+
+Fixes: c88d4c7b049e8 ("AppArmor: core policy routines")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Reviewed-by: Ryan Lee <ryan.lee@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/include/policy_ns.h |    2 ++
+ security/apparmor/policy_ns.c         |    2 ++
+ 2 files changed, 4 insertions(+)
+
+--- a/security/apparmor/include/policy_ns.h
++++ b/security/apparmor/include/policy_ns.h
+@@ -18,6 +18,8 @@
+ #include "label.h"
+ #include "policy.h"
++/* Match max depth of user namespaces */
++#define MAX_NS_DEPTH 32
+ /* struct aa_ns_acct - accounting of profiles in namespace
+  * @max_size: maximum space allowed for all profiles in namespace
+--- a/security/apparmor/policy_ns.c
++++ b/security/apparmor/policy_ns.c
+@@ -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.18/apparmor-fix-memory-leak-in-verify_header.patch b/queue-6.18/apparmor-fix-memory-leak-in-verify_header.patch
new file mode 100644 (file)
index 0000000..860a732
--- /dev/null
@@ -0,0 +1,40 @@
+From e38c55d9f834e5b848bfed0f5c586aaf45acb825 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Tue, 20 Jan 2026 15:24:04 +0100
+Subject: apparmor: fix memory leak in verify_header
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit e38c55d9f834e5b848bfed0f5c586aaf45acb825 upstream.
+
+The function sets `*ns = NULL` on every call, leaking the namespace
+string allocated in previous iterations when multiple profiles are
+unpacked. This also breaks namespace consistency checking since *ns
+is always NULL when the comparison is made.
+
+Remove the incorrect assignment.
+The caller (aa_unpack) initializes *ns to NULL once before the loop,
+which is sufficient.
+
+Fixes: dd51c8485763 ("apparmor: provide base for multiple profiles to be replaced at once")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy_unpack.c |    1 -
+ 1 file changed, 1 deletion(-)
+
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -1177,7 +1177,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.18/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch b/queue-6.18/apparmor-fix-missing-bounds-check-on-default-table-in-verify_dfa.patch
new file mode 100644 (file)
index 0000000..460a4f4
--- /dev/null
@@ -0,0 +1,91 @@
+From d352873bbefa7eb39995239d0b44ccdf8aaa79a4 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 29 Jan 2026 16:51:11 +0100
+Subject: apparmor: fix missing bounds check on DEFAULT table in verify_dfa()
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit d352873bbefa7eb39995239d0b44ccdf8aaa79a4 upstream.
+
+The verify_dfa() function only checks DEFAULT_TABLE bounds when the state
+is not differentially encoded.
+
+When the verification loop traverses the differential encoding chain,
+it reads k = DEFAULT_TABLE[j] and uses k as an array index without
+validation. A malformed DFA with DEFAULT_TABLE[j] >= state_count,
+therefore, causes both out-of-bounds reads and writes.
+
+[   57.179855] ==================================================================
+[   57.180549] BUG: KASAN: slab-out-of-bounds in verify_dfa+0x59a/0x660
+[   57.180904] Read of size 4 at addr ffff888100eadec4 by task su/993
+
+[   57.181554] CPU: 1 UID: 0 PID: 993 Comm: su Not tainted 6.19.0-rc7-next-20260127 #1 PREEMPT(lazy)
+[   57.181558] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
+[   57.181563] Call Trace:
+[   57.181572]  <TASK>
+[   57.181577]  dump_stack_lvl+0x5e/0x80
+[   57.181596]  print_report+0xc8/0x270
+[   57.181605]  ? verify_dfa+0x59a/0x660
+[   57.181608]  kasan_report+0x118/0x150
+[   57.181620]  ? verify_dfa+0x59a/0x660
+[   57.181623]  verify_dfa+0x59a/0x660
+[   57.181627]  aa_dfa_unpack+0x1610/0x1740
+[   57.181629]  ? __kmalloc_cache_noprof+0x1d0/0x470
+[   57.181640]  unpack_pdb+0x86d/0x46b0
+[   57.181647]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   57.181653]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   57.181656]  ? aa_unpack_nameX+0x1a8/0x300
+[   57.181659]  aa_unpack+0x20b0/0x4c30
+[   57.181662]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   57.181664]  ? stack_depot_save_flags+0x33/0x700
+[   57.181681]  ? kasan_save_track+0x4f/0x80
+[   57.181683]  ? kasan_save_track+0x3e/0x80
+[   57.181686]  ? __kasan_kmalloc+0x93/0xb0
+[   57.181688]  ? __kvmalloc_node_noprof+0x44a/0x780
+[   57.181693]  ? aa_simple_write_to_buffer+0x54/0x130
+[   57.181697]  ? policy_update+0x154/0x330
+[   57.181704]  aa_replace_profiles+0x15a/0x1dd0
+[   57.181707]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   57.181710]  ? __kvmalloc_node_noprof+0x44a/0x780
+[   57.181712]  ? aa_loaddata_alloc+0x77/0x140
+[   57.181715]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   57.181717]  ? _copy_from_user+0x2a/0x70
+[   57.181730]  policy_update+0x17a/0x330
+[   57.181733]  profile_replace+0x153/0x1a0
+[   57.181735]  ? rw_verify_area+0x93/0x2d0
+[   57.181740]  vfs_write+0x235/0xab0
+[   57.181745]  ksys_write+0xb0/0x170
+[   57.181748]  do_syscall_64+0x8e/0x660
+[   57.181762]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
+[   57.181765] RIP: 0033:0x7f6192792eb2
+
+Remove the MATCH_FLAG_DIFF_ENCODE condition to validate all DEFAULT_TABLE
+entries unconditionally.
+
+Fixes: 031dcc8f4e84 ("apparmor: dfa add support for state differential encoding")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/match.c |    5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+--- a/security/apparmor/match.c
++++ b/security/apparmor/match.c
+@@ -160,9 +160,10 @@ static int verify_dfa(struct aa_dfa *dfa
+       if (state_count == 0)
+               goto out;
+       for (i = 0; i < state_count; i++) {
+-              if (!(BASE_TABLE(dfa)[i] & MATCH_FLAG_DIFF_ENCODE) &&
+-                  (DEFAULT_TABLE(dfa)[i] >= state_count))
++              if (DEFAULT_TABLE(dfa)[i] >= state_count) {
++                      pr_err("AppArmor DFA default state out of bounds");
+                       goto out;
++              }
+               if (BASE_TABLE(dfa)[i] & MATCH_FLAGS_INVALID) {
+                       pr_err("AppArmor DFA state with invalid match flags");
+                       goto out;
diff --git a/queue-6.18/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch b/queue-6.18/apparmor-fix-race-between-freeing-data-and-fs-accessing-it.patch
new file mode 100644 (file)
index 0000000..7f47529
--- /dev/null
@@ -0,0 +1,704 @@
+From 8e135b8aee5a06c52a4347a5a6d51223c6f36ba3 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Sun, 1 Mar 2026 16:10:51 -0800
+Subject: apparmor: fix race between freeing data and fs accessing it
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 8e135b8aee5a06c52a4347a5a6d51223c6f36ba3 upstream.
+
+AppArmor was putting the reference to i_private data on its end after
+removing the original entry from the file system. However the inode
+can 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 <qsa@qualys.com>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Maxime BĂ©lair <maxime.belair@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c            |  194 +++++++++++++++++-------------
+ security/apparmor/include/label.h         |   16 +-
+ security/apparmor/include/lib.h           |   12 +
+ security/apparmor/include/policy.h        |    8 -
+ security/apparmor/include/policy_unpack.h |    6 
+ security/apparmor/label.c                 |   12 +
+ security/apparmor/policy_unpack.c         |    6 
+ 7 files changed, 153 insertions(+), 101 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -32,6 +32,7 @@
+ #include "include/crypto.h"
+ #include "include/ipc.h"
+ #include "include/label.h"
++#include "include/lib.h"
+ #include "include/policy.h"
+ #include "include/policy_ns.h"
+ #include "include/resource.h"
+@@ -62,6 +63,7 @@
+  * securityfs and apparmorfs filesystems.
+  */
++#define IREF_POISON 101
+ /*
+  * support fns
+@@ -153,6 +155,71 @@ static int aafs_show_path(struct seq_fil
+       return 0;
+ }
++static struct aa_ns *get_ns_common_ref(struct aa_common_ref *ref)
++{
++      if (ref) {
++              struct aa_label *reflabel = container_of(ref, struct aa_label,
++                                                       count);
++              return aa_get_ns(labels_ns(reflabel));
++      }
++
++      return NULL;
++}
++
++static struct aa_proxy *get_proxy_common_ref(struct aa_common_ref *ref)
++{
++      if (ref)
++              return aa_get_proxy(container_of(ref, struct aa_proxy, count));
++
++      return NULL;
++}
++
++static struct aa_loaddata *get_loaddata_common_ref(struct aa_common_ref *ref)
++{
++      if (ref)
++              return aa_get_i_loaddata(container_of(ref, struct aa_loaddata,
++                                                    count));
++      return NULL;
++}
++
++static void aa_put_common_ref(struct aa_common_ref *ref)
++{
++      if (!ref)
++              return;
++
++      switch (ref->reftype) {
++      case REF_RAWDATA:
++              aa_put_i_loaddata(container_of(ref, struct aa_loaddata,
++                                             count));
++              break;
++      case REF_PROXY:
++              aa_put_proxy(container_of(ref, struct aa_proxy,
++                                        count));
++              break;
++      case REF_NS:
++              /* ns count is held on its unconfined label */
++              aa_put_ns(labels_ns(container_of(ref, struct aa_label, count)));
++              break;
++      default:
++              AA_BUG(true, "unknown refcount type");
++              break;
++      }
++}
++
++static void aa_get_common_ref(struct aa_common_ref *ref)
++{
++      kref_get(&ref->count);
++}
++
++static void aafs_evict(struct inode *inode)
++{
++      struct aa_common_ref *ref = inode->i_private;
++
++      clear_inode(inode);
++      aa_put_common_ref(ref);
++      inode->i_private = (void *) IREF_POISON;
++}
++
+ static void aafs_free_inode(struct inode *inode)
+ {
+       if (S_ISLNK(inode->i_mode))
+@@ -162,6 +229,7 @@ static void aafs_free_inode(struct inode
+ static const struct super_operations aafs_super_ops = {
+       .statfs = simple_statfs,
++      .evict_inode = aafs_evict,
+       .free_inode = aafs_free_inode,
+       .show_path = aafs_show_path,
+ };
+@@ -262,7 +330,8 @@ static int __aafs_setup_d_inode(struct i
+  * aafs_remove(). Will return ERR_PTR on failure.
+  */
+ static struct dentry *aafs_create(const char *name, umode_t mode,
+-                                struct dentry *parent, void *data, void *link,
++                                struct dentry *parent,
++                                struct aa_common_ref *data, void *link,
+                                 const struct file_operations *fops,
+                                 const struct inode_operations *iops)
+ {
+@@ -299,6 +368,9 @@ static struct dentry *aafs_create(const
+               goto fail_dentry;
+       inode_unlock(dir);
++      if (data)
++              aa_get_common_ref(data);
++
+       return dentry;
+ fail_dentry:
+@@ -323,7 +395,8 @@ fail_lock:
+  * see aafs_create
+  */
+ static struct dentry *aafs_create_file(const char *name, umode_t mode,
+-                                     struct dentry *parent, void *data,
++                                     struct dentry *parent,
++                                     struct aa_common_ref *data,
+                                      const struct file_operations *fops)
+ {
+       return aafs_create(name, mode, parent, data, NULL, fops, NULL);
+@@ -448,7 +521,7 @@ end_section:
+ static ssize_t profile_load(struct file *f, const char __user *buf, size_t size,
+                           loff_t *pos)
+ {
+-      struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++      struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+       int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns,
+                                 f->f_cred);
+@@ -466,7 +539,7 @@ static const struct file_operations aa_f
+ static ssize_t profile_replace(struct file *f, const char __user *buf,
+                              size_t size, loff_t *pos)
+ {
+-      struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++      struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+       int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY,
+                                 buf, size, pos, ns, f->f_cred);
+       aa_put_ns(ns);
+@@ -486,7 +559,7 @@ static ssize_t profile_remove(struct fil
+       struct aa_loaddata *data;
+       struct aa_label *label;
+       ssize_t error;
+-      struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
++      struct aa_ns *ns = get_ns_common_ref(f->f_inode->i_private);
+       label = begin_current_label_crit_section();
+       /* high level check about policy management - fine grained in
+@@ -576,7 +649,7 @@ static int ns_revision_open(struct inode
+       if (!rev)
+               return -ENOMEM;
+-      rev->ns = aa_get_ns(inode->i_private);
++      rev->ns = get_ns_common_ref(inode->i_private);
+       if (!rev->ns)
+               rev->ns = aa_get_current_ns();
+       file->private_data = rev;
+@@ -1062,7 +1135,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) {
+@@ -1254,7 +1327,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)
+@@ -1387,7 +1460,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;
+@@ -1432,7 +1505,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);
+               }
+       }
+ }
+@@ -1471,45 +1543,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;
+@@ -1520,7 +1588,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 */
+@@ -1544,13 +1611,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;
+       }
+ }
+@@ -1584,14 +1648,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
+@@ -1637,7 +1694,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;
+@@ -1779,27 +1837,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 */
+@@ -1836,7 +1891,7 @@ static struct dentry *ns_mkdir_op(struct
+       if (error)
+               return ERR_PTR(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
+@@ -1886,7 +1941,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.
+@@ -1956,27 +2011,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;
+@@ -2001,40 +2035,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
+@@ -102,7 +102,7 @@ enum label_flags {
+ struct aa_label;
+ struct aa_proxy {
+-      struct kref count;
++      struct aa_common_ref count;
+       struct aa_label __rcu *label;
+ };
+@@ -125,7 +125,7 @@ struct label_it {
+  * vec: vector of profiles comprising the compound label
+  */
+ struct aa_label {
+-      struct kref count;
++      struct aa_common_ref count;
+       struct rb_node node;
+       struct rcu_head rcu;
+       struct aa_proxy *proxy;
+@@ -357,7 +357,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;
+@@ -366,7 +366,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;
+ }
+@@ -386,7 +386,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;
+@@ -426,7 +426,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);
+ }
+ /* wrapper fn to indicate semantics of the check */
+@@ -443,7 +443,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;
+ }
+@@ -451,7 +451,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
+@@ -85,6 +85,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
+@@ -355,7 +355,7 @@ static inline bool profile_mediates_safe
+ 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;
+ }
+@@ -369,7 +369,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;
+@@ -389,7 +389,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;
+@@ -402,7 +402,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;
+@@ -375,7 +377,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) {
+@@ -412,7 +415,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
+@@ -119,7 +119,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);
+ }
+@@ -166,7 +167,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.18/apparmor-fix-race-on-rawdata-dereference.patch b/queue-6.18/apparmor-fix-race-on-rawdata-dereference.patch
new file mode 100644 (file)
index 0000000..2442572
--- /dev/null
@@ -0,0 +1,440 @@
+From a0b7091c4de45a7325c8780e6934a894f92ac86b Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Tue, 24 Feb 2026 10:20:02 -0800
+Subject: apparmor: fix race on rawdata dereference
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit a0b7091c4de45a7325c8780e6934a894f92ac86b upstream.
+
+There is a race condition that leads to a use-after-free situation:
+because the rawdata inodes are not refcounted, an attacker can start
+open()ing one of the rawdata files, and at the same time remove the
+last reference to this rawdata (by removing the corresponding profile,
+for example), which frees its struct aa_loaddata; as a result, when
+seq_rawdata_open() is reached, i_private is a dangling pointer and
+freed memory is accessed.
+
+The rawdata inodes weren't refcounted to avoid a circular refcount and
+were supposed to be held by the profile rawdata reference.  However
+during profile removal there is a window where the vfs and profile
+destruction race, resulting in the use after free.
+
+Fix this by moving to a double refcount scheme. Where the profile
+refcount on rawdata is used to break the circular dependency. Allowing
+for freeing of the rawdata once all inode references to the rawdata
+are put.
+
+Fixes: 5d5182cae401 ("apparmor: move to per loaddata files, instead of replicating in profiles")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Maxime BĂ©lair <maxime.belair@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c            |   35 ++++++++------
+ security/apparmor/include/policy_unpack.h |   71 ++++++++++++++++++------------
+ security/apparmor/policy.c                |   12 ++---
+ security/apparmor/policy_unpack.c         |   32 +++++++++----
+ 4 files changed, 93 insertions(+), 57 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -79,7 +79,7 @@ static void rawdata_f_data_free(struct r
+       if (!private)
+               return;
+-      aa_put_loaddata(private->loaddata);
++      aa_put_i_loaddata(private->loaddata);
+       kvfree(private);
+ }
+@@ -404,7 +404,8 @@ static struct aa_loaddata *aa_simple_wri
+       data->size = copy_size;
+       if (copy_from_user(data->data, userbuf, copy_size)) {
+-              aa_put_loaddata(data);
++              /* trigger free - don't need to put pcount */
++              aa_put_i_loaddata(data);
+               return ERR_PTR(-EFAULT);
+       }
+@@ -432,7 +433,10 @@ static ssize_t policy_update(u32 mask, c
+       error = PTR_ERR(data);
+       if (!IS_ERR(data)) {
+               error = aa_replace_profiles(ns, label, mask, data);
+-              aa_put_loaddata(data);
++              /* put pcount, which will put count and free if no
++               * profiles referencing it.
++               */
++              aa_put_profile_loaddata(data);
+       }
+ end_section:
+       end_current_label_crit_section(label);
+@@ -503,7 +507,7 @@ static ssize_t profile_remove(struct fil
+       if (!IS_ERR(data)) {
+               data->data[size] = 0;
+               error = aa_remove_profiles(ns, label, data->data, size);
+-              aa_put_loaddata(data);
++              aa_put_profile_loaddata(data);
+       }
+  out:
+       end_current_label_crit_section(label);
+@@ -1250,18 +1254,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;
+@@ -1272,7 +1275,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);
+ }
+@@ -1384,9 +1387,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);
+@@ -1411,7 +1413,7 @@ fail_decompress:
+       return error;
+ fail_private_alloc:
+-      aa_put_loaddata(loaddata);
++      aa_put_i_loaddata(loaddata);
+       return error;
+ }
+@@ -1428,9 +1430,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);
+               }
+       }
+ }
+@@ -1469,18 +1471,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) {
+@@ -1488,6 +1493,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;
+       }
+@@ -1496,24 +1502,25 @@ int __aa_fs_create_rawdata(struct aa_ns
+                               &seq_rawdata_compressed_size_fops);
+       if (IS_ERR(dent))
+               goto fail;
++      aa_get_i_loaddata(rawdata);
+       rawdata->dents[AAFS_LOADDATA_COMPRESSED_SIZE] = dent;
+       dent = aafs_create_file("raw_data", S_IFREG | 0444,
+                                     dir, rawdata, &rawdata_fops);
+       if (IS_ERR(dent))
+               goto fail;
++      aa_get_i_loaddata(rawdata);
+       rawdata->dents[AAFS_LOADDATA_DATA] = dent;
+       d_inode(dent)->i_size = rawdata->size;
+       rawdata->ns = aa_get_ns(ns);
+       list_add(&rawdata->list, &ns->rawdata_list);
+-      /* no refcount on inode rawdata */
+       return 0;
+ fail:
+       remove_rawdata_dents(rawdata);
+-
++      aa_put_i_loaddata(rawdata);
+       return PTR_ERR(dent);
+ }
+ #endif /* CONFIG_SECURITY_APPARMOR_EXPORT_BINARY */
+--- a/security/apparmor/include/policy_unpack.h
++++ b/security/apparmor/include/policy_unpack.h
+@@ -87,17 +87,29 @@ struct aa_ext {
+       u32 version;
+ };
+-/*
+- * struct aa_loaddata - buffer of policy raw_data set
++/* struct aa_loaddata - buffer of policy raw_data set
++ * @count: inode/filesystem refcount - use aa_get_i_loaddata()
++ * @pcount: profile refcount - use aa_get_profile_loaddata()
++ * @list: list the loaddata is on
++ * @work: used to do a delayed cleanup
++ * @dents: refs to dents created in aafs
++ * @ns: the namespace this loaddata was loaded into
++ * @name:
++ * @size: the size of the data that was loaded
++ * @compressed_size: the size of the data when it is compressed
++ * @revision: unique revision count that this data was loaded as
++ * @abi: the abi number the loaddata uses
++ * @hash: a hash of the loaddata, used to help dedup data
+  *
+- * there is no loaddata ref for being on ns list, nor a ref from
+- * d_inode(@dentry) when grab a ref from these, @ns->lock must be held
+- * && __aa_get_loaddata() needs to be used, and the return value
+- * checked, if NULL the loaddata is already being reaped and should be
+- * considered dead.
++ * There is no loaddata ref for being on ns->rawdata_list, so
++ * @ns->lock must be held when walking the list. Dentries and
++ * inode opens hold refs on @count; profiles hold refs on @pcount.
++ * When the last @pcount drops, do_ploaddata_rmfs() removes the
++ * fs entries and drops the associated @count ref.
+  */
+ struct aa_loaddata {
+       struct kref count;
++      struct kref pcount;
+       struct list_head list;
+       struct work_struct work;
+       struct dentry *dents[AAFS_LOADDATA_NDENTS];
+@@ -119,52 +131,55 @@ struct aa_loaddata {
+ int aa_unpack(struct aa_loaddata *udata, struct list_head *lh, const char **ns);
+ /**
+- * __aa_get_loaddata - get a reference count to uncounted data reference
++ * aa_get_loaddata - get a reference count from a counted data reference
+  * @data: reference to get a count on
+  *
+- * Returns: pointer to reference OR NULL if race is lost and reference is
+- *          being repeated.
+- * Requires: @data->ns->lock held, and the return code MUST be checked
+- *
+- * Use only from inode->i_private and @data->list found references
++ * Returns: pointer to reference
++ * Requires: @data to have a valid reference count on it. It is a bug
++ *           if the race to reap can be encountered when it is used.
+  */
+ static inline struct aa_loaddata *
+-__aa_get_loaddata(struct aa_loaddata *data)
++aa_get_i_loaddata(struct aa_loaddata *data)
+ {
+-      if (data && kref_get_unless_zero(&(data->count)))
+-              return data;
+-      return NULL;
++      if (data)
++              kref_get(&(data->count));
++      return data;
+ }
++
+ /**
+- * aa_get_loaddata - get a reference count from a counted data reference
++ * aa_get_profile_loaddata - get a profile reference count on loaddata
+  * @data: reference to get a count on
+  *
+- * Returns: point to reference
+- * Requires: @data to have a valid reference count on it. It is a bug
+- *           if the race to reap can be encountered when it is used.
++ * Returns: pointer to reference
++ * Requires: @data to have a valid reference count on it.
+  */
+ static inline struct aa_loaddata *
+-aa_get_loaddata(struct aa_loaddata *data)
++aa_get_profile_loaddata(struct aa_loaddata *data)
+ {
+-      struct aa_loaddata *tmp = __aa_get_loaddata(data);
+-
+-      AA_BUG(data && !tmp);
+-
+-      return tmp;
++      if (data)
++              kref_get(&(data->pcount));
++      return data;
+ }
+ void __aa_loaddata_update(struct aa_loaddata *data, long revision);
+ bool aa_rawdata_eq(struct aa_loaddata *l, struct aa_loaddata *r);
+ void aa_loaddata_kref(struct kref *kref);
++void aa_ploaddata_kref(struct kref *kref);
+ struct aa_loaddata *aa_loaddata_alloc(size_t size);
+-static inline void aa_put_loaddata(struct aa_loaddata *data)
++static inline void aa_put_i_loaddata(struct aa_loaddata *data)
+ {
+       if (data)
+               kref_put(&data->count, aa_loaddata_kref);
+ }
++static inline void aa_put_profile_loaddata(struct aa_loaddata *data)
++{
++      if (data)
++              kref_put(&data->pcount, aa_ploaddata_kref);
++}
++
+ #if IS_ENABLED(CONFIG_KUNIT)
+ bool aa_inbounds(struct aa_ext *e, size_t size);
+ size_t aa_unpack_u16_chunk(struct aa_ext *e, char **chunk);
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -336,7 +336,7 @@ void aa_free_profile(struct aa_profile *
+       }
+       kfree_sensitive(profile->hash);
+-      aa_put_loaddata(profile->rawdata);
++      aa_put_profile_loaddata(profile->rawdata);
+       aa_label_destroy(&profile->label);
+       kfree_sensitive(profile);
+@@ -1154,7 +1154,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)
+@@ -1206,10 +1206,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;
+                               }
+@@ -1222,7 +1222,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);
+@@ -1355,7 +1355,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
+@@ -109,34 +109,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);
+       }
+ }
+@@ -154,6 +167,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.18/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch b/queue-6.18/apparmor-fix-side-effect-bug-in-match_char-macro-usage.patch
new file mode 100644 (file)
index 0000000..07e333d
--- /dev/null
@@ -0,0 +1,123 @@
+From 8756b68edae37ff546c02091989a4ceab3f20abd Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 29 Jan 2026 17:08:25 +0100
+Subject: apparmor: fix side-effect bug in match_char() macro usage
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit 8756b68edae37ff546c02091989a4ceab3f20abd upstream.
+
+The match_char() macro evaluates its character parameter multiple
+times when traversing differential encoding chains. When invoked
+with *str++, the string pointer advances on each iteration of the
+inner do-while loop, causing the DFA to check different characters
+at each iteration and therefore skip input characters.
+This results in out-of-bounds reads when the pointer advances past
+the input buffer boundary.
+
+[   94.984676] ==================================================================
+[   94.985301] BUG: KASAN: slab-out-of-bounds in aa_dfa_match+0x5ae/0x760
+[   94.985655] Read of size 1 at addr ffff888100342000 by task file/976
+
+[   94.986319] CPU: 7 UID: 1000 PID: 976 Comm: file Not tainted 6.19.0-rc7-next-20260127 #1 PREEMPT(lazy)
+[   94.986322] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
+[   94.986329] Call Trace:
+[   94.986341]  <TASK>
+[   94.986347]  dump_stack_lvl+0x5e/0x80
+[   94.986374]  print_report+0xc8/0x270
+[   94.986384]  ? aa_dfa_match+0x5ae/0x760
+[   94.986388]  kasan_report+0x118/0x150
+[   94.986401]  ? aa_dfa_match+0x5ae/0x760
+[   94.986405]  aa_dfa_match+0x5ae/0x760
+[   94.986408]  __aa_path_perm+0x131/0x400
+[   94.986418]  aa_path_perm+0x219/0x2f0
+[   94.986424]  apparmor_file_open+0x345/0x570
+[   94.986431]  security_file_open+0x5c/0x140
+[   94.986442]  do_dentry_open+0x2f6/0x1120
+[   94.986450]  vfs_open+0x38/0x2b0
+[   94.986453]  ? may_open+0x1e2/0x2b0
+[   94.986466]  path_openat+0x231b/0x2b30
+[   94.986469]  ? __x64_sys_openat+0xf8/0x130
+[   94.986477]  do_file_open+0x19d/0x360
+[   94.986487]  do_sys_openat2+0x98/0x100
+[   94.986491]  __x64_sys_openat+0xf8/0x130
+[   94.986499]  do_syscall_64+0x8e/0x660
+[   94.986515]  ? count_memcg_events+0x15f/0x3c0
+[   94.986526]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   94.986540]  ? handle_mm_fault+0x1639/0x1ef0
+[   94.986551]  ? vma_start_read+0xf0/0x320
+[   94.986558]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   94.986561]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   94.986563]  ? fpregs_assert_state_consistent+0x50/0xe0
+[   94.986572]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   94.986574]  ? arch_exit_to_user_mode_prepare+0x9/0xb0
+[   94.986587]  ? srso_alias_return_thunk+0x5/0xfbef5
+[   94.986588]  ? irqentry_exit+0x3c/0x590
+[   94.986595]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
+[   94.986597] RIP: 0033:0x7fda4a79c3ea
+
+Fix by extracting the character value before invoking match_char,
+ensuring single evaluation per outer loop.
+
+Fixes: 074c1cd798cb ("apparmor: dfa move character match into a macro")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/match.c |   30 ++++++++++++++++++++----------
+ 1 file changed, 20 insertions(+), 10 deletions(-)
+
+--- a/security/apparmor/match.c
++++ b/security/apparmor/match.c
+@@ -463,13 +463,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;
+@@ -503,13 +508,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.18/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch b/queue-6.18/apparmor-fix-unprivileged-local-user-can-do-privileged-policy-management.patch
new file mode 100644 (file)
index 0000000..15845a9
--- /dev/null
@@ -0,0 +1,182 @@
+From 6601e13e82841879406bf9f369032656f441a425 Mon Sep 17 00:00:00 2001
+From: John Johansen <john.johansen@canonical.com>
+Date: Fri, 7 Nov 2025 08:36:04 -0800
+Subject: apparmor: fix unprivileged local user can do privileged policy management
+
+From: John Johansen <john.johansen@canonical.com>
+
+commit 6601e13e82841879406bf9f369032656f441a425 upstream.
+
+An unprivileged local user can load, replace, and remove profiles by
+opening the apparmorfs interfaces, via a confused deputy attack, by
+passing the opened fd to a privileged process, and getting the
+privileged process to write to the interface.
+
+This does require a privileged target that can be manipulated to do
+the write for the unprivileged process, but once such access is
+achieved full policy management is possible and all the possible
+implications that implies: removing confinement, DoS of system or
+target applications by denying all execution, by-passing the
+unprivileged user namespace restriction, to exploiting kernel bugs for
+a local privilege escalation.
+
+The policy management interface can not have its permissions simply
+changed from 0666 to 0600 because non-root processes need to be able
+to load policy to different policy namespaces.
+
+Instead ensure the task writing the interface has privileges that
+are a subset of the task that opened the interface. This is already
+done via policy for confined processes, but unconfined can delegate
+access to the opened fd, by-passing the usual policy check.
+
+Fixes: b7fd2c0340eac ("apparmor: add per policy ns .load, .replace, .remove interface files")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/apparmorfs.c     |   16 +++++++++-------
+ security/apparmor/include/policy.h |    2 +-
+ security/apparmor/policy.c         |   34 +++++++++++++++++++++++++++++++++-
+ 3 files changed, 43 insertions(+), 9 deletions(-)
+
+--- a/security/apparmor/apparmorfs.c
++++ b/security/apparmor/apparmorfs.c
+@@ -412,7 +412,8 @@ static struct aa_loaddata *aa_simple_wri
+ }
+ static ssize_t policy_update(u32 mask, const char __user *buf, size_t size,
+-                           loff_t *pos, struct aa_ns *ns)
++                           loff_t *pos, struct aa_ns *ns,
++                           const struct cred *ocred)
+ {
+       struct aa_loaddata *data;
+       struct aa_label *label;
+@@ -423,7 +424,7 @@ static ssize_t policy_update(u32 mask, c
+       /* high level check about policy management - fine grained in
+        * below after unpack
+        */
+-      error = aa_may_manage_policy(current_cred(), label, ns, mask);
++      error = aa_may_manage_policy(current_cred(), label, ns, ocred, mask);
+       if (error)
+               goto end_section;
+@@ -444,7 +445,8 @@ static ssize_t profile_load(struct file
+                           loff_t *pos)
+ {
+       struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
+-      int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns);
++      int error = policy_update(AA_MAY_LOAD_POLICY, buf, size, pos, ns,
++                                f->f_cred);
+       aa_put_ns(ns);
+@@ -462,7 +464,7 @@ static ssize_t profile_replace(struct fi
+ {
+       struct aa_ns *ns = aa_get_ns(f->f_inode->i_private);
+       int error = policy_update(AA_MAY_LOAD_POLICY | AA_MAY_REPLACE_POLICY,
+-                                buf, size, pos, ns);
++                                buf, size, pos, ns, f->f_cred);
+       aa_put_ns(ns);
+       return error;
+@@ -487,7 +489,7 @@ static ssize_t profile_remove(struct fil
+        * below after unpack
+        */
+       error = aa_may_manage_policy(current_cred(), label, ns,
+-                                   AA_MAY_REMOVE_POLICY);
++                                   f->f_cred, AA_MAY_REMOVE_POLICY);
+       if (error)
+               goto out;
+@@ -1821,7 +1823,7 @@ static struct dentry *ns_mkdir_op(struct
+       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)
+@@ -1871,7 +1873,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
+@@ -419,7 +419,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
+@@ -925,17 +925,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;
+@@ -951,6 +978,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.18/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch b/queue-6.18/apparmor-replace-recursive-profile-removal-with-iterative-approach.patch
new file mode 100644 (file)
index 0000000..3d1f86f
--- /dev/null
@@ -0,0 +1,85 @@
+From ab09264660f9de5d05d1ef4e225aa447c63a8747 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Tue, 13 Jan 2026 09:09:43 +0100
+Subject: apparmor: replace recursive profile removal with iterative approach
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit ab09264660f9de5d05d1ef4e225aa447c63a8747 upstream.
+
+The profile removal code uses recursion when removing nested profiles,
+which can lead to kernel stack exhaustion and system crashes.
+
+Reproducer:
+  $ pf='a'; for ((i=0; i<1024; i++)); do
+      echo -e "profile $pf { \n }" | apparmor_parser -K -a;
+      pf="$pf//x";
+  done
+  $ echo -n a > /sys/kernel/security/apparmor/.remove
+
+Replace the recursive __aa_profile_list_release() approach with an
+iterative approach in __remove_profile(). The function repeatedly
+finds and removes leaf profiles until the entire subtree is removed,
+maintaining the same removal semantic without recursion.
+
+Fixes: c88d4c7b049e ("AppArmor: core policy routines")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy.c |   30 +++++++++++++++++++++++++++---
+ 1 file changed, 27 insertions(+), 3 deletions(-)
+
+--- a/security/apparmor/policy.c
++++ b/security/apparmor/policy.c
+@@ -183,19 +183,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.18/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch b/queue-6.18/apparmor-validate-dfa-start-states-are-in-bounds-in-unpack_pdb.patch
new file mode 100644 (file)
index 0000000..c5b1669
--- /dev/null
@@ -0,0 +1,55 @@
+From 9063d7e2615f4a7ab321de6b520e23d370e58816 Mon Sep 17 00:00:00 2001
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Date: Thu, 15 Jan 2026 15:30:50 +0100
+Subject: apparmor: validate DFA start states are in bounds in unpack_pdb
+
+From: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+
+commit 9063d7e2615f4a7ab321de6b520e23d370e58816 upstream.
+
+Start states are read from untrusted data and used as indexes into the
+DFA state tables. The aa_dfa_next() function call in unpack_pdb() will
+access dfa->tables[YYTD_ID_BASE][start], and if the start state exceeds
+the number of states in the DFA, this results in an out-of-bound read.
+
+==================================================================
+ BUG: KASAN: slab-out-of-bounds in aa_dfa_next+0x2a1/0x360
+ Read of size 4 at addr ffff88811956fb90 by task su/1097
+ ...
+
+Reject policies with out-of-bounds start states during unpacking
+to prevent the issue.
+
+Fixes: ad5ff3db53c6 ("AppArmor: Add ability to load extended policy")
+Reported-by: Qualys Security Advisory <qsa@qualys.com>
+Tested-by: Salvatore Bonaccorso <carnil@debian.org>
+Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com>
+Reviewed-by: Cengiz Can <cengiz.can@canonical.com>
+Signed-off-by: Massimiliano Pellizzer <massimiliano.pellizzer@canonical.com>
+Signed-off-by: John Johansen <john.johansen@canonical.com>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ security/apparmor/policy_unpack.c |   12 +++++++++++-
+ 1 file changed, 11 insertions(+), 1 deletion(-)
+
+--- a/security/apparmor/policy_unpack.c
++++ b/security/apparmor/policy_unpack.c
+@@ -770,7 +770,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);
index 2698f26f8f9401fc86da3f401c5975cf8379778c..7113305bcdbc76a1999ace41283753aaa5a6355b 100644 (file)
@@ -1,2 +1,13 @@
 net-sched-act_gate-snapshot-parameters-with-rcu-on-replace.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