
Exploit for CVE-2026-46215, a Linux kernel DRM GEM use-after-free local privilege escalation. Uses racing, slab spraying, and Dirty Pipe-style file overwrite to turn an unprivileged render-node user into passwordless root.
change_handle Use-After-Free (unprivileged LPE)Local privilege escalation via a use-after-free in the DRM core ioctl
DRM_IOCTL_GEM_CHANGE_HANDLE (drm_gem_change_handle_ioctl, ioctl nr 0xD2).
Reachable by any user with access to a render node (/dev/dri/renderD*,
granted to the active session by systemd-logind on all major desktop
distributions). The chain in this repo takes the UAF to passwordless root from
an unprivileged user.
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)53096728b891 ("drm: Add DRM prime interface to reassign GEM handle", David Francis / AMD), added for AMD's CRIU work5e28b7b94408). The ioctl is also being disabled upstream in 7.1 because of this and related races.This bug was first reported by Puttimet Thammasaeng, who holds the upstream
Reported-by credit on the fix. I found and reported it independently to
[email protected] on 2026-04-12. My report was acknowledged and forwarded to
the maintainers, but the earlier report is the one credited upstream. This repo
contains my own analysis and exploit.
Writeup: https://cyberstan.co.uk
The fix is in released stable kernels (6.18.32 and 7.0.9 onwards). This repo was published after fixes became broadly available.
drm_gem_change_handle_ioctl() moves a GEM object from one handle to another
but never adjusts obj->handle_count. It also skips drm_vma_node_allow/revoke
and the driver open/close callbacks. Because handle_count stays at 1, a
concurrent GEM_CLOSE on the old handle drops it to 0 and frees the object
while the new handle still references it in the IDR. That dangling handle is the
use-after-free, later dereferenced in drm_gem_object_release_handle().
GEM_CHANGE_HANDLE against GEM_CLOSE to get a dangling handle.pipe_buffer array
(msg_msg feng shui to condition kmalloc-512, then splice-filled pipes).pipe_buf_ops through a driver info ioctl: obj->size (offset 216)
overlaps pipe_buf[5].ops, giving a kernel pointer and the KASLR base.obj->name (offset 224) lands on
pipe_buf[5].flags and sets PIPE_BUF_FLAG_CAN_MERGE (name = 16 = 0x10)./etc/passwd, root becomes passwordless.Offsets are verified via pahole and are specific to the layout across 6.18 to
7.0. Override the GEM_* / PIPEBUF_* defines for other kernels.
poc.c - the exploit. Build static with -lpthread.run_exploit.sh - builds the PoC and a minimal initramfs, boots it in QEMU.qemu-system-x86_64, gcc, busybox (static), fakeroot, cpio, gzip.
KVM (/dev/kvm) is recommended; the race is far more reliable with it.
The exploit needs a vulnerable target built with specific options:
CONFIG_KASAN must be OFF. KASAN quarantines freed slabs and blocks the
pipe-spray reclaim, so the exploit will not work against a KASAN build.virtio_gpu (used by the demo) or nouveau.CONFIG_DRM=y, CONFIG_DRM_VIRTIO_GPU=y, CONFIG_DEVTMPFS=y,
CONFIG_BLK_DEV_INITRD=y.Steps:
CONFIG_KASAN is not set.make -j"$(nproc)" bzImage, producing arch/x86/boot/bzImage.The demo boots with nokaslr so the leaked pointer is deterministic. The leak
defeats KASLR on its own, so KASLR-on also works, the address just varies per
boot.
Look at drivers/gpu/drm/drm_gem.c, function drm_gem_change_handle_ioctl().
Quick check:
awk '/^int drm_gem_change_handle_ioctl/,/^}/' \
drivers/gpu/drm/drm_gem.c | grep -c handle_count
0 means VULNERABLE (no refcount handling).By structure:
file_priv->prime.lock, a single
idr_alloc(&file_priv->object_idr, obj, ...), then idr_remove() on the old
handle. No drm_gem_object_handle_get.idr_alloc then idr_replace(NULL) to detach the
old handle), with the real object only swapped in once the prime operations
succeed../run_exploit.sh /path/to/bzImage
It builds poc.c, wraps it in an initramfs, and boots QEMU with
-device virtio-gpu-pci and nokaslr. The PoC runs as uid 1000 (unprivileged),
then the script prints /etc/passwd before and after and drops you to a shell.
Leave the VM with poweroff -f or Ctrl-A X.
[!] Race won (iter 977): handle=132049
[!] KASLR: pipe_buf_ops = 0xffffffff82428400
[!] EXPLOIT SUCCESSFUL
[!] FLINK: 16 = 0x10
[*] /etc/passwd:
root::0:0:pwned:/root:/bin/sh
[!] LPE CONFIRMED
[!] root account is now passwordless
A read-only (chmod 444) /etc/passwd owned by root is overwritten by an
unprivileged process. The root: line loses its password field.
Rebuild against a patched tree (6.18.32, 7.0.9, 7.1-rc3 or later, or any tree that passes the patched check above) and re-run:
./run_exploit.sh /path/to/patched-bzImage
Against the patched kernel the exploit should never reach "EXPLOIT SUCCESSFUL": the race no longer frees the object out from under the new handle, so the leak never returns a valid kernel pointer.
Published for research and defensive purposes after upstream fixes shipped. Provided as-is. Run only against VMs you control.