Re: [PATCH v2] apparmor: fix cred UAF caused by begin_current_label_crit_section()

John Johansen <[email protected]>
Newsgroups gmane.linux.kernel.lsm,gmane.comp.security.apparmor,gmane.linux.kernel,gmane.linux.kernel.stable
Organization Canonical
Message-ID <[email protected]>
On 8/6/26 08:55, Jann Horn wrote:
> 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: [email protected]
> Fixes: c75afcd153f6 ("AppArmor: contexts used in attaching policy to system objects")
> Signed-off-by: Jann Horn <[email protected]>

lgtm, and I have done some light testing. I have pushed this to apparmor-next
and will kick a more thorough set of testing

Acked-by: John Johansen <[email protected]>



> ---
> Changes in v2:
> - store task work in task security blob to simplify things
> - remove cc to peterz, I'm no longer changing task_work implementation
> - Link to v1: https://patch.msgid.link/[email protected]
> ---
>   security/apparmor/include/cred.h |  6 +-----
>   security/apparmor/include/task.h | 15 +++++++++++----
>   security/apparmor/task.c         | 27 +++++++++++++++++++++++++++
>   3 files changed, 39 insertions(+), 9 deletions(-)
> 
> diff --git a/security/apparmor/include/cred.h b/security/apparmor/include/cred.h
> index 2b6098149b15..0e8b67159f56 100644
> --- a/security/apparmor/include/cred.h
> +++ b/security/apparmor/include/cred.h
> @@ -222,13 +222,9 @@ static inline struct aa_label *begin_current_label_crit_section(void)
>   {
>   	struct aa_label *label = aa_current_raw_label();
>   
> -	might_sleep();
> -
>   	if (label_is_stale(label)) {
>   		label = aa_get_newest_label(label);
> -		if (aa_replace_current_label(label) == 0)
> -			/* task cred will keep the reference */
> -			aa_put_label(label);
> +		aa_schedule_stale_label_replacement();
>   	}
>   
>   	return label;
> diff --git a/security/apparmor/include/task.h b/security/apparmor/include/task.h
> index b1aaaf60fa8b..6f26758ca10f 100644
> --- a/security/apparmor/include/task.h
> +++ b/security/apparmor/include/task.h
> @@ -21,15 +21,22 @@ static inline struct aa_task_ctx *task_ctx(struct task_struct *task)
>    * @onexec: profile to transition to on next exec  (MAY BE NULL)
>    * @previous: profile the task may return to     (MAY BE NULL)
>    * @token: magic value the task must know for returning to @previous_profile
> + * @label_replacement_tw: for aa_schedule_stale_label_replacement()
> + * @label_replacement_pending: is @label_replacement_tw pending?
> + *
> + * When changing this, check if aa_dup_task_ctx() needs to be updated.
>    */
>   struct aa_task_ctx {
>   	struct aa_label *nnp;
>   	struct aa_label *onexec;
>   	struct aa_label *previous;
>   	u64 token;
> +	struct callback_head label_replacement_tw;
> +	bool label_replacement_pending;
>   };
>   
>   int aa_replace_current_label(struct aa_label *label);
> +void aa_schedule_stale_label_replacement(void);
>   void aa_set_current_onexec(struct aa_label *label, bool stack);
>   int aa_set_current_hat(struct aa_label *label, u64 token);
>   int aa_restore_previous_label(u64 cookie);
> @@ -56,10 +63,10 @@ static inline void aa_free_task_ctx(struct aa_task_ctx *ctx)
>   static inline void aa_dup_task_ctx(struct aa_task_ctx *new,
>   				   const struct aa_task_ctx *old)
>   {
> -	*new = *old;
> -	aa_get_label(new->nnp);
> -	aa_get_label(new->previous);
> -	aa_get_label(new->onexec);
> +	new->nnp = aa_get_label(old->nnp);
> +	new->onexec = aa_get_label(old->onexec);
> +	new->previous = aa_get_label(old->previous);
> +	new->token = old->token;
>   }
>   
>   /**
> diff --git a/security/apparmor/task.c b/security/apparmor/task.c
> index b9fb3738124e..e16ff4130bc2 100644
> --- a/security/apparmor/task.c
> +++ b/security/apparmor/task.c
> @@ -14,6 +14,7 @@
>   
>   #include <linux/gfp.h>
>   #include <linux/ptrace.h>
> +#include <linux/task_work.h>
>   
>   #include "include/path.h"
>   #include "include/audit.h"
> @@ -89,6 +90,32 @@ int aa_replace_current_label(struct aa_label *label)
>   	return 0;
>   }
>   
> +static void aa_replace_stale_label_tw_func(struct callback_head *tw)
> +{
> +	struct aa_task_ctx *ctx = task_ctx(current);
> +	struct aa_label *label;
> +
> +	ctx->label_replacement_pending = false;
> +	label = aa_current_raw_label();
> +	if (!label_is_stale(label))
> +		return;
> +	label = aa_get_newest_label(label);
> +	aa_replace_current_label(label);
> +	aa_put_label(label);
> +}
> +
> +/* replace the current task's stale label on syscall return */
> +void aa_schedule_stale_label_replacement(void)
> +{
> +	struct aa_task_ctx *ctx = task_ctx(current);
> +
> +	if (ctx->label_replacement_pending)
> +		return;
> +	init_task_work(&ctx->label_replacement_tw, aa_replace_stale_label_tw_func);
> +	if (task_work_add(current, &ctx->label_replacement_tw, TWA_RESUME) == 0)
> +		ctx->label_replacement_pending = true;
> +}
> +
>   
>   /**
>    * aa_set_current_onexec - set the tasks change_profile to happen onexec
> 
> ---
> base-commit: 0d839570765118029aa8bf4a95444c6a11aacf85
> change-id: 20260714-fix-apparmor-cred-uaf-cc38ec2b38b7
> 
> Best regards,
> --
> Jann Horn <[email protected]>
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.