cross memory attach && security check
Subsystems (5)
Hello,
Just noticed the new file in mm/ ;) A couple of questions.
process_vm_rw_core() does
task_lock(task);
if (__ptrace_may_access(task, PTRACE_MODE_ATTACH)) {
task_unlock(task);
rc = -EPERM;
goto put_task_struct;
}
mm = task->mm;this is racy, task_lock() can't help. And I don't think you should use it directly.
execve() does exec_mmap() first, this switches to the new ->mm. After that install_exec_creds() changes task->cred. The window is not that small.
I guess you need ->cred_guard_mutex, please look at mm_for_maps().
Another question, process_vm_rw_pages() does get_user_pages() without FOLL_FORCE. Is this on purpose? This limits the usage of the new syscalls.
Hmm. And could you please explain the change in rw_copy_check_uvector()? Why process_vm_rw() doesrw_copy_check_uvector(READ, rvec, check_access => 0) ?
Oleg.
Quoted 12 lines · On Thu, 5 Jan 2012 16:10:12 +0100 Oleg Nesterov <[email protected]> wrote:
Just noticed the new file in
mm/;) A couple of questions.
process_vm_rw_core()doestask_lock(task); if (__ptrace_may_access(task, PTRACE_MODE_ATTACH)) { task_unlock(task); rc = -EPERM; goto put_task_struct; } mm = task->mm;
Quoted 9 lines
this is racy,
task_lock()can't help. And I don't think you should use it directly.
execve()doesexec_mmap()first, this switches to the new ->mm. After thatinstall_exec_creds()changestask->cred. The window is not that small.I guess you need ->cred_guard_mutex, please look at
mm_for_maps().
Thanks, agreed this looks like it's a problem. Need to do a bit more testing, but I think the following patch fixes the race?
process_vm_access.c | 7 +++++++ 1 file changed, 7 insertions(+)
mm/process_vm_access.c +7 -0
@@ -298,9 +298,14 @@ static ssize_t process_vm_rw_core(pid_t pid, const struct iovec *lvec, goto free_proc_pages; } + rc =mutex_lock_interruptible (&task->signal->cred_guard_mutex); +if (rc) + goto put_task_struct; +task_lock (task);if (__ptrace_may_access (task,PTRACE_MODE_ATTACH )) {task_unlock (task); +mutex_unlock (&task->signal->cred_guard_mutex); rc = -EPERM ; goto put_task_struct; }
@@ -308,12 +313,14 @@ static ssize_t process_vm_rw_core(pid_t pid, const struct iovec *lvec,if (!mm || (task->flags &PF_KTHREAD )) {task_unlock (task); +mutex_unlock (&task->signal->cred_guard_mutex); rc = -EINVAL ; goto put_task_struct; }atomic_inc (&mm->mm_users);task_unlock (task); +mutex_unlock (&task->signal->cred_guard_mutex);for (i =0 ; i < riovcnt && iov_l_curr_idx < liovcnt; i++) { rc = process_vm_rw_single_vec(
Quoted 3 lines
Another question,
process_vm_rw_pages()doesget_user_pages()withoutFOLL_FORCE. Is this on purpose? This limits the usage of the new syscalls.
Other than reading the comment for get_user_pages saying that I don't want to set the force flag, I didn't really consider it. The use cases where I'm interested (intranode communication) has the cooperation of the target process anyway so its not needed. Any downsides to having FOLL_FORCE enabled?
Quoted 3 lines
Hmm. And could you please explain the change in
rw_copy_check_uvector()? Whyprocess_vm_rw()doesrw_copy_check_uvector(READ, rvec, check_access => 0)?
process_vm_readv/writev get passed an iovec for another process which describes where to read/write from/to. So we need to do part of what rw_copy_check_uvector ordinarily does (validate and copy the iovec data), but we don't want to check the memory it points to at this point because it refers to memory in the other process (this check is done later).
So the change to rw_copy_check_uvector to optionally not do this second check allows us to reuse the code rather than have another function which is almost identical.
Regards,
Chris
Signature · 1 line
[email protected]
Quoted 31 lines · Christopher Yeoh
Quoted 12 lines · On Thu, 5 Jan 2012 16:10:12 +0100 Oleg Nesterov <[email protected]> wrote:
Just noticed the new file in
mm/;) A couple of questions.
process_vm_rw_core()doestask_lock(task); if (__ptrace_may_access(task, PTRACE_MODE_ATTACH)) { task_unlock(task); rc = -EPERM; goto put_task_struct; } mm = task->mm;Quoted 9 lines
this is racy,
task_lock()can't help. And I don't think you should use it directly.
execve()doesexec_mmap()first, this switches to the new ->mm. After thatinstall_exec_creds()changestask->cred. The window is not that small.I guess you need ->cred_guard_mutex, please look at
mm_for_maps().Thanks, agreed this looks like it's a problem. Need to do a bit more testing, but I think the following patch fixes the race?
mm/process_vm_access.c +0
-0@@ -298,9 +298,14 @@ static ssize_t process_vm_rw_core(pid_t pid, const struct iovec *lvec, goto free_proc_pages;
mm/process_vm_access.c static ssize_t process_vm_rw_core(pid_t pid, const struct iovec *lvec, · line 308
Quoted 10 lines
mm/process_vm_access.c +5
-0} + rc =mutex_lock_interruptible (&task->signal->cred_guard_mutex); +if (rc) + goto put_task_struct; +task_lock (task);if (__ptrace_may_access (task,PTRACE_MODE_ATTACH )) {task_unlock (task); +mutex_unlock (&task->signal->cred_guard_mutex);
Yes, I think this works, but I don't think you should play with task_lock() or ->mm_users, just use get_task_mm(). Better yet, can't we do
--- x/fs/proc/base.c
+++ x/fs/proc/base.c
@@ -254,22 +254,7 @@ static struct mm_struct *check_mem_permission(struct task_struct *task)
struct mm_struct *mm_for_maps(struct task_struct *task)
{
- struct mm_struct *mm;
- int err;
-
- err = mutex_lock_killable(&task->signal->cred_guard_mutex);
- if (err)
- return ERR_PTR(err);
-
- mm = get_task_mm(task);
- if (mm && mm != current->mm &&
- !ptrace_may_access(task, PTRACE_MODE_READ)) {
- mmput(mm);
- mm = ERR_PTR(-EACCES);
- }
- mutex_unlock(&task->signal->cred_guard_mutex);
-
- return mm;
+ return get_check_task_mm(task, PTRACE_MODE_READ);
}
static int proc_pid_cmdline(struct task_struct *task, char * buffer)
--- x/kernel/fork.c
+++ x/kernel/fork.c
@@ -644,6 +644,25 @@ struct mm_struct *get_task_mm(struct task_struct *task)
}
EXPORT_SYMBOL_GPL(get_task_mm);
+struct mm_struct *get_check_task_mm(struct task_struct *task, unsigned int mode)
+{
+ struct mm_struct *mm;
+ int err;
+
+ err = mutex_lock_killable(&task->signal->cred_guard_mutex);
+ if (err)
+ return ERR_PTR(err);
+
+ mm = get_task_mm(task);
+ if (mm && mm != current->mm && !ptrace_may_access(task, mode)) {
+ mmput(mm);
+ mm = ERR_PTR(-EACCES);
+ }
+ mutex_unlock(&task->signal->cred_guard_mutex);
+
+ return mm;
+}
+
/* Please note the differences between mmput and mm_release.
* mmput is called whenever we stop holding onto a mm_struct,
* error success whatever.?
Then process_vm_rw_core() can use get_check_task_mm(PTRACE_MODE_ATTACH).
Quoted 4 lines
Other than reading the comment for
get_user_pagessaying that I don't want to set the force flag, I didn't really consider it. The use cases where I'm interested (intranode communication) has the cooperation of the target process anyway so its not needed. Any downsides to havingFOLL_FORCEenabled?
Without FOLL_FORCE, say, gdb can't use the new syscall to set the breakpoint or to read the !VM_READ mappings. OK, process_vm_rw() has flags, we can add PROCESS_VM_FORCE if needed.
Quoted 5 lines
Quoted 3 lines
Hmm. And could you please explain the change in
rw_copy_check_uvector()? Whyprocess_vm_rw()doesrw_copy_check_uvector(READ, rvec, check_access => 0)?process_vm_readv/writev get passed an iovec for another process
Ah. Thanks I see. And I didn't realize that rvec means "remote vec". Partly I was confused because (I guess) there is another minor bug in process_vm_rw(), I think we need
--- x/mm/process_vm_access.c +++ x/mm/process_vm_access.c @@ -375,10 +375,10 @@ static ssize_t process_vm_rw(pid_t pid, /* Check iovecs */ if (vm_write) - rc = rw_copy_check_uvector(WRITE, lvec, liovcnt, UIO_FASTIOV, + rc = rw_copy_check_uvector(READ, lvec, liovcnt, UIO_FASTIOV, iovstack_l, &iov_l, 1); else - rc = rw_copy_check_uvector(READ, lvec, liovcnt, UIO_FASTIOV, + rc = rw_copy_check_uvector(WRITE, lvec, liovcnt, UIO_FASTIOV, iovstack_l, &iov_l, 1); if (rc <= 0) goto free_iovecs;
However. Yes, this is subjective, but imho the new argument looks a bit ugly. Please look at this code again,
rw_copy_check_uvector(READ, rvec, check_access => 0);
what does this READ means without check_access? Plus we need another argument. Can't we do
--- x/fs/read_write.c
+++ x/fs/read_write.c
@@ -633,8 +633,7 @@ ssize_t do_loop_readv_writev(struct file
ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,
unsigned long nr_segs, unsigned long fast_segs,
struct iovec *fast_pointer,
- struct iovec **ret_pointer,
- int check_access)
+ struct iovec **ret_pointer)
{
unsigned long seg;
ssize_t ret;
@@ -690,8 +689,8 @@ ssize_t rw_copy_check_uvector(int type,
ret = -EINVAL;
goto out;
}
- if (check_access
- && unlikely(!access_ok(vrfy_dir(type), buf, len))) {
+ if (type >= 0 &&
+ unlikely(!access_ok(vrfy_dir(type), buf, len))) {
ret = -EFAULT;
goto out;
}and update the callers? In this case all callers just lose the unneeded argument and the code above does
rw_copy_check_uvector(-1, rvec);
Perhaps we can add another NOCHECK (or whatever) define near READ/WRITE.
What do you think?
Oleg.
Quoted 4 lines · Oleg Nesterov
Partly I was confused because (I guess) there is another minor bug in
process_vm_rw(), I think we need
Quoted 13 lines
--- x/mm/process_vm_access.c +++ x/mm/process_vm_access.c @@ -375,10 +375,10 @@ static ssize_t process_vm_rw(pid_t pid, /* Check iovecs */ if (vm_write) - rc = rw_copy_check_uvector(WRITE, lvec, liovcnt, UIO_FASTIOV, + rc = rw_copy_check_uvector(READ, lvec, liovcnt, UIO_FASTIOV, iovstack_l, &iov_l, 1); else - rc = rw_copy_check_uvector(READ, lvec, liovcnt, UIO_FASTIOV, + rc = rw_copy_check_uvector(WRITE, lvec, liovcnt, UIO_FASTIOV, iovstack_l, &iov_l, 1);
Argh. No, I was wrong, vrfy_dir() swaps READ/WRITE.
Oleg.
Quoted 16 lines · On Mon, 9 Jan 2012 15:53:42 +0100 Oleg Nesterov <[email protected]> wrote:
Quoted 15 lines · Christopher Yeoh
Quoted 3 lines · On Thu, 5 Jan 2012 16:10:12 +0100 Oleg Nesterov <[email protected]> wrote:
I guess you need ->cred_guard_mutex, please look at
mm_for_maps().Thanks, agreed this looks like it's a problem. Need to do a bit more testing, but I think the following patch fixes the race?
(quoted diff) +0
-0@@ -298,9 +298,14 @@ static ssize_t process_vm_rw_core(pid_t pid, const struct iovec *lvec, goto free_proc_pages; }
Quoted 14 lines
Review static ssize_t process_vm_rw_core(pid_t pid, · line 309
Quoted 9 lines
(quoted diff) +5
-0+ rc = mutex_lock_interruptible(&task->signal->cred_guard_mutex); + if (rc) + goto put_task_struct; + task_lock(task); if (__ptrace_may_access(task, PTRACE_MODE_ATTACH)) { task_unlock(task); + mutex_unlock(&task->signal->cred_guard_mutex);Yes, I think this works, but I don't think you should play with
task_lock()or ->mm_users, just useget_task_mm(). Better yet, can't we do
I agree with the consolidation with mm_for_maps (though we might need to argue over EPERM vs EACCES. However, I originally broke out get_task_mm and ptrace_may_access using __ptrace_may_access instead because both these functions grab the task lock at the start and release it at the end. Seemed better just to take it once.
So maybe something like this (untested) instead?
kernel/fork.c +31 -0
@@ -644,6 +644,37 @@ struct mm_struct *get_task_mm(struct task_struct *task) }EXPORT_SYMBOL_GPL (get_task_mm); +struct mm_struct *get_check_task_mm (struct task_struct *task,unsigned int mode) +{ +struct mm_struct *mm; +int err; + + err =mutex_lock_killable (&task->signal->cred_guard_mutex); +if (err) +return ERR_PTR (err); + +task_lock (task); +if (__ptrace_may_access (task, mode)) { + mm =ERR_PTR (-EPERM ); + goto out; + } + + mm = task->mm; +if (mm) { +if (task->flags &PF_KTHREAD ) + mm =NULL ; +else +atomic_inc (&mm->mm_users); + } + +out: +task_unlock (task); +mutex_unlock (&task->signal->cred_guard_mutex); + +return mm; +} +EXPORT_SYMBOL_GPL (get_check_task_mm); +/* Please note the differences between mmput and mm_release. * mmput is called whenever we stop holding onto a mm_struct, * error success whatever.
Quoted 10 lines
Quoted 5 lines
Other than reading the comment for
get_user_pagessaying that I don't want to set the force flag, I didn't really consider it. The use cases where I'm interested (intranode communication) has the cooperation of the target process anyway so its not needed. Any downsides to havingFOLL_FORCEenabled?Without
FOLL_FORCE, say, gdb can't use the new syscall to set the breakpoint or to read the !VM_READ mappings. OK,process_vm_rw()has flags, we can addPROCESS_VM_FORCEif needed.
It wasn't really intended for gdb, but perhaps it could be used/consolidated with other ptrace stuff (I don't know). I'm not really sure what the best thing to do here is. As I mentioned there is a level of cooperation where I am using it and honouring mprotect may help pickup inadvertent application errors. Perhaps a PROCESS_VM_FORCE flag would be the more conservative option.
Quoted 36 lines
However. Yes, this is subjective, but imho the new argument looks a bit ugly. Please look at this code again,
rw_copy_check_uvector(READ, rvec, check_access => 0);what does this READ means without
check_access? Plus we need another argument. Can't we do--- x/fs/read_write.c +++ x/fs/read_write.c @@ -633,8 +633,7 @@ ssize_t do_loop_readv_writev(struct file ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector, unsigned long nr_segs, unsigned long fast_segs, struct iovec *fast_pointer, - struct iovec **ret_pointer, - int check_access) + struct iovec **ret_pointer) { unsigned long seg; ssize_t ret; @@ -690,8 +689,8 @@ ssize_t rw_copy_check_uvector(int type, ret = -EINVAL; goto out; } - if (check_access - && unlikely(!access_ok(vrfy_dir(type), buf, len))) { + if (type >= 0 && + unlikely(!access_ok(vrfy_dir(type), buf, len))) { ret = -EFAULT; goto out; }and update the callers? In this case all callers just lose the unneeded argument and the code above does
Quoted 6 lines
rw_copy_check_uvector(-1, rvec);Perhaps we can add another NOCHECK (or whatever) define near READ/WRITE.
What do you think?
Yes, this is much better. I think for readability we do need a define.
rw_copy_check_uvector(NOCHECK, rvec)
looks a bit odd (why am I passing NOCHECK to a function with the word check in it?). So perhaps maybe IOVEC_ONLY or something like that?
Regards,
Chris
Signature · 1 line
[email protected]
Quoted 30 lines · Christopher Yeoh
Quoted 5 lines · On Mon, 9 Jan 2012 15:53:42 +0100 Oleg Nesterov <[email protected]> wrote:
Yes, I think this works, but I don't think you should play with
task_lock()or ->mm_users, just useget_task_mm(). Better yet, can't we doI agree with the consolidation with
mm_for_maps(though we might need to argue over EPERM vs EACCES. However, I originally broke outget_task_mmandptrace_may_accessusing__ptrace_may_accessinstead because both these functions grab the task lock at the start and release it at the end. Seemed better just to take it once.kernel/fork.c +15
-0+struct mm_struct *get_check_task_mm (struct task_struct *task,unsigned int mode) +{ +struct mm_struct *mm; +int err; + + err =mutex_lock_killable (&task->signal->cred_guard_mutex); +if (err) +return ERR_PTR (err); + +task_lock (task); +if (__ptrace_may_access (task, mode)) { + mm =ERR_PTR (-EPERM ); + goto out; + } +
kernel/fork.c struct mm_struct *get_task_mm(struct task_struct *task) · line 671
Quoted 10 lines
kernel/fork.c +10
-0+ mm = task->mm; +if (mm) { +if (task->flags &PF_KTHREAD ) + mm =NULL ; +else +atomic_inc (&mm->mm_users); + } + +out: +task_unlock (task);
Well, this saves unlock+lock, but adds more copy-and-paste code. Personally I'd prefer to consolidate.
Quoted 11 lines
Quoted 4 lines
Without
FOLL_FORCE, say, gdb can't use the new syscall to set the breakpoint or to read the !VM_READ mappings. OK,process_vm_rw()has flags, we can addPROCESS_VM_FORCEif needed.It wasn't really intended for gdb, but perhaps it could be used/consolidated with other ptrace stuff (I don't know). I'm not really sure what the best thing to do here is. As I mentioned there is a level of cooperation where I am using it and honouring mprotect may help pickup inadvertent application errors. Perhaps a
PROCESS_VM_FORCEflag would be the more conservative option.
OK, agreed.
Quoted 33 lines
Quoted 33 lines
--- x/fs/read_write.c +++ x/fs/read_write.c @@ -633,8 +633,7 @@ ssize_t do_loop_readv_writev(struct file ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector, unsigned long nr_segs, unsigned long fast_segs, struct iovec *fast_pointer, - struct iovec **ret_pointer, - int check_access) + struct iovec **ret_pointer) { unsigned long seg; ssize_t ret; @@ -690,8 +689,8 @@ ssize_t rw_copy_check_uvector(int type, ret = -EINVAL; goto out; } - if (check_access - && unlikely(!access_ok(vrfy_dir(type), buf, len))) { + if (type >= 0 && + unlikely(!access_ok(vrfy_dir(type), buf, len))) { ret = -EFAULT; goto out; }and update the callers? In this case all callers just lose the unneeded argument and the code above does
rw_copy_check_uvector(-1, rvec);Perhaps we can add another NOCHECK (or whatever) define near READ/WRITE.
Quoted 8 lines
Quoted 1 line
What do you think?
Yes, this is much better. I think for readability we do need a define.
rw_copy_check_uvector(NOCHECK, rvec)looks a bit odd (why am I passing NOCHECK to a function with the word check in it?).
But it also has "copy" in the name. However, I agree that NOCHECK doesn't look very good, and
Quoted 1 line
So perhaps maybe
IOVEC_ONLYor something like that?
I agree with any naming ;)
Oleg.