apparmor: do not pass a NULL old label to aa_label_is_subset()

This report was produced with the help of AI tooling.  Per
Documentation/process/security-bugs.rst ("If you resorted to AI assistance to
identify a bug, you must treat it as public") I am reporting it publicly, and I
am not attaching a reproducer - a tested one is available on request.

1. Affected version
   v7.3-rc7 and earlier.  File: security/apparmor/af_unix.c:685
   (update_sk_ctx()).  The file is byte-identical in mainline
   0fcb05b72344f40806cbe810de8872f1698bf053 (2026-10-14), so this is not fixed
   upstream.

2. The problem
   unix_connect_peers() records the peer label in the socket context, and its
   only callers are apparmor_socket_socketpair() (lsm.c:1350) and
   apparmor_unix_stream_connect() (lsm.c:1213).  A connected AF_UNIX SOCK_DGRAM
   socket never goes through either of them, so ctx->peer stays NULL while
   unix_peer(sk) is set.  aa_unix_file_perm() then takes the "there is a peer"
   path (af_unix.c:756 peer_sk != NULL, :759 !is_sk_fs for an abstract address,
   :763 !is_peer_fs, :781 plabel = peer ctx->label), calls
   update_peer_ctx(peer) and then update_sk_ctx(sock->sk, label, plabel) with
   ctx->peer == NULL:

       af_unix.c:679   if (plabel != ctx->peer_lastupdate) {
       af_unix.c:680           old = ctx->peer;        <- NULL
       af_unix.c:685           if (!aa_label_is_subset(plabel, old))
       label.c:522                 sub->size               <- NULL deref

   Any confined task triggers it with a plain read()/write() on such a socket.

3. Triggering conditions
   - any AppArmor profile in enforce mode;
   - socket(AF_UNIX, SOCK_DGRAM, 0) x2, bind() one side to an abstract address,
     connect() the other to it, then write()/read() on the connected side;
   - unprivileged user, no capabilities, no user namespace required.
   Result: oops in process context.  With the default configuration
   (panic_on_oops=0) the task exits "with irqs disabled" and
   "with preempt_count 1" - the fault happens while the unix socket lock is
   held - and the kernel then reports recurring
   "rcu: INFO: rcu_preempt self-detected stall on CPU" for that task (observed
   at t=21s, 84s and 147s, every ~63s), i.e. the machine does not recover.
   With panic_on_oops=1 it is a kernel panic.  That crosses the availability
   boundary the threat model spells out for unprivileged users
   (Documentation/process/threat-model.rst:65-70: users not having elevated
   capabilities "may not ... affect the system's availability (shutdown,
   reboot, panic, hang, ...)").

   Observed on an unmodified v7.3-rc7 kernel (x86_64, qemu/KVM):

       BUG: kernel NULL pointer dereference, address: 000000000000004c
       #PF: supervisor read access in kernel mode
       RIP: 0010:__aa_label_next_not_in_set+0x6/0xd0
       Code: ... 41 57 <8b> 42 4c ...        (mov 0x4c(%rdx),%eax, rdx = 0)
       CR2: 000000000000004c
       Call Trace:
        <TASK>
        aa_label_is_subset+0x37/0x60
        aa_unix_file_perm+0x2d1/0x930
        aa_file_perm+0x479/0x580
        common_file_perm+0x59/0x150
        security_file_permission+0x3b/0x50
        vfs_write+0x73/0x440
        ksys_write+0xbb/0xe0
        do_syscall_64+0xdd/0x4a0
        entry_SYSCALL_64_after_hwframe+0x77/0x7f

   Two negative controls in the same run (a socketpair, and a stream connect to
   an abstract listener) both survive, which is what pins the missing
   unix_connect_peers() call on the dgram path.

4. Reproducer
   Available on request: statically linked initramfs probe + a minimal
   AppArmor policy + the console log above.

5. Proposed fix
   Check 'old' before dereferencing it in update_sk_ctx().  Tested on
   v7.3-rc7: with the patch applied the same probe runs to completion with no
   oops (write -> 1 errno=0, read -> -1 errno=11), the unpatched kernel oopses
   as quoted above.


--- git format-patch output ---
(format-patch 信封的 From/Date 行已删去;下方即为补丁正文:提交信息 + diff)
Subject: [PATCH] apparmor: fix NULL pointer dereference in update_sk_ctx()

update_sk_ctx() passes ctx->peer straight to aa_label_is_subset() without
checking it for NULL.  aa_label_is_subset() documents @sub as non-NULL
through AA_BUG() only, which compiles to nothing unless
CONFIG_SECURITY_APPARMOR_DEBUG_ASSERTS is enabled, and
__aa_label_next_not_in_set() dereferences sub->size, so a NULL @sub is an
oops.

ctx->peer is only ever populated by unix_connect_peers(), which runs for
socketpair() and for stream connect().  For an AF_UNIX SOCK_DGRAM socket
that has been connect()ed, ctx->peer and ctx->peer_lastupdate are both
still NULL, so the "plabel && peer_lastupdate != plabel" test does not
short-circuit and the NULL dereference is taken from the file permission
hook:

  BUG: kernel NULL pointer dereference, address: 000000000000004c
  RIP: 0010:__aa_label_next_not_in_set+0x6/0x...
  CR2: 000000000000004c        (NULL + offsetof(struct aa_label, size))
  RDX: 0000000000000000        (@sub)
  Call Trace:
   aa_label_is_subset
   update_sk_ctx
   aa_unix_file_perm
   aa_file_perm
   security_file_permission
   vfs_write

A NULL peer label means no peer label has been cached yet, so there is no
subset relationship to evaluate; the comment already on the else branch
("race or a subset - don't update") describes the resulting behaviour.

Reproducer: a connect()ed AF_UNIX SOCK_DGRAM socket under a profile that
mediates the access, followed by write().  Verified on v7.3-rc7 with the
trace above.

Fixes: 88fec3526e84 ("apparmor: make sure unix socket labeling is correctly 
updated.")
Signed-off-by: Hang Nan <[email protected]>
---
 security/apparmor/af_unix.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/security/apparmor/af_unix.c b/security/apparmor/af_unix.c
index b908e7448..52f3cc95b 100644
--- a/security/apparmor/af_unix.c
+++ b/security/apparmor/af_unix.c
@@ -682,7 +682,7 @@ static void update_sk_ctx(struct sock *sk, struct aa_label 
*label,
                if (old == plabel) {
                        rcu_assign_pointer(ctx->peer_lastupdate,
                                           aa_get_label(plabel));
-               } else if (aa_label_is_subset(plabel, old)) {
+               } else if (old && aa_label_is_subset(plabel, old)) {
                        rcu_assign_pointer(ctx->peer_lastupdate,
                                           aa_get_label(plabel));
                        rcu_assign_pointer(ctx->peer, aa_get_label(plabel));
-- 
2.47.3


Reply via email to