kernel/futex/syscalls.c | 102 ++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 50 deletions(-)
sys_get_robust_list() and compat_get_robust_list() use
ptrace_may_access() to check if the calling task is allowed to access
another task's robust_list pointer. This check is racy against a
concurrent exec() in the target process.
During exec(), a task may transition from a non-privileged binary to a
privileged one (e.g., setuid binary) and its credentials/memory mappings
may change. If get_robust_list() performs ptrace_may_access() before
this transition, it may erroneously allow access to sensitive information
after the target becomes privileged.
A racy access allows an attacker to exploit a window
during which ptrace_may_access() passes before a target process
transitions to a privileged state via exec().
For example, consider a non-privileged task T that is about to execute a
setuid-root binary. An attacker task A calls get_robust_list(T) while T
is still unprivileged. Since ptrace_may_access() checks permissions
based on current credentials, it succeeds. However, if T begins exec
immediately afterwards, it becomes privileged and may change its memory
mappings. Because get_robust_list() proceeds to access T->robust_list
without synchronizing with exec() it may read user-space pointers from a
now-privileged process.
This violates the intended post-exec access restrictions and could
expose sensitive memory addresses or be used as a primitive in a larger
exploit chain. Consequently, the race can lead to unauthorized
disclosure of information across privilege boundaries and poses a
potential security risk.
Take a read lock on signal->exec_update_lock prior to invoking
ptrace_may_access() and accessing the robust_list/compat_robust_list.
This ensures that the target task's exec state remains stable during the
check, allowing for consistent and synchronized validation of
credentials.
Signed-off-by: Pranav Tyagi <pranav.tyagi03@gmail.com>
Suggested-by: Jann Horn <jann@thejh.net>
Link: https://lore.kernel.org/linux-fsdevel/1477863998-3298-5-git-send-email-jann@thejh.net/
Link: https://github.com/KSPP/linux/issues/119
---
changed in v3:
- replaced RCU with scoped_guard(rcu)
- corrected error return type cast
- added IS_ENABLED(CONFIG_COMPAT) for ensuring compatability
- removed stray newlines and unnecessary line breaks
changed in v2:
- improved changelog
- helper function for common part of compat and native syscalls
kernel/futex/syscalls.c | 102 ++++++++++++++++++++--------------------
1 file changed, 52 insertions(+), 50 deletions(-)
diff --git a/kernel/futex/syscalls.c b/kernel/futex/syscalls.c
index 4b6da9116aa6..c342c16d6d00 100644
--- a/kernel/futex/syscalls.c
+++ b/kernel/futex/syscalls.c
@@ -39,6 +39,52 @@ SYSCALL_DEFINE2(set_robust_list, struct robust_list_head __user *, head,
return 0;
}
+static void __user *get_robust_list_common(int pid, bool compat)
+{
+ struct task_struct *p;
+ void __user *head;
+ unsigned long ret;
+
+ p = current;
+
+ scoped_guard(rcu) {
+ if (pid) {
+ p = find_task_by_vpid(pid);
+ if (!p)
+ return (void __user *)ERR_PTR(-ESRCH);
+ }
+ get_task_struct(p);
+ }
+
+ /*
+ * Hold exec_update_lock to serialize with concurrent exec()
+ * so ptrace_may_access() is checked against stable credentials
+ */
+ ret = down_read_killable(&p->signal->exec_update_lock);
+ if (ret)
+ goto err_put;
+
+ ret = -EPERM;
+ if (!ptrace_may_access(p, PTRACE_MODE_READ_REALCREDS))
+ goto err_unlock;
+
+ if (IS_ENABLED(CONFIG_COMPAT) && compat)
+ head = p->compat_robust_list;
+ else
+ head = p->robust_list;
+
+ up_read(&p->signal->exec_update_lock);
+ put_task_struct(p);
+
+ return head;
+
+err_unlock:
+ up_read(&p->signal->exec_update_lock);
+err_put:
+ put_task_struct(p);
+ return (void __user *)ERR_PTR(ret);
+}
+
/**
* sys_get_robust_list() - Get the robust-futex list head of a task
* @pid: pid of the process [zero for current task]
@@ -49,36 +95,14 @@ SYSCALL_DEFINE3(get_robust_list, int, pid,
struct robust_list_head __user * __user *, head_ptr,
size_t __user *, len_ptr)
{
- struct robust_list_head __user *head;
- unsigned long ret;
- struct task_struct *p;
+ struct robust_list_head __user *head = get_robust_list_common(pid, false);
- rcu_read_lock();
-
- ret = -ESRCH;
- if (!pid)
- p = current;
- else {
- p = find_task_by_vpid(pid);
- if (!p)
- goto err_unlock;
- }
-
- ret = -EPERM;
- if (!ptrace_may_access(p, PTRACE_MODE_READ_REALCREDS))
- goto err_unlock;
-
- head = p->robust_list;
- rcu_read_unlock();
+ if (IS_ERR(head))
+ return PTR_ERR(head);
if (put_user(sizeof(*head), len_ptr))
return -EFAULT;
return put_user(head, head_ptr);
-
-err_unlock:
- rcu_read_unlock();
-
- return ret;
}
long do_futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
@@ -455,36 +479,14 @@ COMPAT_SYSCALL_DEFINE3(get_robust_list, int, pid,
compat_uptr_t __user *, head_ptr,
compat_size_t __user *, len_ptr)
{
- struct compat_robust_list_head __user *head;
- unsigned long ret;
- struct task_struct *p;
+ struct compat_robust_list_head __user *head = get_robust_list_common(pid, true);
- rcu_read_lock();
-
- ret = -ESRCH;
- if (!pid)
- p = current;
- else {
- p = find_task_by_vpid(pid);
- if (!p)
- goto err_unlock;
- }
-
- ret = -EPERM;
- if (!ptrace_may_access(p, PTRACE_MODE_READ_REALCREDS))
- goto err_unlock;
-
- head = p->compat_robust_list;
- rcu_read_unlock();
+ if (IS_ERR(head))
+ return PTR_ERR(head);
if (put_user(sizeof(*head), len_ptr))
return -EFAULT;
return put_user(ptr_to_compat(head), head_ptr);
-
-err_unlock:
- rcu_read_unlock();
-
- return ret;
}
#endif /* CONFIG_COMPAT */
--
2.49.0
On Tue, Aug 05 2025 at 21:17, Pranav Tyagi wrote: > + > + if (IS_ENABLED(CONFIG_COMPAT) && compat) > + head = p->compat_robust_list; This still does not compile because the dead code elimination comes _after_ the compiler decodes this line. I don't even need to fire up a compiler to predict the error emitted when CONFIG_COMPAT=n: error: ‘struct task_struct’ has no member named ‘compat_robust_list’ No? There is a reason why I suggested you to use that helper function. You are obviously free to ignore me, but then please make sure that the stuff you submit compiles _AND_ works. Otherwise if you are not sure, why I told you, ask. Please take your time and stop rushing out half baken crap, which wastes everybodys time. I don't care about your time wasted, but I pretty much care about mine. To be clear: I don't want to see this in my inbox again before next week and then it better be correct. Thanks, tglx
On Wed, Aug 6, 2025 at 3:17 AM Thomas Gleixner <tglx@linutronix.de> wrote: > > On Tue, Aug 05 2025 at 21:17, Pranav Tyagi wrote: > > + > > + if (IS_ENABLED(CONFIG_COMPAT) && compat) > > + head = p->compat_robust_list; > > This still does not compile because the dead code elimination comes > _after_ the compiler decodes this line. I don't even need to fire up a > compiler to predict the error emitted when CONFIG_COMPAT=n: > > error: ‘struct task_struct’ has no member named ‘compat_robust_list’ > > No? > > There is a reason why I suggested you to use that helper function. > > You are obviously free to ignore me, but then please make sure that the > stuff you submit compiles _AND_ works. Otherwise if you are not sure, > why I told you, ask. > > Please take your time and stop rushing out half baken crap, which wastes > everybodys time. I don't care about your time wasted, but I pretty much > care about mine. > > To be clear: I don't want to see this in my inbox again before next week > and then it better be correct. > > Thanks, > > tglx > > > Hello Sir, There is no question of ignoring you. It is my privilege to be communicating with you. Your guidance in the course of this specific patch has only enhanced my understanding. I always try to respect everyone's time and agree that I hurried the last one as time of my mentorship program is closing fast. My apologies. I have again sent the reworked patch (v4) duly corrected based on your observations. I have compiled and checked it with CONFIG_COMPAT=n. And to the extent my little wisdom allowed, I have tested it using the following custom testing code on a virtual machine: #define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> #include <linux/futex.h> #include <linux/unistd.h> #include <stdio.h> int main() { pid_t pid = 0; // 0 = self struct robust_list_head *head; size_t len; long ret; ret = syscall(SYS_get_robust_list, pid, &head, &len); if (ret == -1) { perror("get_robust_list"); return 1; } printf("Robust list head: %p, length: %zu\n", head, len); return 0; } Regards Pranav Tyagi
Hi Pranav, kernel test robot noticed the following build errors: [auto build test ERROR on tip/locking/core] [also build test ERROR on linus/master v6.16 next-20250808] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in https://git-scm.com/docs/git-format-patch#_base_tree_information] url: https://github.com/intel-lab-lkp/linux/commits/Pranav-Tyagi/futex-don-t-leak-robust_list-pointer-on-exec-race/20250806-121303 base: tip/locking/core patch link: https://lore.kernel.org/r/20250805154725.22031-1-pranav.tyagi03%40gmail.com patch subject: [PATCH v3] futex: don't leak robust_list pointer on exec race config: loongarch-allyesconfig (https://download.01.org/0day-ci/archive/20250809/202508090125.VIm8fAXD-lkp@intel.com/config) compiler: clang version 22.0.0git (https://github.com/llvm/llvm-project 7b8dea265e72c3037b6b1e54d5ab51b7e14f328b) reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20250809/202508090125.VIm8fAXD-lkp@intel.com/reproduce) If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags | Reported-by: kernel test robot <lkp@intel.com> | Closes: https://lore.kernel.org/oe-kbuild-all/202508090125.VIm8fAXD-lkp@intel.com/ All errors (new ones prefixed by >>): >> kernel/futex/syscalls.c:72:13: error: no member named 'compat_robust_list' in 'struct task_struct' 72 | head = p->compat_robust_list; | ~ ^ 1 error generated. vim +72 kernel/futex/syscalls.c 41 42 static void __user *get_robust_list_common(int pid, bool compat) 43 { 44 struct task_struct *p; 45 void __user *head; 46 unsigned long ret; 47 48 p = current; 49 50 scoped_guard(rcu) { 51 if (pid) { 52 p = find_task_by_vpid(pid); 53 if (!p) 54 return (void __user *)ERR_PTR(-ESRCH); 55 } 56 get_task_struct(p); 57 } 58 59 /* 60 * Hold exec_update_lock to serialize with concurrent exec() 61 * so ptrace_may_access() is checked against stable credentials 62 */ 63 ret = down_read_killable(&p->signal->exec_update_lock); 64 if (ret) 65 goto err_put; 66 67 ret = -EPERM; 68 if (!ptrace_may_access(p, PTRACE_MODE_READ_REALCREDS)) 69 goto err_unlock; 70 71 if (IS_ENABLED(CONFIG_COMPAT) && compat) > 72 head = p->compat_robust_list; 73 else 74 head = p->robust_list; 75 76 up_read(&p->signal->exec_update_lock); 77 put_task_struct(p); 78 79 return head; 80 81 err_unlock: 82 up_read(&p->signal->exec_update_lock); 83 err_put: 84 put_task_struct(p); 85 return (void __user *)ERR_PTR(ret); 86 } 87 -- 0-DAY CI Kernel Test Service https://github.com/intel/lkp-tests/wiki
© 2016 - 2025 Red Hat, Inc.