DORMANTno replies

[PATCH] apparmor: handle NULL peer label in AF_UNIX context updates

From: Maciek Borzecki <hidden>
Date: 2026-09-17 07:15:51
Also in: lkml
Subsystem: apparmor security module, security subsystem, the rest · Maintainers: John Johansen, John Johansen, Georgia Garcia, Paul Moore, James Morris, "Serge E. Hallyn", Linus Torvalds

When a confined process performs the first file permission revalidation on
an AF_UNIX socket, both update_sk_ctx() and update_peer_ctx() can be called
before the socket's peer label cache (ctx->peer) has been populated. In
those cases they pass NULL as a label argument to aa_label_is_subset() or
aa_label_merge(), which immediately dereferences label->size and causes an
Oops:

  RIP: __aa_label_next_not_in_set+0xd/0x110
  CR2: 000000000000004c
  Call Trace:
    aa_label_is_subset
    aa_unix_file_perm
    aa_file_perm
    apparmor_file_permission
    security_file_permission
    rw_verify_area
    vfs_write
    ksys_write

Observed on Arch Linux kernels 7.2.4-arch1-2 and 7.2.6-arch2-1, triggered
by snapd unit tests writing to a connected AF_UNIX socket. The issue
persists across those stable updates.

Why this can happen:

* apparmor_socket_socketpair() calls unix_connect_peers(), which pre-
  populates ctx->peer for AF_UNIX socketpairs. Because of that, simple
  socketpair() tests never exercise the NULL-peer path.

* For an AF_UNIX SOCK_DGRAM socket that is connected explicitly (or via
  any path that does not go through unix_connect_peers()), ctx->peer is
  still NULL the first time aa_unix_file_perm() runs. The permission check
  succeeds, then update_peer_ctx() tries to merge label into ctx->peer and
  update_sk_ctx() calls aa_label_is_subset(plabel, ctx->peer). Both
  dereference NULL.

The following reproducer profile demonstrates the bug:

    profile aa_unix_server /path/to/repro {
        #include <abstractions/base>

        change_profile -> aa_unix_client,
        file,
        unix,
        /path/to/repro rmix,
    }

    profile aa_unix_client {
        #include <abstractions/base>

        file,
        unix,
    }

Run a program that binds an abstract AF_UNIX SOCK_DGRAM socket under the
server profile, then forks a child that changes into the aa_unix_client
profile, connects the DGRAM socket to the abstract address, and writes().
The write() path reaches the context-update code with ctx->peer == NULL and
oopses.

Fix all spots in update_sk_ctx() and update_peer_ctx() that can see a NULL
ctx->peer:

1. update_sk_ctx() RCU check: only call aa_label_is_subset() when
   ctx->peer is non-NULL. If it is still NULL, an update is required.

2. update_sk_ctx() spin-locked section: treat a NULL old peer label as
   "plabel is a superset", i.e. populate ctx->peer with plabel.

3. update_peer_ctx(): if ctx->peer is currently NULL, just store the new
   label directly instead of trying to merge with NULL.

Fixes: 88fec3526e84 ("apparmor: make sure unix socket labeling is correctly updated.")
Signed-off-by: Maciek Borzecki <redacted>
---
 security/apparmor/af_unix.c | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/security/apparmor/af_unix.c b/security/apparmor/af_unix.c
index b908e744818c9bfef1638c6abadec75c241dc9b9..4762b70cdc43b5dac87f079067eca0ca74d5dd52 100644
--- a/security/apparmor/af_unix.c
+++ b/security/apparmor/af_unix.c
@@ -659,7 +659,8 @@ static void update_sk_ctx(struct sock *sk, struct aa_label *label,
 	rcu_read_lock();
 	update_sk = (plabel &&
 		     (plabel != rcu_access_pointer(ctx->peer_lastupdate) ||
-		      !aa_label_is_subset(plabel, rcu_dereference(ctx->peer)))) ||
+		      (rcu_access_pointer(ctx->peer) &&
+		       !aa_label_is_subset(plabel, rcu_dereference(ctx->peer))))) ||
 	  !__aa_subj_label_is_cached(label, rcu_dereference(ctx->label));
 	rcu_read_unlock();
 	if (!update_sk)
@@ -682,7 +683,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));
@@ -700,13 +701,18 @@ static void update_peer_ctx(struct sock *sk, struct aa_sk_ctx *ctx,
 	spin_lock(&unix_sk(sk)->lock);
 	old = rcu_dereference_protected(ctx->peer,
 					lockdep_is_held(&unix_sk(sk)->lock));
-	l = aa_label_merge(old, label, GFP_ATOMIC);
-	if (l) {
-		if (l != old) {
-			rcu_assign_pointer(ctx->peer, l);
-			aa_put_label(old);
-		} else
-			aa_put_label(l);
+	if (!old) {
+		rcu_assign_pointer(ctx->peer, aa_get_label(label));
+	} else {
+		l = aa_label_merge(old, label, GFP_ATOMIC);
+		if (l) {
+			if (l != old) {
+				rcu_assign_pointer(ctx->peer, l);
+				aa_put_label(old);
+			} else {
+				aa_put_label(l);
+			}
+		}
 	}
 	spin_unlock(&unix_sk(sk)->lock);
 }
-- 
2.55.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help