ANT-2026-CM0TCREP · torvalds/linux

use-after-free high

CVE-2026-43074

Severity Claude high · Security research firm - · Maintainer high

REPORT

Anthropic's analysis of this finding, sealed at approval.

ANT-2026-CM0TCREP: UAF in ep_get_upwards_depth_proc() giving unprivileged LPE

During epoll_ctl(A, EPOLL_CTL_ADD, B), the nesting-depth check walks A->refs under rcu_read_lock() and follows each epi->ep back-pointer to the watching eventpoll instance. While the epitem list entries are protected by kfree_rcu(), the struct eventpoll they point to is released via plain kfree() in ep_free(). A second thread closing a watcher fd can therefore free that eventpoll while the walker still holds a pointer to it, after which the walker writes loop_check_gen (u64) and loop_check_depth (u8) into freed memory. By spraying the freed kmalloc slot with attacker-controlled data, this becomes a constrained write-what-where primitive; chained with two info leaks, the reporter's PoC overwrites cred->cap_permitted, gains CAP_SYS_MODULE, and loads a kernel module to obtain root in ~20 seconds on a default preempt=voluntary build.

Target

Project: torvalds/linux
Version: >= commit f2e467a48287 ("eventpoll: Fix semi-unbounded recursion", 2025-07-11)
Location: ep_get_upwards_depth_proc() in fs/eventpoll.c; ep_free() in fs/eventpoll.c

Technical Details

KASAN reports "slab-use-after-free in ep_get_upwards_depth_proc" within seconds under the minimal reproducer. The root cause is a lifetime mismatch: hlist_for_each_entry_rcu() over ep->refs is only safe because epitem objects are freed via kfree_rcu(), but the epi->ep pointer targets a struct eventpoll whose destructor ep_free() calls plain kfree(ep) with no grace-period wait. The RCU read lock therefore does not protect the dereference of epi->ep, and the subsequent stores to ep->gen and ep->loop_check_depth hit freed (and potentially reallocated) memory.

Crash signature: KASAN: slab-use-after-free in ep_get_upwards_depth_proc

Reproduction

  1. Leak KASLR base and a pointer into current task's struct cred via an io_uring fdinfo OOB read.
  2. Leak the kernel address of a kmalloc-192 slot adjacent to a controlled user_key_payload via a keyctl PKEY_ENCRYPT OOB read; stage a fake epitem there.
  3. Build a large ep->refs list (~4000 watchers) so the depth-check walk is long; run ~25,000 priming epoll_ctl calls to advance loop_check_gen near a useful value.
  4. On CPU 0, repeatedly call epoll_ctl(ADD) to trigger ep_get_upwards_depth_proc(); on CPU 1, close watcher fds to free their struct eventpoll mid-walk; a third thread fires membarrier(PRIVATE_EXPEDITED) IPIs to stall CPU 0 under preempt=voluntary.
  5. Immediately spray the freed eventpoll slot with a user_key_payload whose bytes place a pointer to the stage-2 fake at the epi->ep offset; the walker follows it and writes loop_check_gen into what is actually cred->cap_permitted.
  6. The next field (cap_effective) is zero and is misread as an empty ep->refs list head, so the walker returns cleanly without crashing.
  7. Repeat until a written counter value has bit 16 (CAP_SYS_MODULE) set; call capset() to copy permitted into effective, then finit_module() an embedded module that commits root creds.

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Switch ep_free() from kfree(ep) to kfree_rcu(ep, ...) so the struct eventpoll outlives any RCU-protected walker holding a reference via epi->ep. Reporter notes this is the minimal fix and may not be the preferred one.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-CM0TCREP.


Reference: ANT-2026-CM0TCREP
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-29 Sent to maintainer
  2. 2026-03-29 Maintainer acknowledged
  3. 2026-04-11 Patch released
  4. 2026-05-14 Reported to tracker
  5. 2026-08-17 Publicly revealed
PROVENANCE

SHA-3-512 hash:

23e00ddc8a68a55c2e0e8b9c4e8b357ed0d74f718fceef3afd888c3880979df69908e2016aa0a16295ba68a048f1d7e66752738ef735298bd5b24febf4499ade

Committed 2026-03-29 17:04 PT

