[PATCH 3/4] rust_binder: cap set_max_threads at RLIMIT_NPROC
From: Yunseong Kim
Date: Wed Jun 03 2026 - 14:04:48 EST
The Rust binder's set_max_threads() stores the user-provided u32 value
directly into proc.max_threads without any bounds checking. This is the
same bug as in the C binder: an unprivileged process can set max_threads
to 0xFFFFFFFF, enabling unlimited binder thread spawning that bypasses
RLIMIT_NPROC and leads to OOM.
Cap max_threads at the calling task's RLIMIT_NPROC by reading
current->signal->rlim[RLIMIT_NPROC].rlim_cur, following the same
pattern used by io_uring (io-wq.c:1488) to limit its worker threads.
kcov-dataflow tracking (before):
ENTRY Process::ioctl(cmd=BINDER_SET_MAX_THREADS)
ENTRY set_max_threads(max=0xffffffff)
self.inner.lock().max_threads = 0xffffffff ← stored directly
RET set_max_threads() ← void, no error
kcov-dataflow tracking (after):
ENTRY Process::ioctl(cmd=BINDER_SET_MAX_THREADS)
ENTRY set_max_threads(max=0xffffffff)
nproc_limit = current->signal->rlim[NPROC] = 50
0xffffffff > 50 ← rejected
RET set_max_threads() = Err(EINVAL)
Reproduction:
# CONFIG_ANDROID_BINDER_IPC_RUST=y
$ ulimit -u 50
$ ./To-Ulimit-and-Beyond
BINDER_SET_MAX_THREADS = 0xFFFFFFFF: accepted (before) / rejected (after)
Link: https://lore.kernel.org/all/20260603-kcov-dataflow-next-20260603-v2-0-fee0939de2c4@xxxxxxxx/
Signed-off-by: Yunseong Kim <yunseong.kim@xxxxxxxx>
---
drivers/android/binder/process.rs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/drivers/android/binder/process.rs b/drivers/android/binder/process.rs
index 96b8440ceac6..4752b84505bd 100644
--- a/drivers/android/binder/process.rs
+++ b/drivers/android/binder/process.rs
@@ -1139,8 +1139,19 @@ fn remove_thread(&self, thread: Arc<Thread>) {
thread.release();
}
- fn set_max_threads(&self, max: u32) {
+ fn set_max_threads(&self, max: u32) -> Result {
+ // Cap at the calling task's RLIMIT_NPROC, following the same pattern
+ // as io_uring (io-wq.c) for limiting kernel-side thread creation.
+ // SAFETY: current is valid, signal and rlim are always initialized.
+ let nproc_limit = unsafe {
+ let task = kernel::current!().as_ptr();
+ (*(*(*task).signal).rlim.as_ptr().add(bindings::RLIMIT_NPROC as usize)).rlim_cur
+ };
+ if (max as u64) > (nproc_limit as u64) {
+ return Err(EINVAL);
+ }
self.inner.lock().max_threads = max;
+ Ok(())
}
fn set_oneway_spam_detection_enabled(&self, enabled: u32) {
@@ -1574,7 +1585,7 @@ fn ioctl_write_only(
) -> Result {
let thread = this.get_current_thread()?;
match cmd {
- uapi::BINDER_SET_MAX_THREADS => this.set_max_threads(reader.read()?),
+ uapi::BINDER_SET_MAX_THREADS => this.set_max_threads(reader.read()?)?,
uapi::BINDER_THREAD_EXIT => this.remove_thread(thread),
uapi::BINDER_SET_CONTEXT_MGR => this.set_as_manager(None, &thread)?,
uapi::BINDER_SET_CONTEXT_MGR_EXT => {
--
2.43.0