From: Andrei Vagin <hidden> Date: 2021-04-14 05:54:58
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
Another use-case is CRIU (Checkpoint/Restore in User-space). Several
process properties can be received only from the process itself. Right
now, we use a parasite code that is injected into the process. We do
this with ptrace but it is slow, unsafe, and tricky. process_vm_exec can
simplify the process of injecting a parasite code and it will allow
pre-dump memory without stopping processes. The pre-dump here is when we
enable a memory tracker and dump the memory while a process is continue
running. On each interaction we dump memory that has been changed from
the previous iteration. In the final step, we will stop processes and
dump their full state. Right now the most effective way to dump process
memory is to create a set of pipes and splice memory into these pipes
from the parasite code. With process_vm_exec, we will be able to call
vmsplice directly. It means that we will not need to stop a process to
inject the parasite code.
= How it works =
process_vm_exec has two modes:
* Execute code in an address space of a target process and stop on any
signal or system call.
* Execute a system call in an address space of a target process.
int process_vm_exec(pid_t pid, struct sigcontext uctx,
unsigned long flags, siginfo_t siginfo,
sigset_t *sigmask, size_t sizemask)
PID - target process identification. We can consider to use pidfd
instead of PID here.
sigcontext contains a process state with what the process will be
resumed after switching the address space and then when a process will
be stopped, its sate will be saved back to sigcontext.
siginfo is information about a signal that has interrupted the process.
If a process is interrupted by a system call, signfo will contain a
synthetic siginfo of the SIGSYS signal.
sigmask is a set of signals that process_vm_exec returns via signfo.
# How fast is it
In the fourth patch, you can find two benchmarks that execute a function
that calls system calls in a loop. ptrace_vm_exe uses ptrace to trap
system calls, proces_vm_exec uses the process_vm_exec syscall to do the
same thing.
ptrace_vm_exec: 1446 ns/syscall
ptrocess_vm_exec: 289 ns/syscall
PS: This version is just a prototype. Its goal is to collect the initial
feedback, to discuss the interfaces, and maybe to get some advice on
implementation..
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Andy Lutomirski <luto@kernel.org>
Cc: Anton Ivanov <anton.ivanov@cambridgegreys.com>
Cc: Christian Brauner <redacted>
Cc: Dmitry Safonov <redacted>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Jeff Dike <redacted>
Cc: Mike Rapoport <redacted>
Cc: Michael Kerrisk (man-pages) <redacted>
Cc: Oleg Nesterov <oleg@redhat.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Richard Weinberger <richard@nod.at>
Cc: Thomas Gleixner <redacted>
Andrei Vagin (4):
signal: add a helper to restore a process state from sigcontex
arch/x86: implement the process_vm_exec syscall
arch/x86: allow to execute syscalls via process_vm_exec
selftests: add tests for process_vm_exec
arch/Kconfig | 15 ++
arch/x86/Kconfig | 1 +
arch/x86/entry/common.c | 19 +++
arch/x86/entry/syscalls/syscall_64.tbl | 1 +
arch/x86/include/asm/sigcontext.h | 2 +
arch/x86/kernel/Makefile | 1 +
arch/x86/kernel/process_vm_exec.c | 160 ++++++++++++++++++
arch/x86/kernel/signal.c | 125 ++++++++++----
include/linux/entry-common.h | 2 +
include/linux/process_vm_exec.h | 17 ++
include/linux/sched.h | 7 +
include/linux/syscalls.h | 6 +
include/uapi/asm-generic/unistd.h | 4 +-
include/uapi/linux/process_vm_exec.h | 8 +
kernel/entry/common.c | 2 +-
kernel/fork.c | 9 +
kernel/sys_ni.c | 2 +
.../selftests/process_vm_exec/Makefile | 7 +
tools/testing/selftests/process_vm_exec/log.h | 26 +++
.../process_vm_exec/process_vm_exec.c | 105 ++++++++++++
.../process_vm_exec/process_vm_exec_fault.c | 111 ++++++++++++
.../process_vm_exec/process_vm_exec_syscall.c | 81 +++++++++
.../process_vm_exec/ptrace_vm_exec.c | 111 ++++++++++++
23 files changed, 785 insertions(+), 37 deletions(-)
create mode 100644 arch/x86/kernel/process_vm_exec.c
create mode 100644 include/linux/process_vm_exec.h
create mode 100644 include/uapi/linux/process_vm_exec.h
create mode 100644 tools/testing/selftests/process_vm_exec/Makefile
create mode 100644 tools/testing/selftests/process_vm_exec/log.h
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec.c
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec_fault.c
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec_syscall.c
create mode 100644 tools/testing/selftests/process_vm_exec/ptrace_vm_exec.c
--
2.29.2
From: Andrei Vagin <hidden> Date: 2021-04-14 05:55:03
It will be used to implement process_vm_exec.
Signed-off-by: Andrei Vagin <redacted>
---
arch/x86/kernel/signal.c | 78 ++++++++++++++++++++++------------------
1 file changed, 43 insertions(+), 35 deletions(-)
@@ -79,51 +79,43 @@ static void force_valid_ss(struct pt_regs *regs)# define CONTEXT_COPY_SIZE sizeof(struct sigcontext)#endif-staticintrestore_sigcontext(structpt_regs*regs,-structsigcontext__user*usc,+staticint__restore_sigcontext(structpt_regs*regs,+structsigcontext__user*sc,unsignedlonguc_flags){-structsigcontextsc;--/* Always make any pending restarted system calls return -EINTR */-current->restart_block.fn=do_no_restart_syscall;--if(copy_from_user(&sc,usc,CONTEXT_COPY_SIZE))-return-EFAULT;-#ifdef CONFIG_X86_32-set_user_gs(regs,sc.gs);-regs->fs=sc.fs;-regs->es=sc.es;-regs->ds=sc.ds;+set_user_gs(regs,sc->gs);+regs->fs=sc->fs;+regs->es=sc->es;+regs->ds=sc->ds;#endif /* CONFIG_X86_32 */-regs->bx=sc.bx;-regs->cx=sc.cx;-regs->dx=sc.dx;-regs->si=sc.si;-regs->di=sc.di;-regs->bp=sc.bp;-regs->ax=sc.ax;-regs->sp=sc.sp;-regs->ip=sc.ip;+regs->bx=sc->bx;+regs->cx=sc->cx;+regs->dx=sc->dx;+regs->si=sc->si;+regs->di=sc->di;+regs->bp=sc->bp;+regs->ax=sc->ax;+regs->sp=sc->sp;+regs->ip=sc->ip;#ifdef CONFIG_X86_64-regs->r8=sc.r8;-regs->r9=sc.r9;-regs->r10=sc.r10;-regs->r11=sc.r11;-regs->r12=sc.r12;-regs->r13=sc.r13;-regs->r14=sc.r14;-regs->r15=sc.r15;+regs->r8=sc->r8;+regs->r9=sc->r9;+regs->r10=sc->r10;+regs->r11=sc->r11;+regs->r12=sc->r12;+regs->r13=sc->r13;+regs->r14=sc->r14;+regs->r15=sc->r15;#endif /* CONFIG_X86_64 *//* Get CS/SS and force CPL3 */-regs->cs=sc.cs|0x03;-regs->ss=sc.ss|0x03;+regs->cs=sc->cs|0x03;+regs->ss=sc->ss|0x03;-regs->flags=(regs->flags&~FIX_EFLAGS)|(sc.flags&FIX_EFLAGS);+regs->flags=(regs->flags&~FIX_EFLAGS)|(sc->flags&FIX_EFLAGS);/* disable syscall checks */regs->orig_ax=-1;
@@ -136,10 +128,26 @@ static int restore_sigcontext(struct pt_regs *regs,force_valid_ss(regs);#endif-returnfpu__restore_sig((void__user*)sc.fpstate,+returnfpu__restore_sig((void__user*)sc->fpstate,IS_ENABLED(CONFIG_X86_32));}+staticintrestore_sigcontext(structpt_regs*regs,+structsigcontext__user*usc,+unsignedlonguc_flags)+{+structsigcontextsc;++/* Always make any pending restarted system calls return -EINTR */+current->restart_block.fn=do_no_restart_syscall;++if(copy_from_user(&sc,usc,CONTEXT_COPY_SIZE))+return-EFAULT;++return__restore_sigcontext(regs,&sc,uc_flags);+}++static__always_inlineint__unsafe_setup_sigcontext(structsigcontext__user*sc,void__user*fpstate,structpt_regs*regs,unsignedlongmask)
From: Andrei Vagin <hidden> Date: 2021-04-14 05:55:07
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
process_vm_exec swaps the current address space with an address space of
a specified process, sets a state from sigcontex and resumes the process.
When a process receives a signal or calls a system call,
process_vm_exec saves the process state back to sigcontext, restores the
origin address space, restores the origin process state, and returns to
userspace.
If it was interrupted by a signal and the signal is in the user_mask,
the signal is dequeued and information about it is saved in uinfo.
If process_vm_exec is interrupted by a system call, a synthetic siginfo
for the SIGSYS signal is generated.
The behavior of this system call is similar to PTRACE_SYSEMU but
everything is happing in the context of one process, so
process_vm_exec shows a better performance.
PTRACE_SYSEMU is primarily used to implement sandboxes (application
kernels) like User-mode Linux or gVisor. These type of sandboxes
intercepts applications system calls and acts as the guest kernel.
A simple benchmark, where a "tracee" process executes systems calls in a
loop and a "tracer" process traps syscalls and handles them just
incrementing the tracee instruction pointer to skip the syscall
instruction shows that process_vm_exec works more than 5 times faster
than PTRACE_SYSEMU.
Signed-off-by: Andrei Vagin <redacted>
---
arch/Kconfig | 15 +++
arch/x86/Kconfig | 1 +
arch/x86/entry/common.c | 16 +++
arch/x86/entry/syscalls/syscall_64.tbl | 1 +
arch/x86/include/asm/sigcontext.h | 2 +
arch/x86/kernel/Makefile | 1 +
arch/x86/kernel/process_vm_exec.c | 133 +++++++++++++++++++++++++
arch/x86/kernel/signal.c | 47 +++++++++
include/linux/process_vm_exec.h | 15 +++
include/linux/sched.h | 7 ++
include/linux/syscalls.h | 6 ++
include/uapi/asm-generic/unistd.h | 4 +-
kernel/fork.c | 9 ++
kernel/sys_ni.c | 2 +
14 files changed, 258 insertions(+), 1 deletion(-)
create mode 100644 arch/x86/kernel/process_vm_exec.c
create mode 100644 include/linux/process_vm_exec.h
@@ -362,6 +362,7 @@ 438 common pidfd_getfd sys_pidfd_getfd 439 common faccessat2 sys_faccessat2 440 common process_madvise sys_process_madvise+441 64 process_vm_exec sys_process_vm_exec # # Due to a historical design error, certain syscalls are numbered differently
@@ -896,3 +914,32 @@ COMPAT_SYSCALL_DEFINE0(x32_rt_sigreturn)return0;}#endif++#ifdef CONFIG_PROCESS_VM_EXEC+longswap_vm_exec_context(structsigcontext__user*uctx)+{+structsigcontextctx={};+sigset_tset={};+++if(copy_from_user(&ctx,uctx,CONTEXT_COPY_SIZE))+return-EFAULT;+/* A floating point state is managed from user-space. */+if(ctx.fpstate!=0)+return-EINVAL;+if(!user_access_begin(uctx,sizeof(*uctx)))+return-EFAULT;+unsafe_put_sigcontext(uctx,NULL,current_pt_regs(),(&set),Efault);+user_access_end();++if(__restore_sigcontext(current_pt_regs(),&ctx,0))+gotobadframe;++return0;+Efault:+user_access_end();+badframe:+signal_fault(current_pt_regs(),uctx,"swap_vm_exec_context");+return-EFAULT;+}+#endif
@@ -1347,4 +1347,10 @@ int __sys_getsockopt(int fd, int level, int optname, char __user *optval,int__user*optlen);int__sys_setsockopt(intfd,intlevel,intoptname,char__user*optval,intoptlen);++#ifdef CONFIG_PROCESS_VM_EXEC+voidrestore_vm_exec_context(structpt_regs*regs);+#else+staticinlinevoidrestore_vm_exec_context(structpt_regs*regs){}+#endif#endif
@@ -350,6 +350,8 @@ COND_SYSCALL(pkey_mprotect);COND_SYSCALL(pkey_alloc);COND_SYSCALL(pkey_free);+/* execute in another address space */+COND_SYSCALL(process_vm_exec);/**Architecturespecificweaksyscallentries.
From: Andrei Vagin <hidden> Date: 2021-04-14 05:55:10
process_vm_exec allows to execute code in an address space of another
process. It changes the current address space to the target address
space and resume the current process with registers from sigcontex that
is passed in the arguments.
This changes adds the PROCESS_VM_EXEC_SYSCALL flag and if it is set
process_vm_exec will execute a system call with arguments from sigcontext.
process_vm_exec retuns 0 if the system call has been executed and an error
code in other cases.
A return code of the system call can be found in a proper register in
sigcontext.
Signed-off-by: Andrei Vagin <redacted>
---
arch/x86/entry/common.c | 5 ++++-
arch/x86/kernel/process_vm_exec.c | 29 +++++++++++++++++++++++++++-
include/linux/entry-common.h | 2 ++
include/linux/process_vm_exec.h | 2 ++
include/uapi/linux/process_vm_exec.h | 8 ++++++++
kernel/entry/common.c | 2 +-
6 files changed, 45 insertions(+), 3 deletions(-)
create mode 100644 include/uapi/linux/process_vm_exec.h
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
It seems to me like your proposed API doesn't really fit either one of
those usecases well...
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
In this case, since you really only want an mm_struct to run code
under, it seems weird to create a whole task with its own PID and so
on. It seems to me like something similar to the /dev/kvm API would be
more appropriate here? Implementation options that I see for that
would be:
1. mm_struct-based:
a set of syscalls to create a new mm_struct,
change memory mappings under that mm_struct, and switch to it
2. pagetable-mirroring-based:
like /dev/kvm, an API to create a new pagetable, mirror parts of
the mm_struct's pagetables over into it with modified permissions
(like KVM_SET_USER_MEMORY_REGION),
and run code under that context.
page fault handling would first handle the fault against mm->pgd
as normal, then mirror the PTE over into the secondary pagetables.
invalidation could be handled with MMU notifiers.
Another use-case is CRIU (Checkpoint/Restore in User-space). Several
process properties can be received only from the process itself. Right
now, we use a parasite code that is injected into the process. We do
this with ptrace but it is slow, unsafe, and tricky.
But this API will only let you run code under the *mm* of the target
process, not fully in the context of a target *task*, right? So you
still won't be able to use this for accessing anything other than
memory? That doesn't seem very generically useful to me.
Also, I don't doubt that anything involving ptrace is kinda tricky,
but it would be nice to have some more detail on what exactly makes
this slow, unsafe and tricky. Are there API additions for ptrace that
would make this work better? I imagine you're thinking of things like
an API for injecting a syscall into the target process without having
to first somehow find an existing SYSCALL instruction in the target
process?
process_vm_exec can
simplify the process of injecting a parasite code and it will allow
pre-dump memory without stopping processes. The pre-dump here is when we
enable a memory tracker and dump the memory while a process is continue
running. On each interaction we dump memory that has been changed from
the previous iteration. In the final step, we will stop processes and
dump their full state. Right now the most effective way to dump process
memory is to create a set of pipes and splice memory into these pipes
from the parasite code. With process_vm_exec, we will be able to call
vmsplice directly. It means that we will not need to stop a process to
inject the parasite code.
Alternatively you could add splice support to /proc/$pid/mem or add a
syscall similar to process_vm_readv() that splices into a pipe, right?
From: Anton Ivanov <anton.ivanov@cambridgegreys.com> Date: 2021-04-14 07:23:15
On 14/04/2021 06:52, Andrei Vagin wrote:
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
Certainly interesting, but will require um to rework most of its memory
management and we will most likely need extra mm support to make use of
it in UML. We are not likely to get away just with one syscall there.
Another use-case is CRIU (Checkpoint/Restore in User-space). Several
process properties can be received only from the process itself. Right
now, we use a parasite code that is injected into the process. We do
this with ptrace but it is slow, unsafe, and tricky. process_vm_exec can
simplify the process of injecting a parasite code and it will allow
pre-dump memory without stopping processes. The pre-dump here is when we
enable a memory tracker and dump the memory while a process is continue
running. On each interaction we dump memory that has been changed from
the previous iteration. In the final step, we will stop processes and
dump their full state. Right now the most effective way to dump process
memory is to create a set of pipes and splice memory into these pipes
from the parasite code. With process_vm_exec, we will be able to call
vmsplice directly. It means that we will not need to stop a process to
inject the parasite code.
= How it works =
process_vm_exec has two modes:
* Execute code in an address space of a target process and stop on any
signal or system call.
* Execute a system call in an address space of a target process.
int process_vm_exec(pid_t pid, struct sigcontext uctx,
unsigned long flags, siginfo_t siginfo,
sigset_t *sigmask, size_t sizemask)
PID - target process identification. We can consider to use pidfd
instead of PID here.
sigcontext contains a process state with what the process will be
resumed after switching the address space and then when a process will
be stopped, its sate will be saved back to sigcontext.
siginfo is information about a signal that has interrupted the process.
If a process is interrupted by a system call, signfo will contain a
synthetic siginfo of the SIGSYS signal.
sigmask is a set of signals that process_vm_exec returns via signfo.
# How fast is it
In the fourth patch, you can find two benchmarks that execute a function
that calls system calls in a loop. ptrace_vm_exe uses ptrace to trap
system calls, proces_vm_exec uses the process_vm_exec syscall to do the
same thing.
ptrace_vm_exec: 1446 ns/syscall
ptrocess_vm_exec: 289 ns/syscall
PS: This version is just a prototype. Its goal is to collect the initial
feedback, to discuss the interfaces, and maybe to get some advice on
implementation..
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Andy Lutomirski <luto@kernel.org>
Cc: Anton Ivanov <anton.ivanov@cambridgegreys.com>
Cc: Christian Brauner <redacted>
Cc: Dmitry Safonov <redacted>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Jeff Dike <redacted>
Cc: Mike Rapoport <redacted>
Cc: Michael Kerrisk (man-pages) <redacted>
Cc: Oleg Nesterov <oleg@redhat.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Richard Weinberger <richard@nod.at>
Cc: Thomas Gleixner <redacted>
Andrei Vagin (4):
signal: add a helper to restore a process state from sigcontex
arch/x86: implement the process_vm_exec syscall
arch/x86: allow to execute syscalls via process_vm_exec
selftests: add tests for process_vm_exec
arch/Kconfig | 15 ++
arch/x86/Kconfig | 1 +
arch/x86/entry/common.c | 19 +++
arch/x86/entry/syscalls/syscall_64.tbl | 1 +
arch/x86/include/asm/sigcontext.h | 2 +
arch/x86/kernel/Makefile | 1 +
arch/x86/kernel/process_vm_exec.c | 160 ++++++++++++++++++
arch/x86/kernel/signal.c | 125 ++++++++++----
include/linux/entry-common.h | 2 +
include/linux/process_vm_exec.h | 17 ++
include/linux/sched.h | 7 +
include/linux/syscalls.h | 6 +
include/uapi/asm-generic/unistd.h | 4 +-
include/uapi/linux/process_vm_exec.h | 8 +
kernel/entry/common.c | 2 +-
kernel/fork.c | 9 +
kernel/sys_ni.c | 2 +
.../selftests/process_vm_exec/Makefile | 7 +
tools/testing/selftests/process_vm_exec/log.h | 26 +++
.../process_vm_exec/process_vm_exec.c | 105 ++++++++++++
.../process_vm_exec/process_vm_exec_fault.c | 111 ++++++++++++
.../process_vm_exec/process_vm_exec_syscall.c | 81 +++++++++
.../process_vm_exec/ptrace_vm_exec.c | 111 ++++++++++++
23 files changed, 785 insertions(+), 37 deletions(-)
create mode 100644 arch/x86/kernel/process_vm_exec.c
create mode 100644 include/linux/process_vm_exec.h
create mode 100644 include/uapi/linux/process_vm_exec.h
create mode 100644 tools/testing/selftests/process_vm_exec/Makefile
create mode 100644 tools/testing/selftests/process_vm_exec/log.h
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec.c
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec_fault.c
create mode 100644 tools/testing/selftests/process_vm_exec/process_vm_exec_syscall.c
create mode 100644 tools/testing/selftests/process_vm_exec/ptrace_vm_exec.c
From: Johannes Berg <johannes@sipsolutions.net> Date: 2021-04-14 07:34:55
On Wed, 2021-04-14 at 08:22 +0100, Anton Ivanov wrote:
On 14/04/2021 06:52, Andrei Vagin wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
Certainly interesting, but will require um to rework most of its memory
management and we will most likely need extra mm support to make use of
it in UML. We are not likely to get away just with one syscall there.
From: Benjamin Berg <hidden> Date: 2021-04-14 09:24:51
On Wed, 2021-04-14 at 09:34 +0200, Johannes Berg wrote:
On Wed, 2021-04-14 at 08:22 +0100, Anton Ivanov wrote:
quoted
On 14/04/2021 06:52, Andrei Vagin wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and
write
to a process memory faster than we can do this with ptrace. And now
it
is time for process_vm_exec that allows executing code in an
address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes
that
are used to manage guest address spaces. Guest code is executed in
the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes
use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
Certainly interesting, but will require um to rework most of its
memory
management and we will most likely need extra mm support to make use
of
it in UML. We are not likely to get away just with one syscall there.
Hmm, to me it sounds like it replaces both ptrace and seccomp mode
while completely avoiding the scheduling overhead that these techniques
have. I think everything UML needs is covered:
* The new API can do syscalls in the target memory space
(we can modify the address space)
* The new API can run code until the next syscall happens
(or a signal happens, which means SIGALRM for scheduling works)
* Single step tracing should work by setting EFLAGS
I think the memory management itself stays fundamentally the same. We
just do the initial clone() using CLONE_STOPPED. We don't need any stub
code/data and we have everything we need to modify the address space
and run the userspace process.
Benjamin
From: Andrei Vagin <hidden> Date: 2021-04-14 22:12:54
On Wed, Apr 14, 2021 at 08:46:40AM +0200, Jann Horn wrote:
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
It seems to me like your proposed API doesn't really fit either one of
those usecases well...
We definitely can invent more specific interfaces for each of these
problems. Sure, they will handle their use-cases a bit better than this
generic one. But do we want to have two very specific interfaces with
separate kernel implementations? My previous experiences showed that the
kernel community doesn't like interfaces that are specific for only one
narrow use-case.
So when I was working on process_vm_exec, I was thinking how to make
one interfaces that will be good enough for all these use-cases.
quoted
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
In this case, since you really only want an mm_struct to run code
under, it seems weird to create a whole task with its own PID and so
on. It seems to me like something similar to the /dev/kvm API would be
more appropriate here? Implementation options that I see for that
would be:
1. mm_struct-based:
a set of syscalls to create a new mm_struct,
change memory mappings under that mm_struct, and switch to it
2. pagetable-mirroring-based:
like /dev/kvm, an API to create a new pagetable, mirror parts of
the mm_struct's pagetables over into it with modified permissions
(like KVM_SET_USER_MEMORY_REGION),
and run code under that context.
page fault handling would first handle the fault against mm->pgd
as normal, then mirror the PTE over into the secondary pagetables.
invalidation could be handled with MMU notifiers.
We are ready to discuss this sort of interfaces if the community will
agree to accept it. Are there any other users except sandboxes that will
need something like this? Will the sandbox use-case enough to justify
the addition of this interface?
quoted
Another use-case is CRIU (Checkpoint/Restore in User-space). Several
process properties can be received only from the process itself. Right
now, we use a parasite code that is injected into the process. We do
this with ptrace but it is slow, unsafe, and tricky.
But this API will only let you run code under the *mm* of the target
process, not fully in the context of a target *task*, right? So you
still won't be able to use this for accessing anything other than
memory? That doesn't seem very generically useful to me.
You are right, this will not rid us of the need to run a parasite code.
I wrote that it will make a process of injecting a parasite code a bit
simpler.
Also, I don't doubt that anything involving ptrace is kinda tricky,
but it would be nice to have some more detail on what exactly makes
this slow, unsafe and tricky. Are there API additions for ptrace that
would make this work better? I imagine you're thinking of things like
an API for injecting a syscall into the target process without having
to first somehow find an existing SYSCALL instruction in the target
process?
You describe the first problem right. We need to find or inject a
syscall instruction to a target process.
Right now, we need to do these steps to execute a system call:
* inject the syscall instruction (PTRACE_PEEKDATA/PTRACE_POKEDATA).
* get origin registers
* set new registers
* get a signal mask.
* block signals
* resume the process
* stop it on the next syscall-exit
* get registers
* set origin registers
* restore a signal mask.
One of the CRIU principals is to avoid changing a process state, so if
criu is interrupted, processes must be resumed and continue running. The
procedure of injecting a system call creates a window when a process is
in an inconsistent state, and a disappearing CRIU at such moments will
be fatal for the process. We don't think that we can eliminate such
windows, but we want to make them smaller.
In CRIU, we have a self-healed parasite. The idea is to inject a
parasite code with a signal frame that contains the origin process
state. The parasite runs in an "RPC daemon mode" and gets commands from
criu via a unix socket. If it detects that criu disappeared, it calls
rt_sigreturn and resumes the origin process.
As for the performance of the ptrace, there are a few reasons why it is
slow. First, it is a number of steps what we need to do. Second, it is
two synchronious context switches. Even if we will solve the first
problem with a new ptrace command, it will be not enough to stop using a
parasite in CRIU.
quoted
process_vm_exec can
simplify the process of injecting a parasite code and it will allow
pre-dump memory without stopping processes. The pre-dump here is when we
enable a memory tracker and dump the memory while a process is continue
running. On each interaction we dump memory that has been changed from
the previous iteration. In the final step, we will stop processes and
dump their full state. Right now the most effective way to dump process
memory is to create a set of pipes and splice memory into these pipes
from the parasite code. With process_vm_exec, we will be able to call
vmsplice directly. It means that we will not need to stop a process to
inject the parasite code.
Alternatively you could add splice support to /proc/$pid/mem or add a
syscall similar to process_vm_readv() that splices into a pipe, right?
We send patches to introcude process_vm_splice:
https://lore.kernel.org/patchwork/cover/871116/
but they were not merged and the main reason was a lack of enough users
to justify its addition.
On Tue, Apr 13, 2021 at 10:52:13PM -0700, Andrei Vagin wrote:
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
Just to add to the list of use cases for PROCESS_VM_EXEC_SYSCALL,
another use case is initializing a process from the "outside", instead
of from the "inside" as fork requires. This can be much easier to work
with. http://catern.com/rsys21.pdf goes into this use case in some
depth.
It relies heavily on a remote syscall primitive:
https://github.com/catern/rsyscall. The PROCESS_VM_EXEC_SYSCALL API
proposed in this patch would be a great replacement for the current
implementation, which relies on running code inside the target process.
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
[...]
I still think that this whole API is fundamentally the wrong approach
because it tries to shoehorn multiple usecases with different
requirements into a single API. But that aside:
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
As far as I understand, only kthreads are allowed to do this (as
implemented in kthread_use_mm()).
On Mon, Jun 28, 2021, at 9:13 AM, Jann Horn wrote:
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
[...]
I still think that this whole API is fundamentally the wrong approach
because it tries to shoehorn multiple usecases with different
requirements into a single API. But that aside:
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
exec_mmap() does it, so it can’t be entirely impossible.
On Mon, Jun 28, 2021 at 6:30 PM Andy Lutomirski [off-list ref] wrote:
On Mon, Jun 28, 2021, at 9:13 AM, Jann Horn wrote:
quoted
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
[...]
I still think that this whole API is fundamentally the wrong approach
because it tries to shoehorn multiple usecases with different
requirements into a single API. But that aside:
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
exec_mmap() does it, so it can’t be entirely impossible.
Yeah, true, execve can do it - I guess the thing that makes that
special is that it's running after de_thread(), so it's guaranteed to
be single-threaded?
From: Andrei Vagin <hidden> Date: 2021-07-02 06:25:54
On Mon, Jun 28, 2021 at 06:13:29PM +0200, Jann Horn wrote:
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
[...]
I still think that this whole API is fundamentally the wrong approach
because it tries to shoehorn multiple usecases with different
requirements into a single API. But that aside:
Here, I can't agree with you, but this is discussed in the parallel
thread.
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
As far as I understand, only kthreads are allowed to do this (as
implemented in kthread_use_mm()).
kthread_use_mm() was renamed from use_mm in the v5.8 kernel. Before
that, it wasn't used for user processes in the kernel, but it was
exported for modules, and we used it without any visible problems. We
understood that there could be some issues like zap_threads and it was
one of reasons why we decided to introduce this system call.
I understand that there are no places in the kernel where we change mm
of user threads back and forth, but are there any real concerns why we
should not do that? I agree that zap_threads should be fixed, but it
will the easy one.
From: Andrei Vagin <hidden> Date: 2021-07-02 07:01:34
On Wed, Apr 14, 2021 at 08:46:40AM +0200, Jann Horn wrote:
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
It seems to me like your proposed API doesn't really fit either one of
those usecases well...
quoted
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
In this case, since you really only want an mm_struct to run code
under, it seems weird to create a whole task with its own PID and so
on. It seems to me like something similar to the /dev/kvm API would be
more appropriate here? Implementation options that I see for that
would be:
1. mm_struct-based:
a set of syscalls to create a new mm_struct,
change memory mappings under that mm_struct, and switch to it
I like the idea to have a handle for mm. Instead of pid, we will pass
this handle to process_vm_exec. We have pidfd for processes and we can
introduce mmfd for mm_struct.
2. pagetable-mirroring-based:
like /dev/kvm, an API to create a new pagetable, mirror parts of
the mm_struct's pagetables over into it with modified permissions
(like KVM_SET_USER_MEMORY_REGION),
and run code under that context.
page fault handling would first handle the fault against mm->pgd
as normal, then mirror the PTE over into the secondary pagetables.
invalidation could be handled with MMU notifiers.
I found this idea interesting and decided to look at it more closely.
After reading the kernel code for a few days, I realized that it would
not be easy to implement something like this, but more important is that
I don’t understand what problem it solves. Will it simplify the
user-space code? I don’t think so. Will it improve performance? It is
unclear for me too.
First, in the KVM case, we have a few big linear mappings and need to
support one “shadow” address space. In the case of sandboxes, we can
have a tremendous amount of mappings and many address spaces that we
need to manage. Memory mappings will be mapped with different addresses
in a supervisor address space and “guest” address spaces. If guest
address spaces will not have their mm_structs, we will need to reinvent
vma-s in some form. If guest address spaces have mm_structs, this will
look similar to https://lwn.net/Articles/830648/.
Second, each pagetable is tied up with mm_stuct. You suggest creating
new pagetables that will not have their mm_struct-s (sorry if I
misunderstood something). I am not sure that it will be easy to
implement. How many corner cases will be there?
As for page faults in a secondary address space, we will need to find a
fault address in the main address space, handle the fault there and then
mirror the PTE to the secondary pagetable. Effectively, it means that
page faults will be handled in two address spaces. Right now, we use
memfd and shared mappings. It means that each fault is handled only in
one address space, and we map a guest memory region to the supervisor
address space only when we need to access it. A large portion of guest
anonymous memory is never mapped to the supervisor address space.
Will an overhead of mirrored address spaces be smaller than memfd shared
mappings? I am not sure.
Third, this approach will not get rid of having process_vm_exec. We will
need to switch to a guest address space with a specified state and
switch back on faults or syscalls. If the main concern is the ability to
run syscalls on a remote mm, we can think about how to fix this. I see
two ways what we can do here:
* Specify the exact list of system calls that are allowed. The first
three candidates are mmap, munmap, and vmsplice.
* Instead of allowing us to run system calls, we can implement this in
the form of commands. In the case of sandboxes, we need to implement
only two commands to create and destroy memory mappings in a target
address space.
Thanks,
Andrei
From: Peter Zijlstra <peterz@infradead.org> Date: 2021-07-02 08:51:43
I'm terrified of all of this...
On Tue, Apr 13, 2021 at 10:52:15PM -0700, Andrei Vagin wrote:
+long swap_vm_exec_context(struct sigcontext __user *uctx)
+{
+ struct sigcontext ctx = {};
+ sigset_t set = {};
+
+
+ if (copy_from_user(&ctx, uctx, CONTEXT_COPY_SIZE))
+ return -EFAULT;
+ /* A floating point state is managed from user-space. */
+ if (ctx.fpstate != 0)
+ return -EINVAL;
+ if (!user_access_begin(uctx, sizeof(*uctx)))
+ return -EFAULT;
+ unsafe_put_sigcontext(uctx, NULL, current_pt_regs(), (&set), Efault);
+ user_access_end();
But here you save the sigcontext without FPU state.
+
+ if (__restore_sigcontext(current_pt_regs(), &ctx, 0))
+ goto badframe;
And here you restore sigcontext, *with* FPU state. At which point your
FPU state is irrecoverably lost.
Also, I'm not at all convinced this can ever do the right thing when the
tasks don't agree on what the FPU state is. I suppose in the best case
the save will EFAULT.
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
As far as I understand, only kthreads are allowed to do this (as
implemented in kthread_use_mm()).
kthread_use_mm() was renamed from use_mm in the v5.8 kernel. Before
that, it wasn't used for user processes in the kernel, but it was
exported for modules, and we used it without any visible problems. We
understood that there could be some issues like zap_threads and it was
one of reasons why we decided to introduce this system call.
I understand that there are no places in the kernel where we change mm
of user threads back and forth, but are there any real concerns why we
should not do that? I agree that zap_threads should be fixed, but it
will the easy one.
My point is that if you break a preexisting assumption like this,
you'll have to go through the kernel and search for places that rely
on this assumption, and fix them up, which may potentially require
thinking about what kinds of semantics would actually be appropriate
there. Like the MCE killing logic (collect_procs_anon() and such). And
current_is_single_threaded(), in which the current patch probably
leads to logic security bugs. And __uprobe_perf_filter(). Before my
refactoring of the ELF coredump logic in kernel 5.10 (commit
b2767d97f5ff75 and the ones before it), you'd have also probably
created memory corruption bugs in races between elf_core_dump() and
syscalls like mmap()/munmap(). (Note that this is not necessarily an
exhaustive list.)
On Fri, Jul 2, 2021 at 9:01 AM Andrei Vagin [off-list ref] wrote:
On Wed, Apr 14, 2021 at 08:46:40AM +0200, Jann Horn wrote:
quoted
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
It seems to me like your proposed API doesn't really fit either one of
those usecases well...
quoted
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
In this case, since you really only want an mm_struct to run code
under, it seems weird to create a whole task with its own PID and so
on. It seems to me like something similar to the /dev/kvm API would be
more appropriate here? Implementation options that I see for that
would be:
1. mm_struct-based:
a set of syscalls to create a new mm_struct,
change memory mappings under that mm_struct, and switch to it
I like the idea to have a handle for mm. Instead of pid, we will pass
this handle to process_vm_exec. We have pidfd for processes and we can
introduce mmfd for mm_struct.
I personally think that it might be quite unwieldy when it comes to
the restrictions you get from trying to have shared memory with the
owning process - I'm having trouble figuring out how you can implement
copy-on-write semantics without relying on copy-on-write logic in the
host OS and without being able to use userfaultfd.
But if that's not a problem somehow, and you can find some reasonable
way to handle memory usage accounting and fix up everything that
assumes that multithreaded userspace threads don't switch ->mm, I
guess this might work for your usecase.
quoted
2. pagetable-mirroring-based:
like /dev/kvm, an API to create a new pagetable, mirror parts of
the mm_struct's pagetables over into it with modified permissions
(like KVM_SET_USER_MEMORY_REGION),
and run code under that context.
page fault handling would first handle the fault against mm->pgd
as normal, then mirror the PTE over into the secondary pagetables.
invalidation could be handled with MMU notifiers.
I found this idea interesting and decided to look at it more closely.
After reading the kernel code for a few days, I realized that it would
not be easy to implement something like this,
Yeah, it might need architecture-specific code to flip the page tables
on userspace entry/exit, and maybe also for mirroring them. And for
the TLB flushing logic...
but more important is that
I don’t understand what problem it solves. Will it simplify the
user-space code? I don’t think so. Will it improve performance? It is
unclear for me too.
Some reasons I can think of are:
- direct guest memory access: I imagined you'd probably want to be able to
directly access userspace memory from the supervisor, and
with this approach that'd become easy.
- integration with on-demand paging of the host OS: You'd be able to
create things like file-backed copy-on-write mappings from the
host filesystem, or implement your own mappings backed by some kind
of storage using userfaultfd.
- sandboxing: For sandboxing usecases (not your usecase), it would be
possible to e.g. create a read-only clone of the entire address space of a
process and give write access to specific parts of it, or something
like that.
These address space clones could potentially be created and destroyed
fairly quickly.
- accounting: memory usage would be automatically accounted to the
supervisor process, so even without a parasite process, you'd be able
to see the memory usage correctly in things like "top".
- small (non-pageable) memory footprint in the host kernel:
The only things the host kernel would have to persistently store would be
the normal MM data structures for the supervisor plus the mappings
from "guest userspace" memory ranges to supervisor memory ranges;
userspace pagetables would be discardable, and could even be shared
with those of the supervisor in cases where the alignment fits.
So with this, large anonymous mappings with 4K granularity only cost you
~0.20% overhead across host and guest address space; without this, if you
used shared mappings instead, you'd pay twice that for every 2MiB range
from which parts are accessed in both contexts, plus probably another
~0.2% or so for the "struct address_space"?
- all memory-management-related syscalls could be directly performed
in the "kernel" process
But yeah, some of those aren't really relevant for your usecase, and I
guess things like the accounting aspect could just as well be solved
differently...
First, in the KVM case, we have a few big linear mappings and need to
support one “shadow” address space. In the case of sandboxes, we can
have a tremendous amount of mappings and many address spaces that we
need to manage. Memory mappings will be mapped with different addresses
in a supervisor address space and “guest” address spaces. If guest
address spaces will not have their mm_structs, we will need to reinvent
vma-s in some form. If guest address spaces have mm_structs, this will
look similar to https://lwn.net/Articles/830648/.
Second, each pagetable is tied up with mm_stuct. You suggest creating
new pagetables that will not have their mm_struct-s (sorry if I
misunderstood something).
Yeah, that's what I had in mind, page tables without an mm_struct.
I am not sure that it will be easy to
implement. How many corner cases will be there?
Yeah, it would require some work around TLB flushing and entry/exit
from userspace. But from a high-level perspective it feels to me like
a change with less systematic impact. Maybe I'm wrong about that.
As for page faults in a secondary address space, we will need to find a
fault address in the main address space, handle the fault there and then
mirror the PTE to the secondary pagetable.
Right.
Effectively, it means that
page faults will be handled in two address spaces. Right now, we use
memfd and shared mappings. It means that each fault is handled only in
one address space, and we map a guest memory region to the supervisor
address space only when we need to access it. A large portion of guest
anonymous memory is never mapped to the supervisor address space.
Will an overhead of mirrored address spaces be smaller than memfd shared
mappings? I am not sure.
But as long as the mappings are sufficiently big and aligned properly,
or you explicitly manage the supervisor address space, some of that
cost disappears: E.g. even if a page is mapped in both address spaces,
you wouldn't have a memory cost for the second mapping if the page
tables are shared.
Third, this approach will not get rid of having process_vm_exec. We will
need to switch to a guest address space with a specified state and
switch back on faults or syscalls.
Yeah, you'd still need a syscall for running code under a different
set of page tables. But that's something that KVM _almost_ already
does.
If the main concern is the ability to
run syscalls on a remote mm, we can think about how to fix this. I see
two ways what we can do here:
* Specify the exact list of system calls that are allowed. The first
three candidates are mmap, munmap, and vmsplice.
* Instead of allowing us to run system calls, we can implement this in
the form of commands. In the case of sandboxes, we need to implement
only two commands to create and destroy memory mappings in a target
address space.
FWIW, there is precedent for something similar: The Android folks
already added process_madvise() for remotely messing with the VMAs of
another process to some degree.
I'm pretty sure you're not currently allowed to overwrite the ->mm
pointer of a userspace thread. For example, zap_threads() assumes that
all threads running under a process have the same ->mm. (And if you're
fiddling with ->mm stuff, you should probably CC linux-mm@.)
As far as I understand, only kthreads are allowed to do this (as
implemented in kthread_use_mm()).
kthread_use_mm() was renamed from use_mm in the v5.8 kernel. Before
that, it wasn't used for user processes in the kernel, but it was
exported for modules, and we used it without any visible problems. We
understood that there could be some issues like zap_threads and it was
one of reasons why we decided to introduce this system call.
I understand that there are no places in the kernel where we change mm
of user threads back and forth, but are there any real concerns why we
should not do that? I agree that zap_threads should be fixed, but it
will the easy one.
My point is that if you break a preexisting assumption like this,
you'll have to go through the kernel and search for places that rely
on this assumption, and fix them up, which may potentially require
thinking about what kinds of semantics would actually be appropriate
there. Like the MCE killing logic (collect_procs_anon() and such). And
current_is_single_threaded(), in which the current patch probably
leads to logic security bugs. And __uprobe_perf_filter(). Before my
refactoring of the ELF coredump logic in kernel 5.10 (commit
b2767d97f5ff75 and the ones before it), you'd have also probably
created memory corruption bugs in races between elf_core_dump() and
syscalls like mmap()/munmap(). (Note that this is not necessarily an
exhaustive list.)
There’s nmi_uaccess_okay(), and its callers assume that, when a task is perf tracing itself, that an event on that task with nmi_uaccess_okay() means that uaccess will access that task’s memory.
Core dump code probably expects that dumping memory will access the correct mm.
I cannot fathom why any kind of remote vm access touched FPU state at all.
What PKRU value is supposed to be used when doing mm swap shenanigans? How about PASID?
What happens if one task attempts to issue a KVM ioctl while its mm is swapped?
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
process_vm_exec swaps the current address space with an address space of
a specified process, sets a state from sigcontex and resumes the process.
When a process receives a signal or calls a system call,
process_vm_exec saves the process state back to sigcontext, restores the
origin address space, restores the origin process state, and returns to
userspace.
If it was interrupted by a signal and the signal is in the user_mask,
the signal is dequeued and information about it is saved in uinfo.
If process_vm_exec is interrupted by a system call, a synthetic siginfo
for the SIGSYS signal is generated.
The behavior of this system call is similar to PTRACE_SYSEMU but
everything is happing in the context of one process, so
process_vm_exec shows a better performance.
PTRACE_SYSEMU is primarily used to implement sandboxes (application
kernels) like User-mode Linux or gVisor. These type of sandboxes
intercepts applications system calls and acts as the guest kernel.
A simple benchmark, where a "tracee" process executes systems calls in a
loop and a "tracer" process traps syscalls and handles them just
incrementing the tracee instruction pointer to skip the syscall
instruction shows that process_vm_exec works more than 5 times faster
than PTRACE_SYSEMU.
[...]
+long swap_vm_exec_context(struct sigcontext __user *uctx)
+{
+ struct sigcontext ctx = {};
+ sigset_t set = {};
+
+
+ if (copy_from_user(&ctx, uctx, CONTEXT_COPY_SIZE))
+ return -EFAULT;
+ /* A floating point state is managed from user-space. */
+ if (ctx.fpstate != 0)
+ return -EINVAL;
+ if (!user_access_begin(uctx, sizeof(*uctx)))
+ return -EFAULT;
+ unsafe_put_sigcontext(uctx, NULL, current_pt_regs(), (&set), Efault);
+ user_access_end();
+
+ if (__restore_sigcontext(current_pt_regs(), &ctx, 0))
+ goto badframe;
+
+ return 0;
+Efault:
+ user_access_end();
+badframe:
+ signal_fault(current_pt_regs(), uctx, "swap_vm_exec_context");
+ return -EFAULT;
+}
Comparing the pieces of context that restore_sigcontext() restores
with what a normal task switch does (see __switch_to() and callees), I
noticed: On CPUs with FSGSBASE support, I think sandboxed code could
overwrite FSBASE/GSBASE using the WRFSBASE/WRGSBASE instructions,
causing the supervisor to access attacker-controlled addresses when it
tries to access a thread-local variable like "errno"? Signal handling
saves the segment registers, but not the FS/GS base addresses.
jannh@laptop:~/test$ cat signal_gsbase.c
// compile with -mfsgsbase
#include <stdio.h>
#include <signal.h>
#include <immintrin.h>
void signal_handler(int sig, siginfo_t *info, void *ucontext_) {
puts("signal handler");
_writegsbase_u64(0x12345678);
}
int main(void) {
struct sigaction new_act = {
.sa_sigaction = signal_handler,
.sa_flags = SA_SIGINFO
};
sigaction(SIGUSR1, &new_act, NULL);
printf("original gsbase is 0x%lx\n", _readgsbase_u64());
raise(SIGUSR1);
printf("post-signal gsbase is 0x%lx\n", _readgsbase_u64());
}
jannh@laptop:~/test$ gcc -o signal_gsbase signal_gsbase.c -mfsgsbase
jannh@laptop:~/test$ ./signal_gsbase
original gsbase is 0x0
signal handler
post-signal gsbase is 0x12345678
jannh@laptop:~/test$
So to make this usable for a sandboxing usecase, you'd also have to
save and restore FSBASE/GSBASE, just like __switch_to().
But here you save the sigcontext without FPU state.
quoted
+
+ if (__restore_sigcontext(current_pt_regs(), &ctx, 0))
+ goto badframe;
And here you restore sigcontext, *with* FPU state. At which point your
FPU state is irrecoverably lost.
process_vm_exec doesn't change a process FPU state. Unlike signals, here
we can control it from a user-space. A process can set an FPU state
before process_vm_exec and then retore its FPU state after the
call.
This version of patches has a bug that I fixed in my tree when I
implemented the user-space part for gVisor. I didn't take into account
that restore_sigcontext(ctx) clears a process fpu state if ctx->fpstate
is zero. I moved fpu__restore_sig out from __restore_sigcontext to fix
this issue:
https://github.com/avagin/linux-task-diag/commit/55b7194d00ff
Also, I'm not at all convinced this can ever do the right thing when the
tasks don't agree on what the FPU state is. I suppose in the best case
the save will EFAULT.
From: Andy Lutomirski <luto@kernel.org> Date: 2021-07-02 22:44:46
On 4/13/21 10:52 PM, Andrei Vagin wrote:
process_vm_exec has two modes:
* Execute code in an address space of a target process and stop on any
signal or system call.
We already have a perfectly good context switch mechanism: context
switches. If you execute code, you are basically guaranteed to be
subject to being hijacked, which means you pretty much can't allow
syscalls. But there's a lot of non-syscall state, and I think context
switching needs to be done with extreme care.
(Just as example, suppose you switch mms, then set %gs to point to the
LDT, then switch back. Now you're in a weird state. With %ss the plot
is a bit thicker. And there are emulated vsyscalls and such.)
If you, PeterZ, and the UMCG could all find an acceptable, efficient way
to wake-and-wait so you can switch into an injected task in the target
process and switch back quickly, then I think a much nicer solution will
become available.
* Execute a system call in an address space of a target process.
I could get behind this, but there are plenty of cans of worms to watch
out for. Serious auditing would be needed.
From: Andrei Vagin <hidden> Date: 2021-07-02 22:52:33
On Fri, Jul 02, 2021 at 10:56:38PM +0200, Jann Horn wrote:
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
This change introduces the new system call:
process_vm_exec(pid_t pid, struct sigcontext *uctx, unsigned long flags,
siginfo_t * uinfo, sigset_t *sigmask, size_t sizemask)
process_vm_exec allows to execute the current process in an address
space of another process.
process_vm_exec swaps the current address space with an address space of
a specified process, sets a state from sigcontex and resumes the process.
When a process receives a signal or calls a system call,
process_vm_exec saves the process state back to sigcontext, restores the
origin address space, restores the origin process state, and returns to
userspace.
If it was interrupted by a signal and the signal is in the user_mask,
the signal is dequeued and information about it is saved in uinfo.
If process_vm_exec is interrupted by a system call, a synthetic siginfo
for the SIGSYS signal is generated.
The behavior of this system call is similar to PTRACE_SYSEMU but
everything is happing in the context of one process, so
process_vm_exec shows a better performance.
PTRACE_SYSEMU is primarily used to implement sandboxes (application
kernels) like User-mode Linux or gVisor. These type of sandboxes
intercepts applications system calls and acts as the guest kernel.
A simple benchmark, where a "tracee" process executes systems calls in a
loop and a "tracer" process traps syscalls and handles them just
incrementing the tracee instruction pointer to skip the syscall
instruction shows that process_vm_exec works more than 5 times faster
than PTRACE_SYSEMU.
[...]
quoted
+long swap_vm_exec_context(struct sigcontext __user *uctx)
+{
+ struct sigcontext ctx = {};
+ sigset_t set = {};
+
+
+ if (copy_from_user(&ctx, uctx, CONTEXT_COPY_SIZE))
+ return -EFAULT;
+ /* A floating point state is managed from user-space. */
+ if (ctx.fpstate != 0)
+ return -EINVAL;
+ if (!user_access_begin(uctx, sizeof(*uctx)))
+ return -EFAULT;
+ unsafe_put_sigcontext(uctx, NULL, current_pt_regs(), (&set), Efault);
+ user_access_end();
+
+ if (__restore_sigcontext(current_pt_regs(), &ctx, 0))
+ goto badframe;
+
+ return 0;
+Efault:
+ user_access_end();
+badframe:
+ signal_fault(current_pt_regs(), uctx, "swap_vm_exec_context");
+ return -EFAULT;
+}
Comparing the pieces of context that restore_sigcontext() restores
with what a normal task switch does (see __switch_to() and callees), I
noticed: On CPUs with FSGSBASE support, I think sandboxed code could
overwrite FSBASE/GSBASE using the WRFSBASE/WRGSBASE instructions,
causing the supervisor to access attacker-controlled addresses when it
tries to access a thread-local variable like "errno"? Signal handling
saves the segment registers, but not the FS/GS base addresses.
jannh@laptop:~/test$ cat signal_gsbase.c
// compile with -mfsgsbase
#include <stdio.h>
#include <signal.h>
#include <immintrin.h>
void signal_handler(int sig, siginfo_t *info, void *ucontext_) {
puts("signal handler");
_writegsbase_u64(0x12345678);
}
int main(void) {
struct sigaction new_act = {
.sa_sigaction = signal_handler,
.sa_flags = SA_SIGINFO
};
sigaction(SIGUSR1, &new_act, NULL);
printf("original gsbase is 0x%lx\n", _readgsbase_u64());
raise(SIGUSR1);
printf("post-signal gsbase is 0x%lx\n", _readgsbase_u64());
}
jannh@laptop:~/test$ gcc -o signal_gsbase signal_gsbase.c -mfsgsbase
jannh@laptop:~/test$ ./signal_gsbase
original gsbase is 0x0
signal handler
post-signal gsbase is 0x12345678
jannh@laptop:~/test$
So to make this usable for a sandboxing usecase, you'd also have to
save and restore FSBASE/GSBASE, just like __switch_to().
From: Andrei Vagin <hidden> Date: 2021-07-18 00:42:42
On Fri, Jul 02, 2021 at 05:12:02PM +0200, Jann Horn wrote:
On Fri, Jul 2, 2021 at 9:01 AM Andrei Vagin [off-list ref] wrote:
quoted
On Wed, Apr 14, 2021 at 08:46:40AM +0200, Jann Horn wrote:
quoted
On Wed, Apr 14, 2021 at 7:59 AM Andrei Vagin [off-list ref] wrote:
quoted
We already have process_vm_readv and process_vm_writev to read and write
to a process memory faster than we can do this with ptrace. And now it
is time for process_vm_exec that allows executing code in an address
space of another process. We can do this with ptrace but it is much
slower.
= Use-cases =
It seems to me like your proposed API doesn't really fit either one of
those usecases well...
quoted
Here are two known use-cases. The first one is “application kernel”
sandboxes like User-mode Linux and gVisor. In this case, we have a
process that runs the sandbox kernel and a set of stub processes that
are used to manage guest address spaces. Guest code is executed in the
context of stub processes but all system calls are intercepted and
handled in the sandbox kernel. Right now, these sort of sandboxes use
PTRACE_SYSEMU to trap system calls, but the process_vm_exec can
significantly speed them up.
In this case, since you really only want an mm_struct to run code
under, it seems weird to create a whole task with its own PID and so
on. It seems to me like something similar to the /dev/kvm API would be
more appropriate here? Implementation options that I see for that
would be:
1. mm_struct-based:
a set of syscalls to create a new mm_struct,
change memory mappings under that mm_struct, and switch to it
I like the idea to have a handle for mm. Instead of pid, we will pass
this handle to process_vm_exec. We have pidfd for processes and we can
introduce mmfd for mm_struct.
I personally think that it might be quite unwieldy when it comes to
the restrictions you get from trying to have shared memory with the
owning process - I'm having trouble figuring out how you can implement
copy-on-write semantics without relying on copy-on-write logic in the
host OS and without being able to use userfaultfd.
It is easy. COW mappings are mapped to guest address spaces without the
write permission. If one of processes wants to write something, it
triggers a fault that is handled in the Sentry (supervisor/kernel).
But if that's not a problem somehow, and you can find some reasonable
way to handle memory usage accounting and fix up everything that
assumes that multithreaded userspace threads don't switch ->mm, I
guess this might work for your usecase.
quoted
quoted
2. pagetable-mirroring-based:
like /dev/kvm, an API to create a new pagetable, mirror parts of
the mm_struct's pagetables over into it with modified permissions
(like KVM_SET_USER_MEMORY_REGION),
and run code under that context.
page fault handling would first handle the fault against mm->pgd
as normal, then mirror the PTE over into the secondary pagetables.
invalidation could be handled with MMU notifiers.
I found this idea interesting and decided to look at it more closely.
After reading the kernel code for a few days, I realized that it would
not be easy to implement something like this,
Yeah, it might need architecture-specific code to flip the page tables
on userspace entry/exit, and maybe also for mirroring them. And for
the TLB flushing logic...
quoted
but more important is that
I don’t understand what problem it solves. Will it simplify the
user-space code? I don’t think so. Will it improve performance? It is
unclear for me too.
Some reasons I can think of are:
- direct guest memory access: I imagined you'd probably want to be able to
directly access userspace memory from the supervisor, and
with this approach that'd become easy.
Right now, we use shared memory regions for that and they work fine. As
I already mentioned the most part of memory are never mapped to the
supervisor address space.
- integration with on-demand paging of the host OS: You'd be able to
create things like file-backed copy-on-write mappings from the
host filesystem, or implement your own mappings backed by some kind
of storage using userfaultfd.
This isn't a problem either...
- sandboxing: For sandboxing usecases (not your usecase), it would be
possible to e.g. create a read-only clone of the entire address space of a
process and give write access to specific parts of it, or something
like that.
These address space clones could potentially be created and destroyed
fairly quickly.
This is a very valid example and I would assume this is where your idea
was coming from. I have some doubts about the idea of additional
sub-page-tables in the kernel, but I know a good way how to implement
your idea with KVM. You can look at how the KVM platform is implemented in
gVisor and this sort of sandboxing can be implemented in the same way.
In a few words, we create a KVM virtual machine, repeat the process
address space in the guest ring0, implement basic operating system-level
stubs, so that the process can jump between the host ring3 and the guest
ring0.
https://github.com/google/gvisor/blob/master/pkg/ring0/https://github.com/google/gvisor/tree/master/pkg/sentry/platform/kvm
When we have all these bits, we can create any page tables for a guest
ring3 and run untrusted code there. The sandbox process switches to
the guest ring0 and then it switches to a guest ring3 with a specified
page tables and a state.
https://cs.opensource.google/gvisor/gvisor/+/master:pkg/sentry/platform/kvm/machine_amd64.go;l=356
With this scheme, the sandbox process will have direct access to page
tables and will be able to change them.
- accounting: memory usage would be automatically accounted to the
supervisor process, so even without a parasite process, you'd be able
to see the memory usage correctly in things like "top".
- small (non-pageable) memory footprint in the host kernel:
The only things the host kernel would have to persistently store would be
the normal MM data structures for the supervisor plus the mappings
from "guest userspace" memory ranges to supervisor memory ranges;
userspace pagetables would be discardable, and could even be shared
with those of the supervisor in cases where the alignment fits.
So with this, large anonymous mappings with 4K granularity only cost you
~0.20% overhead across host and guest address space; without this, if you
used shared mappings instead, you'd pay twice that for every 2MiB range
from which parts are accessed in both contexts, plus probably another
~0.2% or so for the "struct address_space"?
If we use shared mappings, we don't map the most part of guest memory to
the supervisor address space and don't have page tables for it there. I
would say that this is a question where a memory footprint will be
smaller...
- all memory-management-related syscalls could be directly performed
in the "kernel" process
But yeah, some of those aren't really relevant for your usecase, and I
guess things like the accounting aspect could just as well be solved
differently...
quoted
First, in the KVM case, we have a few big linear mappings and need to
support one “shadow” address space. In the case of sandboxes, we can
have a tremendous amount of mappings and many address spaces that we
need to manage. Memory mappings will be mapped with different addresses
in a supervisor address space and “guest” address spaces. If guest
address spaces will not have their mm_structs, we will need to reinvent
vma-s in some form. If guest address spaces have mm_structs, this will
look similar to https://lwn.net/Articles/830648/.
Second, each pagetable is tied up with mm_stuct. You suggest creating
new pagetables that will not have their mm_struct-s (sorry if I
misunderstood something).
Yeah, that's what I had in mind, page tables without an mm_struct.
quoted
I am not sure that it will be easy to
implement. How many corner cases will be there?
Yeah, it would require some work around TLB flushing and entry/exit
from userspace. But from a high-level perspective it feels to me like
a change with less systematic impact. Maybe I'm wrong about that.
quoted
As for page faults in a secondary address space, we will need to find a
fault address in the main address space, handle the fault there and then
mirror the PTE to the secondary pagetable.
Right.
quoted
Effectively, it means that
page faults will be handled in two address spaces. Right now, we use
memfd and shared mappings. It means that each fault is handled only in
one address space, and we map a guest memory region to the supervisor
address space only when we need to access it. A large portion of guest
anonymous memory is never mapped to the supervisor address space.
Will an overhead of mirrored address spaces be smaller than memfd shared
mappings? I am not sure.
But as long as the mappings are sufficiently big and aligned properly,
or you explicitly manage the supervisor address space, some of that
cost disappears: E.g. even if a page is mapped in both address spaces,
you wouldn't have a memory cost for the second mapping if the page
tables are shared.
You are right. It is interesting how many pte-s will be shared. For
example, if a guest process forks a child, all anon memory will be COW,
this means we will need to remove the W bit from pte-s and so we will
need to allocate pte-s for both processes...
quoted
Third, this approach will not get rid of having process_vm_exec. We will
need to switch to a guest address space with a specified state and
switch back on faults or syscalls.
Yeah, you'd still need a syscall for running code under a different
set of page tables. But that's something that KVM _almost_ already
does.
I don't understand this analogy with KVM...
quoted
If the main concern is the ability to
run syscalls on a remote mm, we can think about how to fix this. I see
two ways what we can do here:
* Specify the exact list of system calls that are allowed. The first
three candidates are mmap, munmap, and vmsplice.
* Instead of allowing us to run system calls, we can implement this in
the form of commands. In the case of sandboxes, we need to implement
only two commands to create and destroy memory mappings in a target
address space.
FWIW, there is precedent for something similar: The Android folks
already added process_madvise() for remotely messing with the VMAs of
another process to some degree.
From: Andrei Vagin <hidden> Date: 2021-07-18 01:38:46
On Fri, Jul 02, 2021 at 03:44:41PM -0700, Andy Lutomirski wrote:
On 4/13/21 10:52 PM, Andrei Vagin wrote:
quoted
process_vm_exec has two modes:
* Execute code in an address space of a target process and stop on any
signal or system call.
We already have a perfectly good context switch mechanism: context
switches. If you execute code, you are basically guaranteed to be
subject to being hijacked, which means you pretty much can't allow
syscalls. But there's a lot of non-syscall state, and I think context
switching needs to be done with extreme care.
(Just as example, suppose you switch mms, then set %gs to point to the
LDT, then switch back. Now you're in a weird state. With %ss the plot
is a bit thicker. And there are emulated vsyscalls and such.)
If you, PeterZ, and the UMCG could all find an acceptable, efficient way
to wake-and-wait so you can switch into an injected task in the target
process and switch back quickly, then I think a much nicer solution will
become available.
I know about umcg and I even did a prototype that used fuxet_swap (the
previous attempt of umcg). Here are a few problems and maybe you will
have some ideas on how to solve them.
The main question is how to hijack a stub process where a guest code is
executing. We need to trap system calls, memory faults, and other
exceptions and handle them in the Sentry (supervisor/kernel). All
interested events except system calls generate signals. We can use
seccomp to get signals on system calls too. In my prototype, a guest
code is running in stub processes. One stub process is for each guest
address space. In a stub process, I set a signal handler for SIGSEGV,
SIGBUS, SIGFPE, SIGSYS, SIGILL, set an alternate signal stack, and set
seccomp rules. The signal handler communicates with the Sentry
(supervisor/kernel) via shared memory and uses futex_swap to make fast
switches to the Sentry and back to a stub process.
Here are a few problems. First, we have a signal handler code, its
stack, and a shared memory region in a guest address space, and we need
to guarantee that a guest code will not be able to use them to do
something unexpected.
The second problem is performance. It is much faster if we compare it
with the ptrace platform, but it is still a few times slower than
process_vm_exec. Signal handling is expensive. The kernel has to
generate a signal frame, execute a signal handler, and then it needs to
call rt_sigreturn. Futex_swap makes fast context switches, but it is
still slower than process_vm_exec. UMCG should be faster because it
doesn’t have a futex overhead.
Andy, what do you think about the idea to rework process_vm_exec so that
it executes code and syscalls in the context of a target process?
Maybe you see other ways how we can “hijack” a remote process?
Thanks,
Andrei
quoted
* Execute a system call in an address space of a target process.
I could get behind this, but there are plenty of cans of worms to watch
out for. Serious auditing would be needed.