Revealed 2026-08-17 17:36 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-CM0TCREP",
  "bug_class": "Use-After-Free",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-05-14T22:41:49+00:00",
  "description": "During epoll_ctl(A, EPOLL_CTL_ADD, B), the nesting-depth check walks A->refs under rcu_read_lock() and follows each epi->ep back-pointer to the watching eventpoll instance. While the epitem list entries are protected by kfree_rcu(), the struct eventpoll they point to is released via plain kfree() in ep_free(). A second thread closing a watcher fd can therefore free that eventpoll while the walker still holds a pointer to it, after which the walker writes loop_check_gen (u64) and loop_check_depth (u8) into freed memory. By spraying the freed kmalloc slot with attacker-controlled data, this becomes a constrained write-what-where primitive; chained with two info leaks, the reporter's PoC overwrites cred->cap_permitted, gains CAP_SYS_MODULE, and loads a kernel module to obtain root in ~20 seconds on a default preempt=voluntary build.",
  "discovered_at": null,
  "location": "ep_get_upwards_depth_proc() in fs/eventpoll.c; ep_free() in fs/eventpoll.c",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "torvalds/linux",
  "reproduction": [
    "1. Leak KASLR base and a pointer into current task's struct cred via an io_uring fdinfo OOB read.",
    "2. Leak the kernel address of a kmalloc-192 slot adjacent to a controlled user_key_payload via a keyctl PKEY_ENCRYPT OOB read; stage a fake epitem there.",
    "3. Build a large ep->refs list (~4000 watchers) so the depth-check walk is long; run ~25,000 priming epoll_ctl calls to advance loop_check_gen near a useful value.",
    "4. On CPU 0, repeatedly call epoll_ctl(ADD) to trigger ep_get_upwards_depth_proc(); on CPU 1, close watcher fds to free their struct eventpoll mid-walk; a third thread fires membarrier(PRIVATE_EXPEDITED) IPIs to stall CPU 0 under preempt=voluntary.",
    "5. Immediately spray the freed eventpoll slot with a user_key_payload whose bytes place a pointer to the stage-2 fake at the epi->ep offset; the walker follows it and writes loop_check_gen into what is actually cred->cap_permitted.",
    "6. The next field (cap_effective) is zero and is misread as an empty ep->refs list head, so the walker returns cleanly without crashing.",
    "7. Repeat until a written counter value has bit 16 (CAP_SYS_MODULE) set; call capset() to copy permitted into effective, then finit_module() an embedded module that commits root creds."
  ],
  "technical_details": "Hi --\n\nI believe I've found another local privilege escalation vulnerability\n\nin the kernel. This one requires no special user permissions.\n\n(As in my prior bugs, I used an LLM to find this, but I validated it\n\nmyself. The exploit, again, is something I don't *fully* understand,\n\nbut I've tried my best to explain whats happening and it's easy to\n\nverify the behavior from input-output testing.)\n\nThe vulnerability is in ep_get_upwards_depth_proc(), in\n\nfs/eventpoll.c. This function walks a linked list ep->refs (while\n\nholding rcu_read_lock()) and dereferences epi->ep on each entry:\n\n    static int ep_get_upwards_depth_proc(struct eventpoll *ep, int depth)\n\n    {\n\n        int result = 0;\n\n        struct epitem *epi;\n\n        if (ep->gen == loop_check_gen)\n\n            return ep->loop_check_depth;\n\n        hlist_for_each_entry_rcu(epi, &ep->refs, fllink)\n\n            result = max(result,\n\n                         ep_get_upwards_depth_proc(epi->ep, depth + 1) + 1);\n\n        ep->gen = loop_check_gen;\n\n        ep->loop_check_depth = result;\n\n        return result;\n\n    }\n\nThe list entries (struct epitem) are freed via kfree_rcu(), so the\n\nlist walk is safe. But epi->ep points at a struct eventpoll, which\n\nis freed by plain kfree() in ep_free() without waiting for the\n\ngrace period:\n\n    static void ep_free(struct eventpoll *ep)\n\n    {\n\n        ep_resume_napi_irqs(ep);\n\n        mutex_destroy(&ep->mtx);\n\n        free_uid(ep->user);\n\n        wakeup_source_unregister(ep->ws);\n\n        kfree(ep);                    /* no grace period */\n\n    }\n\nThe walk runs when one epoll_ctl(A, EPOLL_CTL_ADD, B) call checks\n\nwhether adding B to A's interest list would create deep (or infinite)\n\nnesting. The list being walked is A->refs---the set of epoll\n\ninstances that are watching A. Each entry's epi->ep back-pointer names\n\none of those watchers. If a second thread closes a watcher fd while\n\nthe first is mid-walk, ep_free() can kfree() that watcher's struct\n\neventpoll while the walker still holds a pointer to it. The walker\n\nthen writes loop_check_gen (a u64) and loop_check_depth (a u8) into\n\nfreed memory.\n\nI think this was introduced by f2e467a48287 (\"eventpoll: Fix\n\nsemi-unbounded recursion\", 2025-07-11), which added this upward\n\ndepth check.\n\nThe attached repro-2cpu.c is a minimal crasher. One thread on CPU 0\n\nhammers epoll_ctl(ADD) calls, each of which walks a 4000-entry refs list;\n\na second thread on CPU 1 closes the watchers one by one. Under\n\npreempt=voluntary the walk cannot be scheduler-preempted, but natural\n\nIRQ stalls on CPU 0 give the closer time to free+reallocate the buffer\n\nbefore the IRQ finishes. Under a KASAN-compiled kernel this reports\n\nslab-use-after-free in a few seconds.\n\nI've also attached a second lpe.c that chains this with two other\n\ninfo-leak bugs (which I will report separately) to get root on a HEAD\n\nbuild (as of two days ago---I sent a patch for one of the three bugs\n\nin this chain that recently landed). The attack here has to be a bit\n\nmore clever in order to make sure it can abuse the race\n\nproductively. The chain works (again, I believe!) as follows:\n\n1. An io_uring fdinfo OOB read leaks KASLR base and a pointer into\n\n   the current task's struct cred. (I have filed this bug already,\n\n   and the fix will be merged. But any other KALSR bypass could\n\n   substitute here.)\n\n2. A keyctl PKEY_ENCRYPT OOB read leaks the address of a\n\n   kmalloc-192 slot adjacent to a controlled user_key_payload.\n\n   This is where we will stage a fake epitem that the walker will\n\n   follow---it has to live in kernel memory so SMAP doesn't kill us.\n\n3. The epoll UAF gives a write-at-chosen-address primitive: when the\n\n   closer thread frees a watcher's struct eventpoll, a spray reclaims\n\n   that slot with a user_key_payload whose data places a pointer to\n\n   our stage-2 fake at the epi->ep offset. The walker follows that\n\n   pointer and writes loop_check_gen at the offset where ep->gen\n\n   would be.\n\n4. The value written is (as mentioned) not controllable. But there are\n\n   places in the kernel where writing *any* value gives power, and the\n\n   PoC targets cred->cap_permitted as one of these.\n\n   This target has a convenient property: in struct cred,\n\n   cap_permitted is immediately followed by cap_effective. For an\n\n   unprivileged task, cap_effective is zero. The walker, after\n\n   writing to cap_permitted, reads what it thinks is ep->refs.first\n\n   at the next offset---which is cap_effective---gets zero,\n\n   interprets that as an empty list, and returns cleanly.\n\n   Roughly half of counter values have bit 16 (CAP_SYS_MODULE) set,\n\n   so after a few writes cap_permitted contains that bit, the\n\n   exploit calls capset() to copy permitted into effective, and\n\n   finit_module() loads a kernel module that commits root creds.\n\nThe bulk of the PoC's complexity is in making the race win under\n\npreempt=voluntary, which is the distro default. Under preempt=full the\n\nwalker can be scheduler-preempted mid-walk, and then the closer has\n\nall the time in the world to run their exploit. Under a voluntary\n\nscheduler, the walker only yields to IRQs. So the PoC puts the closer\n\nin a separate process on a different CPU and uses membarrier\n\nPRIVATE_EXPEDITED as an IPI gun from a third thread to storm the\n\nwalker's CPU with interrupts---stalling it long enough for close+spray\n\nto complete. A \"priming\" pass that runs 25,000 non-racing epoll_ctl\n\ncalls first brings loop_check_gen near the threshold, so the race only\n\nneeds to win twice. With these techniques the voluntary version hits\n\nroot in ~20 seconds with fairly high reliability.\n\nI've attached a patch that switches ep_free() to kfree_rcu(). This is\n\nthe smallest change I could find that fixes the vulnerability, but I\n\ndon't know if it's the right change. Feel free to disregard.\n\nThanks,\n\nNicholas\n\nReproduction:\n\n  git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n\n  cd linux\n\n  git checkout 5170efd9c344~1\n\n  make defconfig\n\n  scripts/config -e PKCS8_PRIVATE_KEY_PARSER\n\n  make olddefconfig\n\n  make -j$(nproc) bzImage modules\n\n  # Build the embedded kernel module for finit_module (lpe.c embeds the\n\n  # bytes via pwnmod_ko.h, but vermagic must match the kernel you just\n\n  # built):\n\n  mkdir pwnmod && cp pwnmod.c pwnmod/\n\n  echo 'obj-m := pwnmod.o' > pwnmod/Makefile\n\n  make -C . M=$PWD/pwnmod modules\n\n  xxd -i pwnmod/pwnmod.ko | sed 's/pwnmod_pwnmod_ko/pwnmod_ko/' > pwnmod_ko.h\n\n  # Update the io_uring_fops offset in lpe.c for this build:\n\n  FOPS=$(nm vmlinux | awk '/ io_uring_fops$/{print $1}')\n\n  sed -i \"s/^#define KSTATIC_IO_URING_FOPS.*/#define\n\nKSTATIC_IO_URING_FOPS  0x${FOPS}UL/\" lpe.c\n\n  # Alpine nocloud image + cloud-init seed\n\n  wget https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/cloud/nocloud_alpine-3.23.0-x86_64-bios-cloudinit-metal-r0.qcow2\n\n-O alpine.qcow2\n\n  cat > user-data <<'EOF'\n\n  #cloud-config\n\n  disable_root: false\n\n  ssh_pwauth: true\n\n  chpasswd:\n\n    expire: false\n\n    list: |\n\n      root:test\n\n  runcmd:\n\n    - sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config\n\n    - rc-service sshd restart\n\n  EOF\n\n  printf 'instance-id: x\\nlocal-hostname: x\\n' > meta-data\n\n  genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data\n\n  qemu-img create -f qcow2 -F qcow2 -b alpine.qcow2 disk.qcow2 4G\n\n  qemu-system-x86_64 -enable-kvm -m 2G -smp 4 -nographic \\\n\n    -kernel arch/x86/boot/bzImage \\\n\n    -append \"root=/dev/vda rw console=ttyS0 preempt=voluntary rootwait\" \\\n\n    -drive file=disk.qcow2,if=virtio \\\n\n    -drive file=seed.iso,if=virtio,format=raw \\\n\n    -netdev user,id=n0,hostfwd=tcp::2222-:22 -device virtio-net,netdev=n0\n\n  # on the host (lpe.c needs rsa2048.h and pwnmod_ko.h in the same dir):\n\n  gcc -O2 -static -pthread -o lpe lpe.c\n\n  scp -P 2222 lpe root@localhost:/tmp/     # password 'test'\n\n  # ssh -p 2222 root@localhost\n\n  adduser -D testuser\n\n  su testuser -c \"FORCE_2CPU=1 /tmp/lpe\"\n\n  # [+] KBASE = 0xffffffff81000000 (io_uring_fops = ...)\n\n  # [+]  K1 @ g_k1_addr=0xffff... via idx=30 (j+4, marker-verified, tried 7)\n\n  # [.] priming loop_check_gen (~50k, 25000 adds)...\n\n  # [.]  r=0 prm=0000e39f/00000000\n\n  # [.]  r=1 prm=00010465/00000000\n\n  # [+++] CAP_SYS_MODULE in permitted! (round 1)\n\n  # [++++++] PWNED -- uid=0 via init_module\n\n  # uid=0(root) gid=0(root)\n\nFor the crasher, rebuild with KASAN (scripts/config -e KASAN -e\n\nKASAN_GENERIC, same steps otherwise), scp repro-2cpu, and run it\n\nunprivileged; expect a slab-use-after-free splat in\n\nep_get_upwards_depth_proc within a few seconds.",
  "title": "UAF in ep_get_upwards_depth_proc() giving unprivileged LPE",
  "vendor_severity": null
}