| 4ec11f14 | 12-Aug-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: fix deadlock in complain-mode change_hat
The use of change_hat when in complain mode can cause a deadlock when the hat doesn't exist and a new learning profile is created for the missing p
apparmor: fix deadlock in complain-mode change_hat
The use of change_hat when in complain mode can cause a deadlock when the hat doesn't exist and a new learning profile is created for the missing profile. This is because change_hat() has taken the lock to search the hat list and creating the new learning profile needs to take the lock to add it to the list.
From the bug report:
Originally found in 7.0.0 in LTS ubuntu 26.04 with pam_apparmor + su in complain mode set to change hats. Then verified in newest available vanilla kernel I've compiled to see if still present:
7.2-rc7 vanilla -> affected
checked also some other kernels: 6.18.44 vanilla -> affected 6.12.95 with debian patches -> unaffected
On systems without bug (for example 6.12.95 debian) it just prints:
aa_change_hat rc=0
On systems with bug, the executable always hangs, prints nothing and becomes unkillable. (And once stuck this way, it will cause any further hat changes to also cause the changing process to get stuck)
Then in syslog you can find hint about cause:
kernel: INFO: task hat:3409 blocked for more than 483 seconds. kernel: Not tainted 7.2.0-rc7 #1 kernel: "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. kernel: task:hat state:D stack:0 pid:3409 tgid:3409 ppid:2605 task_flags:0x400000 flags:0x00080800 kernel: Call Trace: kernel: <TASK> kernel: __schedule+0x48f/0xfe0 kernel: schedule+0x27/0xa0 kernel: schedule_preempt_disabled+0x15/0x30 kernel: __mutex_lock.constprop.0+0x569/0xa10 kernel: aa_new_learning_profile+0x15f/0x210 kernel: build_change_hat+0x19f/0x3b0 kernel: change_hat.isra.0+0x5dd/0xd60 kernel: aa_change_hat+0x2f3/0x710 kernel: aa_setprocattr_changehat+0x121/0x1f0 kernel: do_setattr+0x28c/0x340 kernel: apparmor_setselfattr+0x20/0x50 kernel: security_setselfattr+0xf6/0x110 kernel: __x64_sys_lsm_set_self_attr+0x53/0x90 kernel: do_syscall_64+0xdd/0x5e0 kernel: ? __mod_memcg_lruvec_state+0xfd/0x260 kernel: ? lruvec_stat_mod_folio+0x8d/0xd0 kernel: ? __folio_mod_stat+0x2d/0x90 kernel: ? map_anon_folio_pte_nopf+0xd1/0x1f0 kernel: ? do_anonymous_page+0x184/0xa10 kernel: ? __handle_mm_fault+0x805/0x870 kernel: ? count_memcg_events+0xef/0x230 kernel: ? handle_mm_fault+0x1f0/0x2f0 kernel: ? do_user_addr_fault+0x2bb/0x7b0 kernel: ? do_syscall_64+0x94/0x5e0 kernel: ? exc_page_fault+0x75/0x160 kernel: entry_SYSCALL_64_after_hwframe+0x76/0x7e kernel: RIP: 0033:0x7f815e134c8d kernel: RSP: 002b:00007fff6df94ea8 EFLAGS: 00000246 ORIG_RAX: 00000000000001cc kernel: RAX: ffffffffffffffda RBX: 0000556d8c81d040 RCX: 00007f815e134c8d kernel: RDX: 0000000000000046 RSI: 0000556d8c81d040 RDI: 0000000000000064 kernel: RBP: 00007fff6df94ef0 R08: 00007f815e212ac8 R09: 000000000000000c kernel: R10: 0000000000000000 R11: 0000000000000246 R12: 0000556d8c81d010 kernel: R13: 0000000000000026 R14: 0000000000000046 R15: 0000000000000064 kernel: </TASK> kernel: INFO: task hat:3409 is blocked on a mutex likely owned by task hat:3409.
To fix the issue, lift the locking out of the core of aa_new_learning_profile(), introduce a wrapper function that takes the lock where needed, and have build_change_hat() call the core function that no longer takes the lock.
In addition fix 4 other issues introduced by commit 32e92764d6f8d ("apparmor: grab ns lock and refresh when looking up changehat child profiles") - aa_get_profile_rcu() was replaced-by: aa_get_profile without the accompanying rcu_dereference_protected() - an extra aa_get_label(label) was introduced at the start of change_hat() without an accompanying aa_put_label() causing a reference count leak. - a reference count leak was introduced in the label_is_stale(label) case, where the newest profile would be leaked instead of the label passed to the function. - a potential UAF when the lookup walks up the tree with new_ns != ns the new label reference is put, and then used for the next lookup. The mutex_lock, will block replacement, and removal in the locked ns. However there are two cases where putting the reference can result in the label being freed even with the lock held.
1. the label does not have a list reference (possible for temporary or special profiles) in which case the put can trigger the cleanup. 2. the new label reference is in a different namespace, which does not have a lock held on it. This extends case 1 to also include replacement, and removal that could be occurring in the namespace new is in.
Reported-by: Martin Petricek <mp@petricek.net> Link: https://lists.ubuntu.com/archives/apparmor/2026-August/014907.html Fixes: 32e92764d6f8d ("apparmor: grab ns lock and refresh when looking up changehat child profiles") Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| fbedf6d9 | 04-Aug-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: constify aa_label parameters on read-only query helpers
Several label helpers only read from their struct aa_label * arguments: they compare labels, test subset relationships, or check the
apparmor: constify aa_label parameters on read-only query helpers
Several label helpers only read from their struct aa_label * arguments: they compare labels, test subset relationships, or check the mediation bitmask, all via direct field/index access. Mark those parameters const struct aa_label * to document intent and let the compiler enforce that the label is not modified.
The converted functions are: - label_mediates(), label_mediates_safe() - aa_label_cmp() (and its vec_cmp() helper) - __aa_label_next_not_in_set(), aa_label_is_subset(), aa_label_is_unconfined_subset() - __aa_subj_label_is_cached() - aa_label_next_confined(), aa_label_next_in_merge()
These all access the label through direct indexing or manual iterators rather than the label_for_each()/fn_for_each() macros, which are not const-correct and so gate the majority of the remaining label consumers (the print, match, and permission-check paths) from being constified.
No functional change.
Signed-off-by: John Johansen <john.johansen@canonical.com> Assisted-by: Claude:claude-opus-4.8
show more ...
|
| 08066bcd | 28-Jul-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: constify aa_profile parameters on read-only compute paths
A number of functions take a struct aa_profile * argument that is only ever read from: they compute DFA matches or apply the profi
apparmor: constify aa_profile parameters on read-only compute paths
A number of functions take a struct aa_profile * argument that is only ever read from: they compute DFA matches or apply the profile's mode flags without modifying the profile, taking a reference on it, or touching its embedded label. Mark those parameters const struct aa_profile * to document intent and let the compiler enforce it.
The converted functions are the permission "compute" path plus a few pure readers: - aa_apply_modes_to_perms(), aa_profile_match_label() - AUDIT_MODE() - aa_label_match() and its match_component()/label_compound_match()/ label_components_match() helpers (label.c) - match_component()/label_compound_match()/label_components_match()/ label_match()/change_profile_perms()/aa_xattrs_match() (domain.c) - match_iface()/match_addr_iface()/match_addr_iface_label()/ skb_match_to_sk()/skb_match_to_cmd() (af_inet.c) - aa_profile_capget(), path_flags(), profile_query_cb()
The remaining aa_profile * parameters cannot be made const: the audit path stores &profile->label into the owned, refcounted apparmor_audit_data.subj_label/peer fields, and the domain/lifecycle paths take references on the profile's embedded label (aa_get_label()/aa_get_newest_label()/aa_get_profile()) or write profile fields.
No functional change.
Signed-off-by: John Johansen <john.johansen@canonical.com> Assisted-by: Claude:claude-opus-4.8
show more ...
|
| 6c27cd23 | 27-Jul-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: refactory mount to use check_perms
Move the mount permissions check to use the common backend aa_check_perms() to check permissions. This will make it so caching, audit, complain, logic ca
apparmor: refactory mount to use check_perms
Move the mount permissions check to use the common backend aa_check_perms() to check permissions. This will make it so caching, audit, complain, logic can be handled consistently in a single place.
Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| a3ed5d43 | 24-Jul-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: mark static tables and structs as read only
static tables, and structs that are initialized as part of their data section or during init should be read only to protect against accidental o
apparmor: mark static tables and structs as read only
static tables, and structs that are initialized as part of their data section or during init should be read only to protect against accidental or malicous changes.
Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 1bd60611 | 24-Jul-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: fix error debug output in fn_label_build
checking PTR_ERROR() is not correct to just determine if any error occured, instead use the IS_ERR macro and also output the PTR_ERR as part of the
apparmor: fix error debug output in fn_label_build
checking PTR_ERROR() is not correct to just determine if any error occured, instead use the IS_ERR macro and also output the PTR_ERR as part of the debug message.
Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| ef6f3940 | 24-Jul-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: make table entry count last enum for static tables
Instead of keeping an external define for the various tables indexed by an enum, make the size the last entry of the enum so the table si
apparmor: make table entry count last enum for static tables
Instead of keeping an external define for the various tables indexed by an enum, make the size the last entry of the enum so the table size will get updated correctly with changes to the enum.
Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 4f0cafcb | 09-Oct-2021 |
John Johansen <john.johansen@canonical.com> |
apparmor: refactor network sock mediation in preparation for inet mediation
Refactor network mediation, introducing the stub code for the fine grained inet mediation. This is a preparatory step and
apparmor: refactor network sock mediation in preparation for inet mediation
Refactor network mediation, introducing the stub code for the fine grained inet mediation. This is a preparatory step and does not change mediation.
Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 9f1e4019 | 10-Aug-2026 |
Hyunwoo Kim <imv4bel@gmail.com> |
apparmor: fix out-of-bounds write when null terminating a label vec
aa_vec_unique() null terminates at vec[n - dups] when VEC_FLAG_TERMINATE is passed. If the components are all distinct no duplicat
apparmor: fix out-of-bounds write when null terminating a label vec
aa_vec_unique() null terminates at vec[n - dups] when VEC_FLAG_TERMINATE is passed. If the components are all distinct no duplicates are dropped, dups is 0 and the terminator goes to vec[n], so the caller has to provide room for n + 1 entries.
aa_label_strn_parse() sets up its vector with vec_setup(profile, vec, len, gfp) and then calls aa_vec_unique(vec, len, VEC_FLAG_TERMINATE), but vec_setup() does not reserve the terminator entry. Up to LOCAL_VEC_ENTRIES it uses the local array of LOCAL_VEC_ENTRIES pointers, above that it allocates exactly len pointers. The terminator therefore lands one entry past the end of the local array when len is LOCAL_VEC_ENTRIES, and one entry past the end of the allocation when len is larger.
len comes from the number of "//&" separated components in the label name and label_count_strn_entries() does not bound it. An unprivileged task reaches the parse by writing to /proc/self/attr/apparmor/current or through lsm_set_self_attr(2), both of which go through do_setattr(), and the name is parsed before the change_profile permission is checked. The query_label() path behind the securityfs .access file, which is mode 0666, performs no permission check at all. Every component has to resolve to a loaded profile, so a system with policy loaded is required.
The other two VEC_FLAG_TERMINATE users work on a label vec that aa_label_alloc() has already sized with "+ 1 for null terminator entry on vec". Reserve the same entry in vec_setup() and DEFINE_VEC(). Passing len + 1 from the caller instead would move len == LOCAL_VEC_ENTRIES out of the local array and into kzalloc().
Fixes: f1bd904175e8 ("apparmor: add the base fns() for domain labels") Cc: stable@vger.kernel.org Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 1555a0e2 | 07-Aug-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: optimize current_label_crit_section() with needput
The {begin,end}_current_label_crit_section() has the same issue as the {__begin,__end} version. That is the check to see if the label has
apparmor: optimize current_label_crit_section() with needput
The {begin,end}_current_label_crit_section() has the same issue as the {__begin,__end} version. That is the check to see if the label has been updated in the end check forces an unnecessary memory barrier. We can optimize this the same way we do with the {__begin,__end} variant by passing in a local variable that carries the state information from the begin check into the end check.
No functional change.
Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 3f4ae5fa | 06-Aug-2026 |
Jann Horn <jannh@google.com> |
apparmor: fix cred UAF caused by begin_current_label_crit_section()
AppArmor's begin_current_label_crit_section() is a scary function called from lots of LSM hooks (in particular VFS/socket-related
apparmor: fix cred UAF caused by begin_current_label_crit_section()
AppArmor's begin_current_label_crit_section() is a scary function called from lots of LSM hooks (in particular VFS/socket-related ones) that checks if the label referenced by the current creds is marked FLAG_STALE, and if so, attempts to use aa_replace_current_label() to replace the creds with an updated version that uses a new label.
The first problem with this is that it would directly lead to UAF of `struct cred` if anything in the kernel takes a pointer to the current creds and accesses these past a security hook invocation that replaces creds, like so: ``` const struct cred *cred = current_cred(); alloc_file_pseudo(...); uid_t uid = cred->euid; ``` I don't know if anything in the kernel actually does this, but I think it is very surprising that this pattern could lead to UAF.
The second problem is that things go wrong when aa_replace_current_label() runs with overridden credentials. aa_replace_current_label() bails out if `current_cred() != current_real_cred()` (mirroring the check in proc_pid_attr_write()), but this check can't actually reliably detect overridden credentials because the overridden creds can be the same as the objective creds.
So in approximately the following scenario, things go wrong:
1. task begins with <creds A> (as both objective and subjective creds), with refcount=2 2. task grabs an extra reference on <creds A> for overriding 3. task calls override_creds(<creds A>), which returns a pointer to the old subjective creds (<creds A>) 4. task enters AppArmor LSM hook 5. AppArmor checks that objective/subjective creds are equal 6. AppArmor replaces both cred pointers with <creds B> and drops 2 refs on <creds A> 7. task leaves AppArmor LSM hook 8. task calls revert_creds(<creds A>) 9. now task->cred is <creds A> while task->real_cred is <creds B>, but the task_struct logically holds two references to <creds B> 10. another task drops the extra reference on <creds A> that was used for overriding, refcount drops to 0 11. now task->real_cred points to freed creds
At this point, any access to current_cred() will be UAF.
I have a test case where I run aa-disable on a profile while a process using that profile is blocked on splice() from a FUSE passthrough file into a full pipe; after the profile update, the pipe becomes empty, splice() resumes, the credentials go out of sync, and a subsequent getuid() syscall results in a KASAN UAF splat.
To fix this, instead of directly replacing creds, do it via task_work that will run at the end of the current syscall. (The point in time at which the cred replacement happens should have no correctness impact; it is just a performance optimization to avoid unnecessarily touching the refcount of the new label.)
Note that AppArmor still performs direct cred replacements in the sb_pivotroot LSM hook after this change, and that direct cred replacements can still happen in VFS ->write() callbacks via proc_pid_attr_write().
There are two options for what to do with aa_dup_task_ctx(): Either explicitly reset new->label_replacement_pending after the entire aa_task_ctx has been copied, or switch to manually copying members over. I am switching to manually copying members over because that should make bugs more obvious.
Cc: stable@vger.kernel.org Fixes: c75afcd153f6 ("AppArmor: contexts used in attaching policy to system objects") Signed-off-by: Jann Horn <jannh@google.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 17b5758b | 01-Feb-2024 |
Maxime Bélair <maxime.belair@canonical.com> |
apparmor: Initial support for compressed policies
This patch allows policies to be compressed in userspace and be sent to the kernel through the existing ".load" and ".replace" kernel interfaces.
T
apparmor: Initial support for compressed policies
This patch allows policies to be compressed in userspace and be sent to the kernel through the existing ".load" and ".replace" kernel interfaces.
The benefits of this approach are: - Save kernel time when loading policies - Allow userspace to provide a higher level of compression than the one provided by the kernel (ZSTD_CLEVEL_DEFAULT), thus saving space. - Allow small embedded systems to only store the compressed version of policies in userspace, saving memory.
Userspace-compressed policies improve system time by up to ~30% for big profiles.
Signed-off-by: Maxime Bélair <maxime.belair@canonical.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 37077e4c | 13-Feb-2026 |
Ryan Lee <ryan.lee@canonical.com> |
apparmor: make include headers self-contained
Besides of resolving clangd IDE warnings, self-contained headers will be less likely to break if the surrounding includes in .c files using them change.
apparmor: make include headers self-contained
Besides of resolving clangd IDE warnings, self-contained headers will be less likely to break if the surrounding includes in .c files using them change.
Signed-off-by: Ryan Lee <ryan.lee@canonical.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 6f060496 | 26-May-2026 |
Ruslan Valiyev <linuxoid@gmail.com> |
apparmor: fix use-after-free in rawdata dedup loop
aa_replace_profiles() walks ns->rawdata_list to dedup the incoming policy blob against entries already attached to existing profiles. Per the kerne
apparmor: fix use-after-free in rawdata dedup loop
aa_replace_profiles() walks ns->rawdata_list to dedup the incoming policy blob against entries already attached to existing profiles. Per the kernel-doc on struct aa_loaddata, list membership does not hold a reference: profiles hold pcount, and when the last pcount drops, do_ploaddata_rmfs() is queued on a workqueue that takes ns->lock and removes the entry. Between dropping the last pcount and the workqueue running, an entry remains on the list with pcount == 0.
aa_get_profile_loaddata() is an unconditional kref_get() on pcount, so when the dedup loop hits such an entry, refcount hardening reports
refcount_t: addition on 0; use-after-free.
inside aa_replace_profiles(), and the poisoned counter then trips "saturated" and "underflow" warnings on the subsequent uses of the same loaddata.
Before commit a0b7091c4de4 ("apparmor: fix race on rawdata dereference") the dedup path used a get_unless_zero-style helper on a single counter, so the existing "if (tmp)" guard was meaningful. The split-refcount refactor introduced aa_get_profile_loaddata(), which has plain kref_get() semantics, and the guard quietly became a no-op.
Introduce aa_get_profile_loaddata_not0(), matching the existing _not0 convention used by aa_get_profile_not0(), and use it for the rawdata_list dedup lookup so dying entries are skipped.
Reproduced on x86_64 with v7.1-rc5 in QEMU+KVM running Ubuntu 24.04 + stress-ng 0.17.06:
stress-ng --apparmor 1 --klog-check --timeout 60s
Without this patch the three refcount_t warnings fire within a few seconds. With it the same 60 s run is clean. Coverage is a smoke-test only; a longer soak with CONFIG_KASAN, CONFIG_KCSAN and CONFIG_PROVE_LOCKING would be welcome from anyone with the cycles.
Fixes: a0b7091c4de4 ("apparmor: fix race on rawdata dereference") Reported-by: Colin Ian King <colin.i.king@gmail.com> Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221513 Cc: stable@vger.kernel.org Signed-off-by: Ruslan Valiyev <linuxoid@gmail.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 59fe6fbc | 29-Jan-2026 |
Georgia Garcia <georgia.garcia@canonical.com> |
apparmor: remove or add symlinks to rawdata according to export_binary
When the export_binary parameter is set, then rawdata is available and there should be a symbolic link for the rawdata in the p
apparmor: remove or add symlinks to rawdata according to export_binary
When the export_binary parameter is set, then rawdata is available and there should be a symbolic link for the rawdata in the profile directory in apparmorfs. If the parameter is unset, then the symlinks should not exist.
The issue arises when changing the value of export_binary on runtime and replacing profiles. If export_binary was set when the profile was originally loaded, then changed to 0 and the profile was reloaded, then the symbolic links would still exist but would return ENOENT because the rawdata no longer exists.
On the opposite side, if export_binary was unset when the profile was originally loaded, then changed to 1 and the profile was reloaded, then the symbolic links would not exist, even though the rawdata does.
Fixes: d61c57fde8191 ("apparmor: make export of raw binary profile to userspace optional") Signed-off-by: Georgia Garcia <georgia.garcia@canonical.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|
| 716d384a | 24-Feb-2026 |
John Johansen <john.johansen@canonical.com> |
apparmor: make fn_label_build() capable of handling not supported
Currently fn_label_build() callback fns must provide a transition or failure. Change this so that a callback can indicate it should
apparmor: make fn_label_build() capable of handling not supported
Currently fn_label_build() callback fns must provide a transition or failure. Change this so that a callback can indicate it should be skipped/not be involved in the label being built.
This will be useful when building object labels based on mediation flags, as to whether the label should be set.
Existing callers can keep treating NULL return as an error because none of those callback fns support skipping, but instead of the old error handling replace with AA_BUG.
Reviewed-by: Georgia Garcia <georgia.garcia@canonical.com> Signed-off-by: John Johansen <john.johansen@canonical.com>
show more ...
|