
Unprivileged proof-of-concept and x86_64 local privilege escalation for a Linux kernel Binder use-after-free (CVE-2026-64468), with KASAN lab and vulnerable/fixed differential.
binder_free_transaction() process-lifetime use-after-free
This repository contains, for the Linux kernel Binder use-after-free fixed by
upstream commit
f223d27a546c1e1f48d38fd67760e78f068fe8c4:
binder_chain_64468.c — an unprivileged proof of concept that reaches the
bug and lets the kernel prove it, with a KASAN laboratory and a
vulnerable/fixed differential (lab/, run.sh, verify.sh).exploit.c — a self-contained local privilege escalation for x86_64.
It compiles with gcc -O2 -pthread -o exploit exploit.c, runs as an ordinary
user, and ends in a root shell.demo/ — a laboratory that boots a real Debian 13 userland on an
unpatched kernel, so the exploit can be compiled by the target's own gcc and
run on the machine it then takes over.Everything runs as an ordinary user (uid/gid 1000, no capabilities, no namespaces) against stock upstream kernels with no patches of any kind.
Warning
This code deliberately races kernel object lifetimes and then hijacks kernel control flow. A lost race corrupts kernel heap state and can panic or hang the machine. Run it only in an isolated, disposable VM that you own. Do not run it on a host.
| State | Claim |
|---|---|
| Confirmed | The vulnerability is real, is reachable from an unprivileged process, and the upstream fix removes it. |
| Demonstrated | The vulnerable dereference of a dying binder_proc is reached naturally and repeatedly on the unpatched kernel, and never on the fixed kernel. |
| Demonstrated | The full use-after-free, reported by KASAN, on the unpatched kernel. |
| Demonstrated | Reclaim of the freed binder_proc with attacker-controlled bytes, kernel control-flow hijack, and privilege escalation to uid 0 from an unprivileged user, on x86_64. |
| Not claimed | Any result on a specific vendor or Android device. Only the upstream kernels listed below were tested, on x86_64. |
| Not claimed | That the shipped exploit works unmodified against a distribution kernel. See Which systems are affected: it needs per-kernel offsets, and on every general-purpose distribution surveyed the binder device is not reachable by an unprivileged user in the first place. |
binder_free_transaction() reads the target process out of the transaction
under t->lock, drops that lock, and then acquires the target's inner lock:
spin_lock(&t->lock);
target_proc = t->to_proc;
spin_unlock(&t->lock);
if (target_proc) {
binder_inner_proc_lock(target_proc); /* use after free */
Nothing keeps target_proc alive across that gap. A process that is being torn
down in parallel can reach binder_proc_dec_tmpref() -> kfree() in between, so
the lock is taken on freed memory. The upstream fix pins t->to_thread while
t->lock is still held, which keeps the owning process alive until the inner
lock has been used and released.
The vulnerability was reported by Alice Ryhl and fixed by Carlos Llamas, both of Google. The original report carries the reference KASAN trace.
The only caller that can reach a foreign to_proc is
binder_send_failed_reply(), and it only walks to t->from_parent when
t->from is NULL:
target_thread = binder_get_txn_from_and_acq_inner(t);
if (target_thread) { ...; binder_free_transaction(t); return; }
next = t->from_parent;
binder_free_transaction(t);
t = next;
from_parent is assigned in exactly one place, and the assignment is guarded a
few lines earlier by binder's bad transaction stack check: a thread may only
send a synchronous transaction while its stack top is a transaction it is
receiving. Therefore, for every link of that chain, the child's sender and the
parent's receiver are the same thread.
That has a sharp consequence. binder_thread_release() walks the dying
thread's stack with proc->inner_lock held, and writes both of
iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock
spin_lock(parent->lock)
iteration j+1 : [holds parent->lock] parent->to_proc = NULL
in adjacent iterations of a single walk, each under the respective
t->lock. A walker learns child->from == NULL only once the walk has released
child->lock, and needs parent->lock for its own snapshot. So the entire
opportunity is the gap between that walk's spin_unlock(&child->lock) and its
spin_lock(&parent->lock) — a couple of instructions. The release walk holds a
spinlock and cannot be preempted there; only an interrupt can delay it.
This is why the race is narrow and why both the proof of concept and the exploit are probabilistic.
binder_chain_64468.c constructs the shortest chain that reaches the
vulnerable access, so the walker's work inside that gap is as small as binder
allows — two t->lock acquisitions and one kfree(), with no foreign
inner_proc_lock, no wake_up and no reply delivery:
B thread i --e2 (sync, code 0x4442414b)--> P thread Y_i
P thread Y_i --e1 (sync, code 0x54414c4c)--> B thread i (nested target)
B thread i stack: [ e2 outgoing , e1 incoming (top) ]
P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]
P then drops its binder fd, so binder_deferred_release() releases
Y_1..Y_K and finally frees the binder_proc, while every B thread
concurrently issues BINDER_THREAD_EXIT:
binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from
binder_send_failed_reply(e1) e1->from == NULL once Y_i was released
-> binder_free_transaction(e1) target_proc already NULL, kfree(e1)
-> binder_free_transaction(e2) target_proc == P <-- vulnerable access
A third process is the binder context manager, used only to hand out the
handles the other two need. exploit.c reuses this exact construction.
A KASAN report needs both of:
parent->lock inside the
narrow window above, so that it snapshots a still-live to_proc; andt->lock and taking the victim's inner lock, and stay off it
until the deferred release has finished and freed the binder_proc.The first condition has its own oracle that does not need KASAN: binder prints
binder: binder_free_proc: Unexpected outstanding_txns -1
whenever it happens, because the walker and binder_thread_release() then both
decrement the same counter for one transaction. Note this is not a
vulnerable/fixed differential by itself — the fix stops the free, not the second
decrement — so it appears on both kernels. It is used here only to show the
vulnerable code path being exercised.
The proof of concept stops at a 4-byte decrement of freed memory. Turning that into uid 0 needs four things, and none of them come from the bug itself: the bug leaks nothing.
struct binder_proc is 648 bytes and is allocated with a plain GFP_KERNEL
kzalloc — not __GFP_ACCOUNT. It therefore lands in kmalloc-1k,
together with every other unaccounted allocation of that size, and is not
isolated behind kmalloc-cg-*. That single fact is what makes the object
reclaimable at all.