diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index ad8ae4008..28bd50977 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -314,6 +314,14 @@ steps: agents: <<: *kvm_agents arch: "amd64" + - <<: *common + <<: *docker + <<: *source_test + label: ":rocket: Systrap tests" + command: make systrap-tests + agents: + <<: *kvm_agents + arch: "amd64" - <<: *common <<: *docker label: ":weight_lifter: Fsstress test" diff --git a/Makefile b/Makefile index a1087e774..addfd5072 100644 --- a/Makefile +++ b/Makefile @@ -298,6 +298,11 @@ kvm-tests: load-basic $(RUNTIME_BIN) @$(call test_runtime,$(RUNTIME),$(INTEGRATION_TARGETS)) .PHONY: kvm-tests +systrap-tests: load-basic $(RUNTIME_BIN) + @$(call install_runtime,$(RUNTIME),--platform=systrap) + @$(call test_runtime,$(RUNTIME),$(INTEGRATION_TARGETS)) +.PHONY: systrap-tests + iptables-tests: load-iptables $(RUNTIME_BIN) @sudo modprobe iptable_filter @sudo modprobe ip6table_filter diff --git a/nogo.yaml b/nogo.yaml index b2572adce..c9ca8cc53 100644 --- a/nogo.yaml +++ b/nogo.yaml @@ -192,6 +192,9 @@ analyzers: - pkg/sentry/fsutil/host_file_mapper_unsafe.go # Special case. - pkg/sentry/platform/kvm/bluepill_unsafe.go # Special case. - pkg/sentry/platform/kvm/machine_unsafe.go # Special case. + - pkg/sentry/platform/systrap/stub_unsafe.go # Special case. + - pkg/sentry/platform/systrap/syscall_thread_unsafe.go # Special case. + - pkg/sentry/platform/systrap/sysmsg_thread_unsafe.go # Special case. - pkg/sentry/platform/safecopy/safecopy_unsafe.go # Special case. - pkg/sentry/usage/memory_unsafe.go # Special case. - pkg/sentry/vfs/mount_unsafe.go # Special case. diff --git a/pkg/sentry/kernel/task_run.go b/pkg/sentry/kernel/task_run.go index 34bc0d33a..1decfb756 100644 --- a/pkg/sentry/kernel/task_run.go +++ b/pkg/sentry/kernel/task_run.go @@ -168,6 +168,12 @@ func (app *runApp) execute(t *Task) taskRunState { // a pending signal, causing another interruption, but that signal should // not interact with the interrupted syscall.) if t.haveSyscallReturn { + if err := t.p.PullFullState(t.MemoryManager().AddressSpace(), t.Arch()); err != nil { + t.Warningf("Unable to pull a full state: %v", err) + t.PrepareExit(linux.WaitStatusExit(int32(ExtractErrno(err, -1)))) + return (*runExit)(nil) + } + if sre, ok := linuxerr.SyscallRestartErrorFromReturn(t.Arch().Return()); ok { if sre == linuxerr.ERESTART_RESTARTBLOCK { t.Debugf("Restarting syscall %d with restart block: not interrupted by handled signal", t.Arch().SyscallNo()) diff --git a/pkg/sentry/platform/systrap/BUILD b/pkg/sentry/platform/systrap/BUILD new file mode 100644 index 000000000..ef8248ac1 --- /dev/null +++ b/pkg/sentry/platform/systrap/BUILD @@ -0,0 +1,103 @@ +load("//tools:arch.bzl", "arch_genrule", "select_arch") +load("//tools:defs.bzl", "go_library") +load("//tools/nogo:defs.bzl", "nogo_facts") +load("//tools/go_generics:defs.bzl", "go_template_instance") + +package(licenses = ["notice"]) + +nogo_facts( + name = "stub_impl", + srcs = [ + "stub_defs.go", + "syscall_thread_defs.go", + ], + output = "stub_impl.s", + template = select_arch( + amd64 = "stub_amd64.s", + arm64 = "stub_arm64.s", + ), + deps = [ + "//pkg/abi/linux", + "//pkg/atomicbitops", + "//pkg/hostarch", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +arch_genrule( + name = "stub_impl_arch", + src = ":stub_impl", + template = "stub_impl_%s.s", +) + +go_template_instance( + name = "subprocess_list", + out = "subprocess_list.go", + package = "systrap", + prefix = "subprocess", + template = "//pkg/ilist:generic_list", + types = { + "Linker": "*subprocess", + "Element": "*subprocess", + }, +) + +go_library( + name = "systrap", + srcs = [ + "filters.go", + "filters_amd64.go", + "filters_arm64.go", + "lib_amd64.s", + "lib_arm64.s", + "stub_defs.go", + "stub_unsafe.go", + "subprocess.go", + "subprocess_amd64.go", + "subprocess_amd64_unsafe.go", + "subprocess_arm64.go", + "subprocess_arm64_unsafe.go", + "subprocess_linux.go", + "subprocess_linux_unsafe.go", + "subprocess_list.go", + "subprocess_pool.go", + "subprocess_unsafe.go", + "syscall_thread.go", + "syscall_thread_amd64.go", + "syscall_thread_arm64.go", + "syscall_thread_defs.go", + "syscall_thread_unsafe.go", + "sysmsg_thread.go", + "sysmsg_thread_amd64.go", + "sysmsg_thread_arm64.go", + "sysmsg_thread_unsafe.go", + "systrap.go", + "systrap_amd64.go", + "systrap_arm64.go", + "systrap_arm64_unsafe.go", + "systrap_unsafe.go", + ":stub_impl_arch", + ], + visibility = ["//:sandbox"], + deps = [ + "//pkg/abi/linux", + "//pkg/atomicbitops", + "//pkg/context", + "//pkg/cpuid", + "//pkg/hostarch", + "//pkg/log", + "//pkg/memutil", + "//pkg/pool", + "//pkg/safecopy", + "//pkg/seccomp", + "//pkg/sentry/arch", + "//pkg/sentry/memmap", + "//pkg/sentry/pgalloc", + "//pkg/sentry/platform", + "//pkg/sentry/platform/interrupt", + "//pkg/sentry/platform/systrap/sysmsg", + "//pkg/sentry/platform/systrap/usertrap", + "//pkg/sentry/usage", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/pkg/sentry/platform/systrap/README.md b/pkg/sentry/platform/systrap/README.md new file mode 100644 index 000000000..540c33c4b --- /dev/null +++ b/pkg/sentry/platform/systrap/README.md @@ -0,0 +1,33 @@ +# The systrap platform + +This platform is similar with the ptrace platform with the difference how system +calls, page-faults and other exceptions handled. + +The kernel allows setting seccomp filters (SECCOMP_RET_TRAP), so that each time +when a thread tries to call a filtered system call, it will receive the SIGSYS +signal. + +With this kernel feature, all stub thread events what have to be handled in the +sentry triggers signals. This means that they can be handled from a signal +handler. + +The systrap platform includes the sysmsg module which implements a stub signal +handler and a protocol of communications of stub threads and the Sentry. + +The initializations of a new stub thread includes next steps: + +* installing seccomp filters to trap all user system calls. +* setting an alternate signal stack which is shared with the Sentry. +* setting the sysmsg signal handler for SIGSYS, SIGSEGV, SIGBUS, SIGFPE, + SIGTRAP, SIGILL. + +User code is executed in context of a stub thread. When it calls a system call +or triggers page-fault, the signal handler is started. It notifies the Sentry +about a new signal, then the Sentry handles this event and notifies the system +thread back that it can continue running. + +When the kernel prepares to execute the signal handler, it generates a signal +frame which contains a process state (registers, FPU state, etc). Then when the +kernel resumes a process, the process state is restored from this frame. The +signal frame is saved on a signal handler stack which is shared with the Sentry. +This allows us to read and modify the thread state from the Sentry. diff --git a/pkg/sentry/platform/systrap/filters.go b/pkg/sentry/platform/systrap/filters.go new file mode 100644 index 000000000..0b3c620bc --- /dev/null +++ b/pkg/sentry/platform/systrap/filters.go @@ -0,0 +1,78 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" +) + +// SyscallFilters returns syscalls made exclusively by the systrap platform. +func (p *Systrap) SyscallFilters() seccomp.SyscallRules { + r := seccomp.SyscallRules{ + unix.SYS_PTRACE: { + { + seccomp.EqualTo(unix.PTRACE_ATTACH), + }, + { + seccomp.EqualTo(unix.PTRACE_CONT), + seccomp.MatchAny{}, + seccomp.EqualTo(0), + seccomp.EqualTo(0), + }, + { + seccomp.EqualTo(unix.PTRACE_GETEVENTMSG), + }, + { + seccomp.EqualTo(unix.PTRACE_GETREGSET), + seccomp.MatchAny{}, + seccomp.EqualTo(linux.NT_PRSTATUS), + }, + { + seccomp.EqualTo(unix.PTRACE_GETSIGINFO), + }, + { + seccomp.EqualTo(unix.PTRACE_SETOPTIONS), + seccomp.MatchAny{}, + seccomp.EqualTo(0), + seccomp.EqualTo(unix.PTRACE_O_TRACESYSGOOD | unix.PTRACE_O_TRACEEXIT | unix.PTRACE_O_EXITKILL), + }, + { + seccomp.EqualTo(unix.PTRACE_SETREGSET), + seccomp.MatchAny{}, + seccomp.EqualTo(linux.NT_PRSTATUS), + }, + { + seccomp.EqualTo(linux.PTRACE_SETSIGMASK), + seccomp.MatchAny{}, + seccomp.EqualTo(8), + }, + { + seccomp.EqualTo(unix.PTRACE_SYSEMU), + seccomp.MatchAny{}, + seccomp.EqualTo(0), + seccomp.EqualTo(0), + }, + { + seccomp.EqualTo(unix.PTRACE_DETACH), + }, + }, + unix.SYS_TGKILL: {}, + unix.SYS_WAIT4: {}, + } + r.Merge(p.archSyscallFilters()) + return r +} diff --git a/pkg/sentry/platform/systrap/filters_amd64.go b/pkg/sentry/platform/systrap/filters_amd64.go new file mode 100644 index 000000000..5a8366439 --- /dev/null +++ b/pkg/sentry/platform/systrap/filters_amd64.go @@ -0,0 +1,27 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package systrap + +import ( + "gvisor.dev/gvisor/pkg/seccomp" +) + +// SyscallFilters returns syscalls made exclusively by the systrap platform. +func (*Systrap) archSyscallFilters() seccomp.SyscallRules { + return seccomp.SyscallRules{} +} diff --git a/pkg/sentry/platform/systrap/filters_arm64.go b/pkg/sentry/platform/systrap/filters_arm64.go new file mode 100644 index 000000000..a292958c5 --- /dev/null +++ b/pkg/sentry/platform/systrap/filters_arm64.go @@ -0,0 +1,42 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package systrap + +import ( + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" +) + +// SyscallFilters returns syscalls made exclusively by the systrap platform. +func (*Systrap) archSyscallFilters() seccomp.SyscallRules { + return seccomp.SyscallRules{ + unix.SYS_PTRACE: { + { + seccomp.EqualTo(unix.PTRACE_GETREGSET), + seccomp.MatchAny{}, + seccomp.EqualTo(linux.NT_ARM_TLS), + }, + { + seccomp.EqualTo(unix.PTRACE_SETREGSET), + seccomp.MatchAny{}, + seccomp.EqualTo(linux.NT_ARM_TLS), + }, + }, + } +} diff --git a/pkg/sentry/platform/systrap/lib_amd64.s b/pkg/sentry/platform/systrap/lib_amd64.s new file mode 100644 index 000000000..cc7bb66d0 --- /dev/null +++ b/pkg/sentry/platform/systrap/lib_amd64.s @@ -0,0 +1,20 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "funcdata.h" +#include "textflag.h" + +TEXT ·spinloop(SB),NOSPLIT,$0 + PAUSE + RET diff --git a/pkg/sentry/platform/systrap/lib_arm64.s b/pkg/sentry/platform/systrap/lib_arm64.s new file mode 100644 index 000000000..385c06548 --- /dev/null +++ b/pkg/sentry/platform/systrap/lib_arm64.s @@ -0,0 +1,20 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "funcdata.h" +#include "textflag.h" + +TEXT ·spinloop(SB),NOSPLIT,$0 + YIELD + RET diff --git a/pkg/sentry/platform/systrap/stub_amd64.s b/pkg/sentry/platform/systrap/stub_amd64.s new file mode 100644 index 000000000..2d5a6820e --- /dev/null +++ b/pkg/sentry/platform/systrap/stub_amd64.s @@ -0,0 +1,211 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "funcdata.h" +#include "textflag.h" + +#define SYS_GETPID {{ .import.unix.Constants.SYS_GETPID }} +#define SYS_EXIT {{ .import.unix.Constants.SYS_EXIT }} +#define SYS_KILL {{ .import.unix.Constants.SYS_KILL }} +#define SYS_GETPPID {{ .import.unix.Constants.SYS_GETPPID }} +#define SIGKILL {{ .import.unix.Constants.SIGKILL }} +#define SIGSTOP {{ .import.unix.Constants.SIGSTOP }} +#define SYS_PRCTL {{ .import.unix.Constants.SYS_PRCTL }} +#define PR_SET_PDEATHSIG {{ .import.unix.Constants.PR_SET_PDEATHSIG }} + +#define SYS_FUTEX {{ .import.unix.Constants.SYS_FUTEX }} +#define FUTEX_WAKE {{ .import.linux.Constants.FUTEX_WAKE }} +#define FUTEX_WAIT {{ .import.linux.Constants.FUTEX_WAIT }} + +#define NEW_STUB {{ .Constants._NEW_STUB }} +#define RUN_SYSCALL_LOOP {{ .Constants._RUN_SYSCALL_LOOP }} + +// syscallSentryMessage offsets. +#define SENTRY_MESSAGE_STATE {{ .syscallSentryMessage.state.Offset }} +#define SENTRY_MESSAGE_SYSNO {{ .syscallSentryMessage.sysno.Offset }} +#define SENTRY_MESSAGE_ARG0 ({{ .syscallSentryMessage.args.Offset }} + 0*8) +#define SENTRY_MESSAGE_ARG1 ({{ .syscallSentryMessage.args.Offset }} + 1*8) +#define SENTRY_MESSAGE_ARG2 ({{ .syscallSentryMessage.args.Offset }} + 2*8) +#define SENTRY_MESSAGE_ARG3 ({{ .syscallSentryMessage.args.Offset }} + 3*8) +#define SENTRY_MESSAGE_ARG4 ({{ .syscallSentryMessage.args.Offset }} + 4*8) +#define SENTRY_MESSAGE_ARG5 ({{ .syscallSentryMessage.args.Offset }} + 5*8) + +// syscallStubMessage offsets. +#define STUB_MESSAGE_OFFSET {{ .Constants.syscallStubMessageOffset }} +#define STUB_MESSAGE_RET {{ .syscallStubMessage.ret.Offset }} + +// initStubProcess bootstraps the child and sends itself SIGSTOP to wait for attach. +// +// R15 contains the expected PPID. R15 is used instead of a more typical DI +// since syscalls will clobber DI and createStub wants to pass a new PPID to +// grandchildren. +// +// This should not be used outside the context of a new ptrace child (as the +// function is otherwise a bunch of nonsense). +TEXT ·initStubProcess(SB),NOSPLIT,$0 +begin: + // N.B. This loop only executes in the context of a single-threaded + // fork child. + + MOVQ $SYS_PRCTL, AX + MOVQ $PR_SET_PDEATHSIG, DI + MOVQ $SIGKILL, SI + SYSCALL + + CMPQ AX, $0 + JNE error + + // If the parent already died before we called PR_SET_DEATHSIG then + // we'll have an unexpected PPID. + MOVQ $SYS_GETPPID, AX + SYSCALL + + CMPQ AX, $0 + JL error + + CMPQ AX, R15 + JNE parent_dead + + MOVQ $SYS_GETPID, AX + SYSCALL + + CMPQ AX, $0 + JL error + + MOVQ $0, BX + + // SIGSTOP to wait for attach. + // + // The SYSCALL instruction will be used for future syscall injection by + // thread.syscall. + MOVQ AX, DI + MOVQ $SYS_KILL, AX + MOVQ $SIGSTOP, SI + SYSCALL + + // The sentry sets BX to $NEW_STUB when creating stub process. + CMPQ BX, $NEW_STUB + JE clone + + // The sentry sets BX to $RUN_SYSCALL_LOOP when requesting a syscall + // thread. + CMPQ BX, $RUN_SYSCALL_LOOP + JE syscall_loop + + // Notify the Sentry that syscall exited. +done: + INT $3 + // Be paranoid. + JMP done +clone: + // subprocess.createStub clones a new stub process that is untraced, + // thus executing this code. We setup the PDEATHSIG before SIGSTOPing + // ourselves for attach by the tracer. + // + // R15 has been updated with the expected PPID. + CMPQ AX, $0 + JE begin + + // The clone syscall returns a non-zero value. + JMP done +error: + // Exit with -errno. + MOVQ AX, DI + NEGQ DI + MOVQ $SYS_EXIT, AX + SYSCALL + HLT + +parent_dead: + MOVQ $SYS_EXIT, AX + MOVQ $1, DI + SYSCALL + HLT + + // syscall_loop handles requests from the Sentry to execute syscalls. + // Look at syscall_thread for more details. + // + // syscall_loop is running without using the stack because it can be + // compromised by sysmsg (guest) threads that run in the same address + // space. +syscall_loop: + // while (sentryMessage->state != R13) { + // futex(sentryMessage->state, FUTEX_WAIT, 0, NULL, NULL, 0); + // } + MOVQ R12, DI + MOVQ $FUTEX_WAIT, SI + MOVQ $0, R10 + MOVQ $0, R8 + MOVQ $0, R9 +wait_for_syscall: + MOVL SENTRY_MESSAGE_STATE(DI), DX + CMPL DX, R13 + JE execute_syscall + + MOVQ $SYS_FUTEX, AX + SYSCALL + JMP wait_for_syscall + +execute_syscall: + // ret = syscall(sysno, args...) + MOVQ SENTRY_MESSAGE_SYSNO(R12), AX + MOVQ SENTRY_MESSAGE_ARG0(R12), DI + MOVQ SENTRY_MESSAGE_ARG1(R12), SI + MOVQ SENTRY_MESSAGE_ARG2(R12), DX + MOVQ SENTRY_MESSAGE_ARG3(R12), R10 + MOVQ SENTRY_MESSAGE_ARG4(R12), R8 + MOVQ SENTRY_MESSAGE_ARG5(R12), R9 + SYSCALL + + // stubMessage->ret = ret + MOVQ AX, (STUB_MESSAGE_OFFSET + STUB_MESSAGE_RET)(R12) + + // for { + // if futex(sentryMessage->state, FUTEX_WAKE, 1) == 1 { + // break; + // } + // } + MOVQ R12, DI + MOVQ $FUTEX_WAKE, SI + MOVQ $1, DX + MOVQ $0, R10 + MOVQ $0, R8 + MOVQ $0, R9 +wake_up_sentry: + MOVQ $SYS_FUTEX, AX + SYSCALL + // futex returns the number of waiters that were woken up. If futex + // returns 0 here, it means that the Sentry has not called futex_wait + // yet and we need to try again. The value of sentryMessage->state + // isn't changed, so futex_wake is the only way to wake up the Sentry. + CMPQ AX, $1 + JNE wake_up_sentry + + INCL R13 + JMP syscall_loop + +// func addrOfInitStubProcess() uintptr +TEXT ·addrOfInitStubProcess(SB), $0-8 + MOVQ $·initStubProcess(SB), AX + MOVQ AX, ret+0(FP) + RET + +// stubCall calls the stub function at the given address with the given PPID. +// +// This is a distinct function because stub, above, may be mapped at any +// arbitrary location, and stub has a specific binary API (see above). +TEXT ·stubCall(SB),NOSPLIT,$0-16 + MOVQ addr+0(FP), AX + MOVQ pid+8(FP), R15 + JMP AX diff --git a/pkg/sentry/platform/systrap/stub_arm64.s b/pkg/sentry/platform/systrap/stub_arm64.s new file mode 100644 index 000000000..916bd1953 --- /dev/null +++ b/pkg/sentry/platform/systrap/stub_arm64.s @@ -0,0 +1,205 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "funcdata.h" +#include "textflag.h" + +#define SYS_GETPID {{ .import.unix.Constants.SYS_GETPID }} +#define SYS_EXIT {{ .import.unix.Constants.SYS_EXIT }} +#define SYS_KILL {{ .import.unix.Constants.SYS_KILL }} +#define SYS_GETPPID {{ .import.unix.Constants.SYS_GETPPID }} +#define SIGKILL {{ .import.unix.Constants.SIGKILL }} +#define SIGSTOP {{ .import.unix.Constants.SIGSTOP }} +#define SYS_PRCTL {{ .import.unix.Constants.SYS_PRCTL }} +#define PR_SET_PDEATHSIG {{ .import.unix.Constants.PR_SET_PDEATHSIG }} + +#define SYS_FUTEX {{ .import.unix.Constants.SYS_FUTEX }} +#define FUTEX_WAKE {{ .import.linux.Constants.FUTEX_WAKE }} +#define FUTEX_WAIT {{ .import.linux.Constants.FUTEX_WAIT }} + +#define NEW_STUB {{ .Constants._NEW_STUB }} +#define RUN_SYSCALL_LOOP {{ .Constants._RUN_SYSCALL_LOOP }} + +// syscallSentryMessage offsets. +#define SENTRY_MESSAGE_STATE {{ .syscallSentryMessage.state.Offset }} +#define SENTRY_MESSAGE_SYSNO {{ .syscallSentryMessage.sysno.Offset }} +#define SENTRY_MESSAGE_ARG0 ({{ .syscallSentryMessage.args.Offset }} + 0*8) +#define SENTRY_MESSAGE_ARG1 ({{ .syscallSentryMessage.args.Offset }} + 1*8) +#define SENTRY_MESSAGE_ARG2 ({{ .syscallSentryMessage.args.Offset }} + 2*8) +#define SENTRY_MESSAGE_ARG3 ({{ .syscallSentryMessage.args.Offset }} + 3*8) +#define SENTRY_MESSAGE_ARG4 ({{ .syscallSentryMessage.args.Offset }} + 4*8) +#define SENTRY_MESSAGE_ARG5 ({{ .syscallSentryMessage.args.Offset }} + 5*8) + +// syscallStubMessage offsets. +#define STUB_MESSAGE_OFFSET {{ .Constants.syscallStubMessageOffset }} +#define STUB_MESSAGE_RET {{ .syscallStubMessage.ret.Offset }} + +// initStubProcess bootstraps the child and sends itself SIGSTOP to wait for attach. +// +// R7 contains the expected PPID. +// +// This should not be used outside the context of a new ptrace child (as the +// function is otherwise a bunch of nonsense). +TEXT ·initStubProcess(SB),NOSPLIT,$0 +begin: + // N.B. This loop only executes in the context of a single-threaded + // fork child. + + MOVD $SYS_PRCTL, R8 + MOVD $PR_SET_PDEATHSIG, R0 + MOVD $SIGKILL, R1 + SVC + + CMN $4095, R0 + BCS error + + // If the parent already died before we called PR_SET_DEATHSIG then + // we'll have an unexpected PPID. + MOVD $SYS_GETPPID, R8 + SVC + + CMP R0, R7 + BNE parent_dead + + MOVD $SYS_GETPID, R8 + SVC + + CMP $0x0, R0 + BLT error + + MOVD $0, R9 + + // SIGSTOP to wait for attach. + // + // The SYSCALL instruction will be used for future syscall injection by + // thread.syscall. + MOVD $SYS_KILL, R8 + MOVD $SIGSTOP, R1 + SVC + + // The sentry sets R9 to $NEW_STUB when creating stub process. + CMP $NEW_STUB, R9 + BEQ clone + + // The sentry sets R9 to $RUN_SYSCALL_LOOP when creating a new syscall + // thread. + CMP $RUN_SYSCALL_LOOP, R9 + BEQ syscall_loop + +done: + // Notify the Sentry that syscall exited. + BRK $3 + B done // Be paranoid. +clone: + // subprocess.createStub clones a new stub process that is untraced, + // thus executing this code. We setup the PDEATHSIG before SIGSTOPing + // ourselves for attach by the tracer. + // + // R7 has been updated with the expected PPID. + CMP $0, R0 + BEQ begin + + // The clone system call returned a non-zero value. + B done + +error: + // Exit with -errno. + NEG R0, R0 + MOVD $SYS_EXIT, R8 + SVC + HLT + +parent_dead: + MOVD $SYS_EXIT, R8 + MOVD $1, R0 + SVC + HLT + + // syscall_loop handles requests from the Sentry to execute syscalls. + // Look at syscall_thread for more details. + // + // syscall_loop is running without using the stack because it can be + // compromised by sysmsg (guest) threads that run in the same address + // space. +syscall_loop: + // while (sentryMessage->state != R13) { + // futex(sentryMessage->state, FUTEX_WAIT, 0, NULL, NULL, 0); + // } + MOVD R12, R0 + MOVD $FUTEX_WAIT, R1 + MOVD $0, R3 + MOVD $0, R4 + MOVD $0, R5 +wait_for_syscall: + // Move the sentry message state to R2. + MOVW SENTRY_MESSAGE_STATE(R12), R2 + CMPW R2, R13 + BEQ execute_syscall + + MOVD $SYS_FUTEX, R8 + SVC + JMP wait_for_syscall + +execute_syscall: + MOVD SENTRY_MESSAGE_SYSNO(R12), R8 + MOVD SENTRY_MESSAGE_ARG0(R12), R0 + MOVD SENTRY_MESSAGE_ARG1(R12), R1 + MOVD SENTRY_MESSAGE_ARG2(R12), R2 + MOVD SENTRY_MESSAGE_ARG3(R12), R3 + MOVD SENTRY_MESSAGE_ARG4(R12), R4 + MOVD SENTRY_MESSAGE_ARG5(R12), R5 + SVC + + // stubMessage->ret = ret + MOVD R0, (STUB_MESSAGE_OFFSET + STUB_MESSAGE_RET)(R12) + + // for { + // if futex(sentryMessage->state, FUTEX_WAKE, 1) == 1 { + // break; + // } + // } + MOVD $FUTEX_WAKE, R1 + MOVD $1, R2 + MOVD $0, R3 + MOVD $0, R4 + MOVD $0, R5 + MOVD $SYS_FUTEX, R8 +wake_up_sentry: + MOVD R12, R0 + SVC + + // futex returns the number of waiters that were woken up. If futex + // returns 0 here, it means that the Sentry has not called futex_wait + // yet and we need to try again. The value of sentryMessage->state + // isn't changed, so futex_wake is the only way to wake up the Sentry. + CMP $1, R0 + BNE wake_up_sentry + + ADDW $1, R13, R13 + JMP syscall_loop + +// func addrOfInitStubProcess() uintptr +TEXT ·addrOfInitStubProcess(SB), $0-8 + MOVD $·initStubProcess(SB), R0 + MOVD R0, ret+0(FP) + RET + +// stubCall calls the stub function at the given address with the given PPID. +// +// This is a distinct function because stub, above, may be mapped at any +// arbitrary location, and stub has a specific binary API (see above). +TEXT ·stubCall(SB),NOSPLIT,$0-16 + MOVD addr+0(FP), R0 + MOVD pid+8(FP), R7 + B (R0) diff --git a/pkg/sentry/platform/systrap/stub_defs.go b/pkg/sentry/platform/systrap/stub_defs.go new file mode 100644 index 000000000..d596a86ea --- /dev/null +++ b/pkg/sentry/platform/systrap/stub_defs.go @@ -0,0 +1,27 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + // Required for fact extraction. + _ "golang.org/x/sys/unix" + _ "gvisor.dev/gvisor/pkg/abi/linux" +) + +// _NEW_STUB is the value of the BX register when a new stub thread is created. +const _NEW_STUB = 1 + +// _NEW_STUB is the value of the BX register when the syscall loop is executed. +const _RUN_SYSCALL_LOOP = 5 diff --git a/pkg/sentry/platform/systrap/stub_unsafe.go b/pkg/sentry/platform/systrap/stub_unsafe.go new file mode 100644 index 000000000..922f56ebf --- /dev/null +++ b/pkg/sentry/platform/systrap/stub_unsafe.go @@ -0,0 +1,219 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "math/rand" + "reflect" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/safecopy" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +// initStubProcess is defined in arch-specific assembly. +func initStubProcess() + +// addrOfInitStubProcess returns the start address of initStubProcess. +// +// In Go 1.17+, Go references to assembly functions resolve to an ABIInternal +// wrapper function rather than the function itself. We must reference from +// assembly to get the ABI0 (i.e., primary) address. +func addrOfInitStubProcess() uintptr + +// stubCall calls the stub at the given address with the given pid. +func stubCall(addr, pid uintptr) + +// unsafeSlice returns a slice for the given address and length. +func unsafeSlice(addr uintptr, length int) (slice []byte) { + sh := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) + sh.Data = addr + sh.Len = length + sh.Cap = length + return +} + +// prepareSeccompRules compiles stub process seccomp filters and fill +// the sock_fprog structure. So the stub process will only need to call +// seccomp system call to apply these filters. +// +//go:nosplit +func prepareSeccompRules(stubSysmsgStart, stubSysmsgRules, stubSysmsgRulesLen uintptr) { + + instrs := sysmsgThreadRules(stubSysmsgStart) + progLen := len(instrs) * int(unsafe.Sizeof(linux.BPFInstruction{})) + progPtr := stubSysmsgRules + unsafe.Sizeof(linux.SockFprog{}) + + if progLen+int(unsafe.Sizeof(linux.SockFprog{})) > int(stubSysmsgRulesLen) { + panic("not enough space for sysmsg seccomp rules") + } + + var targetSlice []linux.BPFInstruction + sh := (*reflect.SliceHeader)(unsafe.Pointer(&targetSlice)) + sh.Data = progPtr + sh.Cap = len(instrs) + sh.Len = sh.Cap + + copy(targetSlice, instrs) + + // stubSysmsgRules and progPtr are addresses from a stub mapping which + // is mapped once and never moved, so it is safe to use unsafe.Pointer + // this way for them. + sockProg := (*linux.SockFprog)(unsafe.Pointer(stubSysmsgRules)) + sockProg.Len = uint16(len(instrs)) + sockProg.Filter = (*linux.BPFInstruction)(unsafe.Pointer(progPtr)) + + // Make the seccomp rules stub read-only. + if _, _, errno := unix.RawSyscall( + unix.SYS_MPROTECT, + stubSysmsgRules, + stubSysmsgRulesLen, + unix.PROT_READ); errno != 0 { + panic("mprotect failed: " + errno.Error()) + } +} + +// stubInit allocates and initializes the stub memory region which includes: +// - the stub code to do initial initialization of a stub process. +// - the sysmsg signal handler code to notify sentry about new events such as +// system calls, memory faults, etc. +// - precompiled seccomp rules to trap application system calls. +// - reserved space for stub-thread stack regions. +func stubInit() { + // *--------stubStart-------------------* + // |--------stubInitProcess-------------| + // | stub code to init stub processes | + // |--------stubSysmsgStart-------------| + // | sysmsg code | + // |--------stubSysmsgRuleStart---------| + // | precompiled sysmsg seccomp rules | + // |--------guard page------------------| + // |--------random gap------------------| + // | | + // |--------stubSysmsgStack-------------| + // | Reserved space for per-thread | + // | sysmsg stacks. | + // *------------------------------------* + + pageMask := uintptr(hostarch.PageSize - 1) + // Grab the existing stub. + procStubBegin := addrOfInitStubProcess() + procStubLen := int(safecopy.FindEndAddress(procStubBegin) - procStubBegin) + procStubSlice := unsafeSlice(procStubBegin, procStubLen) + mapLen := (uintptr(procStubLen) + pageMask) & ^pageMask + + stubSysmsgStart = mapLen + stubSysmsgLen := len(sysmsg.SighandlerBlob) + mapLen += (uintptr(stubSysmsgLen) + pageMask) & ^pageMask + + stubSysmsgRules = mapLen + stubSysmsgRulesLen = hostarch.PageSize * 4 + mapLen += stubSysmsgRulesLen + + stubROMapEnd = mapLen + // Add a guard page. + mapLen += hostarch.PageSize + stubSysmsgStack = mapLen + // Allocate maxGuestThreads plus ONE because each per-thread stack + // has to be aligned to sysmsg.PerThreadMemSize. + // Look at sysmsg/sighandler.c:sysmsg_addr() for more details. + mapLen += sysmsg.PerThreadMemSize * (maxGuestThreads + 1) + + // Randomize stubStart address. + randomOffset := uintptr(rand.Uint64() * hostarch.PageSize) + maxRandomOffset := maxRandomOffsetOfStubAddress - mapLen + stubStart = uintptr(0) + for offset := uintptr(0); offset < maxRandomOffset; offset += hostarch.PageSize { + stubStart = maxStubUserAddress + (randomOffset+offset)%maxRandomOffset + // Map the target address for the stub. + // + // We don't use FIXED here because we don't want to unmap + // something that may have been there already. We just walk + // down the address space until we find a place where the stub + // can be placed. + addr, _, _ := unix.RawSyscall6( + unix.SYS_MMAP, + stubStart, + stubROMapEnd, + unix.PROT_WRITE|unix.PROT_READ, + unix.MAP_PRIVATE|unix.MAP_ANONYMOUS, + 0 /* fd */, 0 /* offset */) + if addr == stubStart { + break + } + if addr != 0 { + // Unmap the region we've mapped accidentally. + unix.RawSyscall(unix.SYS_MUNMAP, addr, stubROMapEnd, 0) + } + stubStart = uintptr(0) + } + + if stubStart == 0 { + // This will happen only if we exhaust the entire address + // space, and it will take a long, long time. + panic("failed to map stub") + } + // Randomize stubSysmsgStack address. + gap := uintptr(rand.Uint64()) * hostarch.PageSize % (maximumUserAddress - stubStart - mapLen) + stubSysmsgStack += uintptr(gap) + + // Copy the stub to the address. + targetSlice := unsafeSlice(stubStart, procStubLen) + copy(targetSlice, procStubSlice) + stubInitProcess = stubStart + + stubSysmsgStart += stubStart + stubSysmsgStack += stubStart + stubROMapEnd += stubStart + + // Align stubSysmsgStack to the per-thread stack size. + // Look at sysmsg/sighandler.c:sysmsg_addr() for more details. + if offset := stubSysmsgStack % sysmsg.PerThreadMemSize; offset != 0 { + stubSysmsgStack += sysmsg.PerThreadMemSize - offset + } + stubSysmsgRules += stubStart + + targetSlice = unsafeSlice(stubSysmsgStart, stubSysmsgLen) + copy(targetSlice, sysmsg.SighandlerBlob) + + // Initialize stub globals + p := (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_deep_sleep_timeout))) + *p = deepSleepTimeout + p = (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_handshake_timeout))) + *p = handshakeTimeout + archState := (*sysmsg.ArchState)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_arch_state))) + archState.Init() + + prepareSeccompRules(stubSysmsgStart, stubSysmsgRules, stubSysmsgRulesLen) + + // Make the stub executable. + if _, _, errno := unix.RawSyscall( + unix.SYS_MPROTECT, + stubStart, + stubROMapEnd-stubStart, + unix.PROT_EXEC|unix.PROT_READ); errno != 0 { + panic("mprotect failed: " + errno.Error()) + } + + // Set the end. + stubEnd = stubStart + mapLen + uintptr(gap) + log.Debugf("stubStart %x stubSysmsgStart %x stubSysmsgStack %x, mapLen %x", stubStart, stubSysmsgStart, stubSysmsgStack, mapLen) + log.Debugf(archState.String()) + +} diff --git a/pkg/sentry/platform/systrap/subprocess.go b/pkg/sentry/platform/systrap/subprocess.go new file mode 100644 index 000000000..82022c61a --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess.go @@ -0,0 +1,861 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "fmt" + "os" + "runtime" + "sync" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/atomicbitops" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/pool" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sentry/pgalloc" + "gvisor.dev/gvisor/pkg/sentry/platform" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/usertrap" + "gvisor.dev/gvisor/pkg/sentry/usage" +) + +var ( + // globalPool tracks all subprocesses in various state: active or available for + // reuse. + globalPool = subprocessPool{} + + // maximumUserAddress is the largest possible user address. + maximumUserAddress = linux.TaskSize + + // stubInitAddress is the initial attempt link address for the stub. + stubInitAddress = linux.TaskSize + + // maxRandomOffsetOfStubAddress is the maximum offset for randomizing a + // stub address. It is set to the default value of mm.mmap_rnd_bits. + // + // Note: Tools like ThreadSanitizer don't like when the memory layout + // is changed significantly. + maxRandomOffsetOfStubAddress = (linux.TaskSize >> 7) & ^(uintptr(hostarch.PageSize) - 1) + + // maxStubUserAddress is the largest possible user address for + // processes running inside gVisor. It is fixed because + // * we don't want to reveal a stub address. + // * it has to be the same across checkpoint/restore. + maxStubUserAddress = maximumUserAddress - maxRandomOffsetOfStubAddress +) + +// Linux kernel errnos which "should never be seen by user programs", but will +// be revealed to ptrace syscall exit tracing. +// +// These constants are only used in subprocess.go. +const ( + ERESTARTSYS = unix.Errno(512) + ERESTARTNOINTR = unix.Errno(513) + ERESTARTNOHAND = unix.Errno(514) +) + +// thread is a traced thread; it is a thread identifier. +// +// This is a convenience type for defining ptrace operations. +type thread struct { + tgid int32 + tid int32 + + // sysmsgStackID is a stack ID in subprocess.sysmsgStackPool. + sysmsgStackID uint64 + + // initRegs are the initial registers for the first thread. + // + // These are used for the register set for system calls. + initRegs arch.Registers +} + +// requestThread is used to request a new sysmsg thread. A thread identifier will +// be sent into the thread channel. +type requestThread struct { + thread chan *thread +} + +// requestStub is used to request a new stub process. +type requestStub struct { + done chan *thread +} + +const ( + maxGuestThreads = 4096 +) + +// subprocess is a collection of threads being traced. +type subprocess struct { + platform.NoAddressSpaceIO + subprocessEntry + + // requests is used to signal creation of new threads. + requests chan any + + // numContexts counts the number of contexts currently active within the + // subprocess. A subprocess should not be fully released to be reused until + // numContexts reaches 0. + numContexts atomicbitops.Int32 + + // mu protects the following fields. + mu sync.Mutex + + // released marks this subprocess as having been released. + // A subprocess can be both released and active because we cannot allow it to + // reused until all tied contexts have been unregistered. + released bool + + // contexts is the set of contexts for which it's possible that + // context.lastFaultSP == this subprocess. + contexts map[*context]struct{} + + // sysmsgStackPool is a pool of available sysmsg stacks. + sysmsgStackPool pool.Pool + + // memoryFile is used to allocate a sysmsg stack which is shared + // between a stub process and the Sentry. + memoryFile *pgalloc.MemoryFile + + // usertrap is the state of the usertrap table which contains syscall + // trampolines. + usertrap *usertrap.State + + syscallThreadMu sync.Mutex + syscallThread *syscallThread +} + +func (s *subprocess) initSyscallThread(ptraceThread *thread) error { + s.syscallThreadMu.Lock() + defer s.syscallThreadMu.Unlock() + + id, ok := s.sysmsgStackPool.Get() + if !ok { + panic("unable to allocate a sysmsg stub thread") + } + + ptraceThread.sysmsgStackID = id + t := syscallThread{ + subproc: s, + thread: ptraceThread, + } + + if err := t.init(); err != nil { + panic(fmt.Sprintf("failed to create a syscall thread")) + } + s.syscallThread = &t + + s.syscallThread.detach() + + return nil +} + +// handlePtraceSyscallRequest executes system calls that can't be run via +// syscallThread without using ptrace. Look at the description of syscallThread +// to get more details about its limitations. +func (s *subprocess) handlePtraceSyscallRequest(req any) { + s.syscallThreadMu.Lock() + defer s.syscallThreadMu.Unlock() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + s.syscallThread.attach() + defer s.syscallThread.detach() + + ptraceThread := s.syscallThread.thread + + switch req.(type) { + case requestThread: + r := req.(requestThread) + t, err := ptraceThread.clone() + if err != nil { + // Should not happen: not recoverable. + panic(fmt.Sprintf("error initializing first thread: %v", err)) + } + + // Since the new thread was created with + // clone(CLONE_PTRACE), it will begin execution with + // SIGSTOP pending and with this thread as its tracer. + // (Hopefully nobody tgkilled it with a signal < + // SIGSTOP before the SIGSTOP was delivered, in which + // case that signal would be delivered before SIGSTOP.) + if sig := t.wait(stopped); sig != unix.SIGSTOP { + panic(fmt.Sprintf("error waiting for new clone: expected SIGSTOP, got %v", sig)) + } + + id, ok := s.sysmsgStackPool.Get() + if !ok { + panic("unable to allocate a sysmsg stub thread") + } + t.sysmsgStackID = id + + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(t.tgid), uintptr(t.tid), uintptr(unix.SIGSTOP)); e != 0 { + panic(fmt.Sprintf("tkill failed: %v", e)) + } + + // Detach the thread. + t.detach() + t.initRegs = ptraceThread.initRegs + + // Return the thread. + r.thread <- t + case requestStub: + r := req.(requestStub) + t, err := ptraceThread.createStub() + if err != nil { + panic(fmt.Sprintf("unable to create a stub process: %s", err)) + } + r.done <- t + + } +} + +// newSubprocess returns a usable subprocess. +// +// This will either be a newly created subprocess, or one from the global pool. +// The create function will be called in the latter case, which is guaranteed +// to happen with the runtime thread locked. +func newSubprocess(create func() (*thread, error), memoryFile *pgalloc.MemoryFile) (*subprocess, error) { + if sp := globalPool.fetchAvailable(); sp != nil { + return sp, nil + } + + // The following goroutine is responsible for creating the first traced + // thread, and responding to requests to make additional threads in the + // traced process. The process will be killed and reaped when the + // request channel is closed, which happens in Release below. + requests := make(chan any) + + // Ready. + sp := &subprocess{ + requests: requests, + contexts: make(map[*context]struct{}), + sysmsgStackPool: pool.Pool{Start: 0, Limit: maxGuestThreads}, + memoryFile: memoryFile, + } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Initialize the first thread. + ptraceThread, err := create() + if err != nil { + return nil, err + } + + if err := sp.initSyscallThread(ptraceThread); err != nil { + return nil, err + } + + go func() { // S/R-SAFE: Platform-related. + + // Wait for requests to create threads. + for req := range requests { + sp.handlePtraceSyscallRequest(req) + } + + // Requests should never be closed. + panic("unreachable") + }() + + sp.unmap() + sp.usertrap = usertrap.New() + + globalPool.add(sp) + return sp, nil +} + +// unmap unmaps non-stub regions of the process. +// +// This will panic on failure (which should never happen). +func (s *subprocess) unmap() { + s.Unmap(0, uint64(stubStart)) + if maximumUserAddress != stubEnd { + s.Unmap(hostarch.Addr(stubEnd), uint64(maximumUserAddress-stubEnd)) + } +} + +// Release kills the subprocess. +// +// Just kidding! We can't safely co-ordinate the detaching of all the +// tracees (since the tracers are random runtime threads, and the process +// won't exit until tracers have been notifier). +// +// Therefore we simply unmap everything in the subprocess and return it to the +// globalPool. This has the added benefit of reducing creation time for new +// subprocesses. +func (s *subprocess) Release() { + go func() { // S/R-SAFE: Platform. + s.unmap() + globalPool.release(s) + }() +} + +// newThread creates a new traced thread. +// +// Precondition: the OS thread must be locked. +func (s *subprocess) newThread() *thread { + // Ask the first thread to create a new one. + var r requestThread + r.thread = make(chan *thread) + s.requests <- r + t := <-r.thread + + // Attach the subprocess to this one. + t.attach() + + // Return the new thread, which is now bound. + return t +} + +// attach attaches to the thread. +func (t *thread) attach() { + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_ATTACH, uintptr(t.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("unable to attach: %v", errno)) + } + + // PTRACE_ATTACH sends SIGSTOP, and wakes the tracee if it was already + // stopped from the SIGSTOP queued by CLONE_PTRACE (see inner loop of + // newSubprocess), so we always expect to see signal-delivery-stop with + // SIGSTOP. + if sig := t.wait(stopped); sig != unix.SIGSTOP { + panic(fmt.Sprintf("wait failed: expected SIGSTOP, got %v", sig)) + } + + // Initialize options. + t.init() +} + +func (t *thread) grabInitRegs() { + // Grab registers. + // + // Note that we adjust the current register RIP value to be just before + // the current system call executed. This depends on the definition of + // the stub itself. + if err := t.getRegs(&t.initRegs); err != nil { + panic(fmt.Sprintf("ptrace get regs failed: %v", err)) + } + t.adjustInitRegsRip() + t.initRegs.SetStackPointer(0) +} + +// detach detaches from the thread. +// +// Because the SIGSTOP is not suppressed, the thread will enter group-stop. +func (t *thread) detach() { + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_DETACH, uintptr(t.tid), 0, uintptr(unix.SIGSTOP), 0, 0); errno != 0 { + panic(fmt.Sprintf("can't detach new clone: %v", errno)) + } +} + +// waitOutcome is used for wait below. +type waitOutcome int + +const ( + // stopped indicates that the process was stopped. + stopped waitOutcome = iota + + // killed indicates that the process was killed. + killed +) + +func (t *thread) Debugf(format string, v ...any) { + prefix := fmt.Sprintf("%8d:", t.tid) + log.DebugfAtDepth(1, prefix+format, v...) +} + +func (t *thread) dumpAndPanic(message string) { + var regs arch.Registers + message += "\n" + if err := t.getRegs(®s); err == nil { + message += dumpRegs(®s) + } else { + log.Warningf("unable to get registers: %v", err) + } + message += fmt.Sprintf("stubStart\t = %016x\n", stubStart) + panic(message) +} + +func (t *thread) dumpRegs(message string) { + var regs arch.Registers + message += "\n" + if err := t.getRegs(®s); err == nil { + message += dumpRegs(®s) + } else { + log.Warningf("unable to get registers: %v", err) + } + log.Infof("%s", message) +} + +func (t *thread) unexpectedStubExit() { + msg, err := t.getEventMessage() + status := unix.WaitStatus(msg) + if status.Signaled() && status.Signal() == unix.SIGKILL { + // SIGKILL can be only sent by a user or OOM-killer. In both + // these cases, we don't need to panic. There is no reasons to + // think that something wrong in gVisor. + log.Warningf("The ptrace stub process %v has been killed by SIGKILL.", t.tgid) + pid := os.Getpid() + unix.Tgkill(pid, pid, unix.Signal(unix.SIGKILL)) + } + t.dumpAndPanic(fmt.Sprintf("wait failed: the process %d:%d exited: %x (err %v)", t.tgid, t.tid, msg, err)) +} + +// wait waits for a stop event. +// +// Precondition: outcome is a valid waitOutcome. +func (t *thread) wait(outcome waitOutcome) unix.Signal { + var status unix.WaitStatus + + for { + r, err := unix.Wait4(int(t.tid), &status, unix.WALL|unix.WUNTRACED, nil) + if err == unix.EINTR || err == unix.EAGAIN { + // Wait was interrupted; wait again. + continue + } else if err != nil { + panic(fmt.Sprintf("ptrace wait failed: %v", err)) + } + if int(r) != int(t.tid) { + panic(fmt.Sprintf("ptrace wait returned %v, expected %v", r, t.tid)) + } + switch outcome { + case stopped: + if !status.Stopped() { + t.dumpAndPanic(fmt.Sprintf("ptrace status unexpected: got %v, wanted stopped", status)) + } + stopSig := status.StopSignal() + if stopSig == 0 { + continue // Spurious stop. + } + if stopSig == unix.SIGTRAP { + if status.TrapCause() == unix.PTRACE_EVENT_EXIT { + t.unexpectedStubExit() + } + // Re-encode the trap cause the way it's expected. + return stopSig | unix.Signal(status.TrapCause()<<8) + } + // Not a trap signal. + return stopSig + case killed: + if !status.Exited() && !status.Signaled() { + t.dumpAndPanic(fmt.Sprintf("ptrace status unexpected: got %v, wanted exited", status)) + } + return unix.Signal(status.ExitStatus()) + default: + // Should not happen. + t.dumpAndPanic(fmt.Sprintf("unknown outcome: %v", outcome)) + } + } +} + +// destroy kills the thread. +// +// Note that this should not be used in the general case; the death of threads +// will typically cause the death of the parent. This is a utility method for +// manually created threads. +func (t *thread) destroy() { + t.detach() + unix.Tgkill(int(t.tgid), int(t.tid), unix.Signal(unix.SIGKILL)) + t.wait(killed) +} + +// init initializes trace options. +func (t *thread) init() { + // Set the TRACESYSGOOD option to differentiate real SIGTRAP. + // set PTRACE_O_EXITKILL to ensure that the unexpected exit of the + // sentry will immediately kill the associated stubs. + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_SETOPTIONS, + uintptr(t.tid), + 0, + unix.PTRACE_O_TRACESYSGOOD|unix.PTRACE_O_TRACEEXIT|unix.PTRACE_O_EXITKILL, + 0, 0) + if errno != 0 { + panic(fmt.Sprintf("ptrace set options failed: %v", errno)) + } +} + +// syscall executes a system call cycle in the traced context. +// +// This is _not_ for use by application system calls, rather it is for use when +// a system call must be injected into the remote context (e.g. mmap, munmap). +// Note that clones are handled separately. +func (t *thread) syscall(regs *arch.Registers) (uintptr, error) { + // Set registers. + if err := t.setRegs(regs); err != nil { + panic(fmt.Sprintf("ptrace set regs failed: %v", err)) + } + + for { + // Execute the syscall instruction. The task has to stop on the + // trap instruction which is right after the syscall + // instruction. + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_CONT, uintptr(t.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("ptrace syscall-enter failed: %v", errno)) + } + + sig := t.wait(stopped) + if sig == unix.SIGTRAP { + // Reached syscall-enter-stop. + break + } else { + // Some other signal caused a thread stop; ignore. + if sig != unix.SIGSTOP && sig != unix.SIGCHLD { + log.Warningf("The thread %d:%d has been interrupted by %d", t.tgid, t.tid, sig) + } + continue + } + } + + // Grab registers. + if err := t.getRegs(regs); err != nil { + panic(fmt.Sprintf("ptrace get regs failed: %v", err)) + } + return syscallReturnValue(regs) +} + +// syscallIgnoreInterrupt ignores interrupts on the system call thread and +// restarts the syscall if the kernel indicates that should happen. +func (t *thread) syscallIgnoreInterrupt( + initRegs *arch.Registers, + sysno uintptr, + args ...arch.SyscallArgument) (uintptr, error) { + for { + regs := createSyscallRegs(initRegs, sysno, args...) + rval, err := t.syscall(®s) + switch err { + case ERESTARTSYS: + continue + case ERESTARTNOINTR: + continue + case ERESTARTNOHAND: + continue + default: + return rval, err + } + } +} + +// NotifyInterrupt implements interrupt.Receiver.NotifyInterrupt. +func (t *thread) NotifyInterrupt() { + unix.Tgkill(int(t.tgid), int(t.tid), unix.Signal(platform.SignalInterrupt)) +} + +// switchToApp is called from the main SwitchToApp entrypoint. +// +// This function returns true on a system call, false on a signal. +// The second return value is true if a syscall instruction can be replaced on +// a function call. +func (s *subprocess) switchToApp(c *context, ac *arch.Context64) (isSyscall bool, shouldPatchSyscall bool, err error) { + // Reset necessary registers. + regs := &ac.StateData().Regs + sysThread, err := s.getSysmsgThread(regs, c, ac) + if err != nil { + return false, false, err + } + msg := sysThread.msg + t := sysThread.thread + t.resetSysemuRegs(regs) + + s.restoreFPState(msg, sysThread.fpuStateToMsgOffset, c, ac) + + // Check for interrupts, and ensure that future interrupts will signal t. + if !c.interrupt.Enable(sysThread) { + // Pending interrupt; simulate. + c.signalInfo = linux.SignalInfo{Signo: int32(platform.SignalInterrupt)} + return false, false, nil + } + defer c.interrupt.Disable() + + restoreArchSpecificState(regs, t, sysThread, msg, ac) + msg.Regs = regs.PtraceRegs + msg.EnableSentryFastPath() + sysThread.waitEvent(sysmsg.StateDone) + + if msg.Type != sysmsg.EventTypeSyscallTrap { + var err error + sysThread.fpuStateToMsgOffset, err = msg.FPUStateOffset() + if err != nil { + return false, false, err + } + } else { + sysThread.fpuStateToMsgOffset = 0 + } + + if msg.Err != 0 { + panic(fmt.Sprintf("stub thread %d failed: err %d line %d: %s", t.tid, msg.Err, msg.Line, msg)) + } + + regs.PtraceRegs = msg.Regs + retrieveArchSpecificState(regs, msg, t, ac) + + // We have a signal. We verify however, that the signal was + // either delivered from the kernel or from this process. We + // don't respect other signals. + c.signalInfo = msg.SignalInfo + if msg.Type == sysmsg.EventTypeSyscallCanBePatched { + msg.Type = sysmsg.EventTypeSyscall + shouldPatchSyscall = true + } + + if msg.Type == sysmsg.EventTypeSyscall || msg.Type == sysmsg.EventTypeSyscallTrap { + if maybePatchSignalInfo(regs, &c.signalInfo) { + return false, false, nil + } + updateSyscallRegs(regs) + return true, shouldPatchSyscall, nil + } else if msg.Type != sysmsg.EventTypeFault { + panic(fmt.Sprintf("unknown message type: %v", msg.Type)) + } + + return false, false, nil +} + +// syscall executes the given system call without handling interruptions. +func (s *subprocess) syscall(sysno uintptr, args ...arch.SyscallArgument) (uintptr, error) { + s.syscallThreadMu.Lock() + defer s.syscallThreadMu.Unlock() + + return s.syscallThread.syscall(sysno, args...) +} + +// MapFile implements platform.AddressSpace.MapFile. +func (s *subprocess) MapFile(addr hostarch.Addr, f memmap.File, fr memmap.FileRange, at hostarch.AccessType, precommit bool) error { + var flags int + if precommit { + flags |= unix.MAP_POPULATE + } + _, err := s.syscall( + unix.SYS_MMAP, + arch.SyscallArgument{Value: uintptr(addr)}, + arch.SyscallArgument{Value: uintptr(fr.Length())}, + arch.SyscallArgument{Value: uintptr(at.Prot())}, + arch.SyscallArgument{Value: uintptr(flags | unix.MAP_SHARED | unix.MAP_FIXED)}, + arch.SyscallArgument{Value: uintptr(f.FD())}, + arch.SyscallArgument{Value: uintptr(fr.Start)}) + return err +} + +// Unmap implements platform.AddressSpace.Unmap. +func (s *subprocess) Unmap(addr hostarch.Addr, length uint64) { + ar, ok := addr.ToRange(length) + if !ok { + panic(fmt.Sprintf("addr %#x + length %#x overflows", addr, length)) + } + s.mu.Lock() + for c := range s.contexts { + c.mu.Lock() + if c.lastFaultSP == s && ar.Contains(c.lastFaultAddr) { + // Forget the last fault so that if c faults again, the fault isn't + // incorrectly reported as a write fault. If this is being called + // due to munmap() of the corresponding vma, handling of the second + // fault will fail anyway. + c.lastFaultSP = nil + delete(s.contexts, c) + } + c.mu.Unlock() + } + s.mu.Unlock() + _, err := s.syscall( + unix.SYS_MUNMAP, + arch.SyscallArgument{Value: uintptr(addr)}, + arch.SyscallArgument{Value: uintptr(length)}) + if err != nil { + // We never expect this to happen. + panic(fmt.Sprintf("munmap(%x, %x)) failed: %v", addr, length, err)) + } +} + +// getSysmsgThread returns a sysmsg thread for the specified context. +func (s *subprocess) getSysmsgThread(tregs *arch.Registers, c *context, ac *arch.Context64) (*sysmsgThread, error) { + sysThread := c.sysmsgThread + if sysThread != nil && sysThread.subproc != s { + // This can happen if a new address space + // has been created (e.g. fork). + sysThread.destroy() + sysThread = nil + } + if sysThread != nil { + return sysThread, nil + } + + // Create a new seccomp process. + var r requestThread + r.thread = make(chan *thread) + s.requests <- r + p := <-r.thread + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + p.attach() + + // Skip SIGSTOP. + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_CONT, uintptr(p.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("ptrace cont failed: %v", errno)) + } + sig := p.wait(stopped) + if sig != unix.SIGSTOP { + panic(fmt.Sprintf("error waiting for new clone: expected SIGSTOP, got %v", sig)) + } + + // Allocate a new stack for the BPF process. + opts := pgalloc.AllocOpts{ + Kind: usage.System, + Dir: pgalloc.TopDown, + } + fr, err := s.memoryFile.Allocate(uint64(sysmsg.PerThreadSharedStackSize), opts) + if err != nil { + // TODO(b/144063246): Need to fail the clone system call. + panic(fmt.Sprintf("failed to allocate a new stack: %v", err)) + } + sysThread = &sysmsgThread{ + thread: p, + subproc: s, + stackRange: fr, + } + + // Map the stack into the sentry. + sentryStackAddr, _, errno := unix.RawSyscall6( + unix.SYS_MMAP, + 0, + sysmsg.PerThreadSharedStackSize, + unix.PROT_WRITE|unix.PROT_READ, + unix.MAP_SHARED|unix.MAP_FILE, + uintptr(s.memoryFile.FD()), uintptr(fr.Start)) + if errno != 0 { + panic(fmt.Sprintf("mmap failed: %v", errno)) + } + + // Before installing the stub syscall filters, we need to call a few + // system calls (e.g. sigaltstack, sigaction) which have in-memory + // arguments. We need to prevent changing these parameters by other + // stub threads, so lets map the future BPF stack as read-only and + // fill syscall arguments from the Sentry. + sysmsgStackAddr := sysThread.sysmsgPerThreadMemAddr() + sysmsg.PerThreadSharedStackOffset + err = sysThread.mapStack(sysmsgStackAddr, true) + if err != nil { + panic(fmt.Sprintf("mmap failed: %v", err)) + } + + sysThread.init(sentryStackAddr, sysmsgStackAddr) + + // Map the stack into the BPF process. + err = sysThread.mapStack(sysmsgStackAddr, false) + if err != nil { + s.memoryFile.DecRef(fr) + panic(fmt.Sprintf("mmap failed: %v", err)) + } + + // Map the stack into the BPF process. + privateStackAddr := sysThread.sysmsgPerThreadMemAddr() + sysmsg.PerThreadPrivateStackOffset + err = sysThread.mapPrivateStack(privateStackAddr, sysmsg.PerThreadPrivateStackSize) + if err != nil { + s.memoryFile.DecRef(fr) + panic(fmt.Sprintf("mmap failed: %v", err)) + } + + sysThread.setMsg(sysmsg.StackAddrToMsg(sentryStackAddr)) + sysThread.msg.Init() + sysThread.msg.Self = uint64(sysmsgStackAddr + sysmsg.MsgOffsetFromSharedStack) + sysThread.msg.Syshandler = uint64(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_syshandler)) + sysThread.msg.SyshandlerStack = uint64(sysmsg.StackAddrToSyshandlerStack(sysThread.sysmsgPerThreadMemAddr())) + + sysThread.msg.State.Set(sysmsg.StateDone) + + // Install a pre-compiled seccomp rules for the BPF process. + _, err = p.syscallIgnoreInterrupt(&p.initRegs, unix.SYS_PRCTL, + arch.SyscallArgument{Value: uintptr(linux.PR_SET_NO_NEW_PRIVS)}, + arch.SyscallArgument{Value: uintptr(1)}, + arch.SyscallArgument{Value: uintptr(0)}) + if err != nil { + panic(fmt.Sprintf("prctl(PR_SET_NO_NEW_PRIVS) failed: %v", err)) + } + + _, err = p.syscallIgnoreInterrupt(&p.initRegs, seccomp.SYS_SECCOMP, + arch.SyscallArgument{Value: uintptr(linux.SECCOMP_SET_MODE_FILTER)}, + arch.SyscallArgument{Value: uintptr(0)}, + arch.SyscallArgument{Value: stubSysmsgRules}) + if err != nil { + panic(fmt.Sprintf("seccomp failed: %v", err)) + } + + // Prepare to start the BPF process. + p.resetSysemuRegs(tregs) + archSpecificSysThreadInit(sysThread, tregs) + if err := p.setRegs(tregs); err != nil { + panic(fmt.Sprintf("ptrace set regs failed: %v", err)) + } + // Send a fake event to stop the BPF process. + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(p.tgid), uintptr(p.tid), uintptr(unix.SIGSEGV)); e != 0 { + panic(fmt.Sprintf("tkill failed: %v", e)) + } + // Skip SIGSTOP. + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(p.tgid), uintptr(p.tid), uintptr(unix.SIGCONT)); e != 0 { + panic(fmt.Sprintf("tkill failed: %v", e)) + } + // Resume the BPF process. + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_DETACH, uintptr(p.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("can't detach new clone: %v", errno)) + } + + sysThread.waitEvent(sysmsg.StateNone) + if msg := sysThread.msg; msg.Err != 0 { + panic(fmt.Sprintf("stub thread failed: %v (line %v)", msg.Err, msg.Line)) + } + + sysThread.fpuStateToMsgOffset, err = sysThread.msg.FPUStateOffset() + if err != nil { + sysThread.destroy() + return nil, err + } + + c.sysmsgThread = sysThread + + return sysThread, nil +} + +// PreFork implements platform.AddressSpace.PreFork. +// We need to take the usertrap lock to be sure that fork() will not be in the +// middle of applying a binary patch. +func (s *subprocess) PreFork() { + s.usertrap.PreFork() +} + +// PostFork implements platform.AddressSpace.PostFork. +func (s *subprocess) PostFork() { + s.usertrap.PostFork() // +checklocksforce: PreFork acquires, above. +} + +// unregisterContext releases all references held for this context. +// +// Precondition: context c must have been active within subprocess s. +func (s *subprocess) unregisterContext(c *context) { + if s == nil { + return + } + s.mu.Lock() + delete(s.contexts, c) + s.numContexts.Add(-1) + released := s.released + s.mu.Unlock() + + if released && s.numContexts.Load() == 0 { + globalPool.release(s) + } +} diff --git a/pkg/sentry/platform/systrap/subprocess_amd64.go b/pkg/sentry/platform/systrap/subprocess_amd64.go new file mode 100644 index 000000000..e8a8bab82 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_amd64.go @@ -0,0 +1,315 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package systrap + +import ( + "fmt" + "runtime" + "strings" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +const ( + // initRegsRipAdjustment is the size of the syscall instruction. + initRegsRipAdjustment = 2 +) + +// resetSysemuRegs sets up emulation registers. +// +// This should be called prior to calling sysemu. +func (t *thread) resetSysemuRegs(regs *arch.Registers) { + regs.Cs = t.initRegs.Cs + regs.Ss = t.initRegs.Ss + regs.Ds = t.initRegs.Ds + regs.Es = t.initRegs.Es + regs.Fs = t.initRegs.Fs + regs.Gs = t.initRegs.Gs +} + +// createSyscallRegs sets up syscall registers. +// +// This should be called to generate registers for a system call. +func createSyscallRegs(initRegs *arch.Registers, sysno uintptr, args ...arch.SyscallArgument) arch.Registers { + // Copy initial registers. + regs := *initRegs + + // Set our syscall number. + regs.Rax = uint64(sysno) + if len(args) >= 1 { + regs.Rdi = args[0].Uint64() + } + if len(args) >= 2 { + regs.Rsi = args[1].Uint64() + } + if len(args) >= 3 { + regs.Rdx = args[2].Uint64() + } + if len(args) >= 4 { + regs.R10 = args[3].Uint64() + } + if len(args) >= 5 { + regs.R8 = args[4].Uint64() + } + if len(args) >= 6 { + regs.R9 = args[5].Uint64() + } + + return regs +} + +// isSingleStepping determines if the registers indicate single-stepping. +func isSingleStepping(regs *arch.Registers) bool { + return (regs.Eflags & arch.X86TrapFlag) != 0 +} + +// updateSyscallRegs updates registers after finishing sysemu. +func updateSyscallRegs(regs *arch.Registers) { + // Ptrace puts -ENOSYS in rax on syscall-enter-stop. + regs.Rax = regs.Orig_rax +} + +// syscallReturnValue extracts a sensible return from registers. +func syscallReturnValue(regs *arch.Registers) (uintptr, error) { + rval := int64(regs.Rax) + if rval < 0 { + return 0, unix.Errno(-rval) + } + return uintptr(rval), nil +} + +func dumpRegs(regs *arch.Registers) string { + var m strings.Builder + + fmt.Fprintf(&m, "Registers:\n") + fmt.Fprintf(&m, "\tR15\t = %016x\n", regs.R15) + fmt.Fprintf(&m, "\tR14\t = %016x\n", regs.R14) + fmt.Fprintf(&m, "\tR13\t = %016x\n", regs.R13) + fmt.Fprintf(&m, "\tR12\t = %016x\n", regs.R12) + fmt.Fprintf(&m, "\tRbp\t = %016x\n", regs.Rbp) + fmt.Fprintf(&m, "\tRbx\t = %016x\n", regs.Rbx) + fmt.Fprintf(&m, "\tR11\t = %016x\n", regs.R11) + fmt.Fprintf(&m, "\tR10\t = %016x\n", regs.R10) + fmt.Fprintf(&m, "\tR9\t = %016x\n", regs.R9) + fmt.Fprintf(&m, "\tR8\t = %016x\n", regs.R8) + fmt.Fprintf(&m, "\tRax\t = %016x\n", regs.Rax) + fmt.Fprintf(&m, "\tRcx\t = %016x\n", regs.Rcx) + fmt.Fprintf(&m, "\tRdx\t = %016x\n", regs.Rdx) + fmt.Fprintf(&m, "\tRsi\t = %016x\n", regs.Rsi) + fmt.Fprintf(&m, "\tRdi\t = %016x\n", regs.Rdi) + fmt.Fprintf(&m, "\tOrig_rax = %016x\n", regs.Orig_rax) + fmt.Fprintf(&m, "\tRip\t = %016x\n", regs.Rip) + fmt.Fprintf(&m, "\tCs\t = %016x\n", regs.Cs) + fmt.Fprintf(&m, "\tEflags\t = %016x\n", regs.Eflags) + fmt.Fprintf(&m, "\tRsp\t = %016x\n", regs.Rsp) + fmt.Fprintf(&m, "\tSs\t = %016x\n", regs.Ss) + fmt.Fprintf(&m, "\tFs_base\t = %016x\n", regs.Fs_base) + fmt.Fprintf(&m, "\tGs_base\t = %016x\n", regs.Gs_base) + fmt.Fprintf(&m, "\tDs\t = %016x\n", regs.Ds) + fmt.Fprintf(&m, "\tEs\t = %016x\n", regs.Es) + fmt.Fprintf(&m, "\tFs\t = %016x\n", regs.Fs) + fmt.Fprintf(&m, "\tGs\t = %016x\n", regs.Gs) + + return m.String() +} + +// adjustInitregsRip adjust the current register RIP value to +// be just before the system call instruction excution +func (t *thread) adjustInitRegsRip() { + t.initRegs.Rip -= initRegsRipAdjustment +} + +// Pass the expected PPID to the child via R15 when creating stub process. +func initChildProcessPPID(initregs *arch.Registers, ppid int32) { + initregs.R15 = uint64(ppid) + // Rbx has to be set to 1 when creating stub process. + initregs.Rbx = _NEW_STUB +} + +// patchSignalInfo patches the signal info to account for hitting the seccomp +// filters from vsyscall emulation, specified below. We allow for SIGSYS as a +// synchronous trap, but patch the structure to appear like a SIGSEGV with the +// Rip as the faulting address. +// +// Note that this should only be called after verifying that the signalInfo has +// been generated by the kernel. +// Returns true if the signal info was patched, false otherwise. +func maybePatchSignalInfo(regs *arch.Registers, signalInfo *linux.SignalInfo) bool { + if signalInfo.Addr() < linux.VSyscallStartAddr || + signalInfo.Addr() >= linux.VSyscallEndAddr { + return false + } + // The syscall event was triggered from vsyscall emulation. + signalInfo.Signo = int32(linux.SIGSEGV) + + // Unwind the kernel emulation, if any has occurred. A SIGSYS is delivered + // with the si_call_addr field pointing to the current RIP. This field + // aligns with the si_addr field for a SIGSEGV, so we don't need to touch + // anything there. We do need to unwind emulation however, so we set the + // instruction pointer to the faulting value, and "unpop" the stack. + regs.Rip = signalInfo.Addr() + regs.Rsp -= 8 + return true +} + +// enableCpuidFault enables cpuid-faulting. +// +// This may fail on older kernels or hardware, so we just disregard the result. +// Host CPUID will be enabled. +// +// This is safe to call in an afterFork context. +// +//go:nosplit +//go:norace +func enableCpuidFault() { + unix.RawSyscall6(unix.SYS_ARCH_PRCTL, linux.ARCH_SET_CPUID, 0, 0, 0, 0, 0) +} + +// appendArchSeccompRules append architecture specific seccomp rules when creating BPF program. +// Ref attachedThread() for more detail. +func appendArchSeccompRules(rules []seccomp.RuleSet) []seccomp.RuleSet { + return append(rules, []seccomp.RuleSet{ + // Rules for trapping vsyscall access. + { + Rules: seccomp.SyscallRules{ + unix.SYS_GETTIMEOFDAY: {}, + unix.SYS_TIME: {}, + unix.SYS_GETCPU: {}, // SYS_GETCPU was not defined in package syscall on amd64. + }, + Action: linux.SECCOMP_RET_TRAP, + Vsyscall: true, + }, + { + Rules: seccomp.SyscallRules{ + unix.SYS_ARCH_PRCTL: []seccomp.Rule{ + {seccomp.EqualTo(linux.ARCH_SET_CPUID), seccomp.EqualTo(0)}, + {seccomp.EqualTo(linux.ARCH_SET_FS)}, + {seccomp.EqualTo(linux.ARCH_GET_FS)}, + }, + }, + Action: linux.SECCOMP_RET_ALLOW, + }, + }...) +} + +func (s *subprocess) PullFullState(c *context, ac *arch.Context64) error { + // Reset necessary registers. + regs := &ac.StateData().Regs + + sysThread, err := s.getSysmsgThread(regs, c, ac) + if err != nil { + return err + } + msg := sysThread.msg + + // In case of EventTypeSyscallTrap, we have only syscall argument + // registers and we need to trigger a signal in the stub process to get + // a full set of registers and an FPU state. + // + // In other cases, we have the full set of registers and need only copy + // the FPU state from a signal frame. + if msg.Type != sysmsg.EventTypeSyscallTrap { + s.saveFPState(msg, sysThread.fpuStateToMsgOffset, c, ac) + return nil + } + + // In case of EventTypeSyscallTrap, the Sentry knows only the syscall + // number and syscall arguments and the target thread is stopped in the + // syshandler stub function. We need to ask syshandler to trigger a + // real syscall to get the full state. + msg.Regs = regs.PtraceRegs + + sysThread.waitEvent(sysmsg.StateSigact) + + if msg.Err != 0 { + panic(fmt.Sprintf("stub thread failed: err %d line %d: %s", msg.Err, msg.Line, msg)) + } + + if msg.Type != sysmsg.EventTypeSyscall { + panic(fmt.Sprintf("unknown message type: type %v: %s", msg.Type, msg)) + } + + sysThread.fpuStateToMsgOffset, err = msg.FPUStateOffset() + if err != nil { + return err + } + + // When we are triggering the real syscall instruction, we don't + // restore all syscall arguments and even the syscall number. + msg.Regs.Rax = regs.Rax + msg.Regs.Orig_rax = regs.Orig_rax + msg.Regs.Rdi = regs.Rdi + msg.Regs.Rsi = regs.Rsi + msg.Regs.Rdx = regs.Rdx + msg.Regs.R10 = regs.R10 + msg.Regs.R8 = regs.R8 + msg.Regs.R9 = regs.R9 + regs.PtraceRegs = msg.Regs + + // The thread has restored all registers that could be changed in + // the syshandler stub function, but it is still in this function. We + // know the return address and let's set it so to be not affected if + // the stub code will be changed after save/restore. + regs.Rip = msg.RetAddr + + s.saveFPState(msg, sysThread.fpuStateToMsgOffset, c, ac) + + c.signalInfo = msg.SignalInfo + + return nil +} + +func restoreArchSpecificState(regs *arch.Registers, t *thread, sysThread *sysmsgThread, msg *sysmsg.Msg, _ *arch.Context64) { + regs.Gs_base = msg.Self + + // Switching gs_base is a rare operation, therefore checking that we need to do + // so is better done in the sentry, because doing so on a host that doesn't + // have FSGSBASE instructions enabled is quite expensive since it would require + // an ARCH_PRCTL syscall. + if regs.Gs_base != sysThread.gsBase { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + t.attach() + + var r arch.Registers + if err := t.getRegs(&r); err != nil { + panic(fmt.Sprintf("ptrace get regs failed: %v", err)) + } + r.Gs_base = regs.Gs_base + if err := t.setRegs(&r); err != nil { + panic(fmt.Sprintf("ptrace set regs failed: %v", err)) + } + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, unix.PTRACE_DETACH, uintptr(t.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("ptrace detach failed: %v", errno)) + } + sysThread.gsBase = regs.Gs_base + } +} + +func archSpecificSysThreadInit(sysThread *sysmsgThread, regs *arch.Registers) { + regs.Gs_base = sysThread.msg.Self + sysThread.gsBase = regs.Gs_base +} + +func retrieveArchSpecificState(regs *arch.Registers, msg *sysmsg.Msg, _ *thread, ac *arch.Context64) { +} diff --git a/pkg/sentry/platform/systrap/subprocess_amd64_unsafe.go b/pkg/sentry/platform/systrap/subprocess_amd64_unsafe.go new file mode 100644 index 000000000..3e81a406e --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_amd64_unsafe.go @@ -0,0 +1,45 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package systrap + +import ( + "unsafe" + + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +func (s *subprocess) restoreFPState(msg *sysmsg.Msg, fpuToMsgOffset uint64, c *context, ac *arch.Context64) { + // c.needRestoreFPState is changed only from the task goroutine, so it can + // be accessed without locks. + if !c.needRestoreFPState { + return + } + c.needRestoreFPState = false + fpState := ac.FloatingPointData().BytePointer() + src := unsafeSlice(uintptr(unsafe.Pointer(fpState)), c.fpLen) + dst := unsafeSlice(uintptr(unsafe.Pointer(msg))+uintptr(fpuToMsgOffset), c.fpLen) + copy(dst, src) +} + +func (s *subprocess) saveFPState(msg *sysmsg.Msg, fpuToMsgOffset uint64, c *context, ac *arch.Context64) { + fpState := ac.FloatingPointData().BytePointer() + src := unsafeSlice(uintptr(unsafe.Pointer(msg))+uintptr(fpuToMsgOffset), c.fpLen) + dst := unsafeSlice(uintptr(unsafe.Pointer(fpState)), c.fpLen) + copy(dst, src) +} diff --git a/pkg/sentry/platform/systrap/subprocess_arm64.go b/pkg/sentry/platform/systrap/subprocess_arm64.go new file mode 100644 index 000000000..6c8de888e --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_arm64.go @@ -0,0 +1,213 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package systrap + +import ( + "fmt" + "strings" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +const ( + // initRegsRipAdjustment is the size of the svc instruction. + initRegsRipAdjustment = 4 +) + +// resetSysemuRegs sets up emulation registers. +// +// This should be called prior to calling sysemu. +func (t *thread) resetSysemuRegs(regs *arch.Registers) { +} + +// createSyscallRegs sets up syscall registers. +// +// This should be called to generate registers for a system call. +func createSyscallRegs(initRegs *arch.Registers, sysno uintptr, args ...arch.SyscallArgument) arch.Registers { + // Copy initial registers (Pc, Sp, etc.). + regs := *initRegs + + // Set our syscall number. + // r8 for the syscall number. + // r0-r6 is used to store the parameters. + regs.Regs[8] = uint64(sysno) + if len(args) >= 1 { + regs.Regs[0] = args[0].Uint64() + } + if len(args) >= 2 { + regs.Regs[1] = args[1].Uint64() + } + if len(args) >= 3 { + regs.Regs[2] = args[2].Uint64() + } + if len(args) >= 4 { + regs.Regs[3] = args[3].Uint64() + } + if len(args) >= 5 { + regs.Regs[4] = args[4].Uint64() + } + if len(args) >= 6 { + regs.Regs[5] = args[5].Uint64() + } + + return regs +} + +// isSingleStepping determines if the registers indicate single-stepping. +func isSingleStepping(regs *arch.Registers) bool { + // Refer to the ARM SDM D2.12.3: software step state machine + // return (regs.Pstate.SS == 1) && (MDSCR_EL1.SS == 1). + // + // Since the host Linux kernel will set MDSCR_EL1.SS on our behalf + // when we call a single-step ptrace command, we only need to check + // the Pstate.SS bit here. + return (regs.Pstate & arch.ARMTrapFlag) != 0 +} + +// updateSyscallRegs updates registers after finishing sysemu. +func updateSyscallRegs(regs *arch.Registers) { + // No special work is necessary. + return +} + +// syscallReturnValue extracts a sensible return from registers. +func syscallReturnValue(regs *arch.Registers) (uintptr, error) { + rval := int64(regs.Regs[0]) + if rval < 0 { + return 0, unix.Errno(-rval) + } + return uintptr(rval), nil +} + +func dumpRegs(regs *arch.Registers) string { + var m strings.Builder + + fmt.Fprintf(&m, "Registers:\n") + + for i := 0; i < 31; i++ { + fmt.Fprintf(&m, "\tRegs[%d]\t = %016x\n", i, regs.Regs[i]) + } + fmt.Fprintf(&m, "\tSp\t = %016x\n", regs.Sp) + fmt.Fprintf(&m, "\tPc\t = %016x\n", regs.Pc) + fmt.Fprintf(&m, "\tPstate\t = %016x\n", regs.Pstate) + + return m.String() +} + +// adjustInitregsRip adjust the current register RIP value to +// be just before the system call instruction excution +func (t *thread) adjustInitRegsRip() { + t.initRegs.Pc -= initRegsRipAdjustment +} + +// Pass the expected PPID to the child via X7 when creating stub process +func initChildProcessPPID(initregs *arch.Registers, ppid int32) { + initregs.Regs[7] = uint64(ppid) + // R9 has to be set to 1 when creating stub process. + initregs.Regs[9] = _NEW_STUB +} + +func maybePatchSignalInfo(regs *arch.Registers, signalInfo *linux.SignalInfo) (patched bool) { + // vsyscall emulation is not supported on ARM64. No need to patch anything. + return false +} + +// Noop on arm64. +// +//go:nosplit +func enableCpuidFault() { +} + +// appendArchSeccompRules append architecture specific seccomp rules when creating BPF program. +// Ref attachedThread() for more detail. +func appendArchSeccompRules(rules []seccomp.RuleSet) []seccomp.RuleSet { + return rules +} + +// probeSeccomp returns true if seccomp is run after ptrace notifications, +// which is generally the case for kernel version >= 4.8. +// +// On arm64, the support of PTRACE_SYSEMU was added in the 5.3 kernel, so +// probeSeccomp can always return true. +func probeSeccomp() bool { + return true +} + +func (s *subprocess) arm64SyscallWorkaround(t *thread, regs *arch.Registers) { + // On ARM64, when ptrace stops on a system call, it uses the x7 + // register to indicate whether the stop has been signalled from + // syscall entry or syscall exit. This means that we can't get a value + // of this register and we can't change it. More details are in the + // comment for tracehook_report_syscall in arch/arm64/kernel/ptrace.c. + // + // This happens only if we stop on a system call, so let's queue a + // signal, resume a stub thread and catch it on a signal handling. + t.NotifyInterrupt() + for { + if _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_SYSEMU, + uintptr(t.tid), 0, 0, 0, 0); errno != 0 { + panic(fmt.Sprintf("ptrace sysemu failed: %v", errno)) + } + + // Wait for the syscall-enter stop. + sig := t.wait(stopped) + if sig == unix.SIGSTOP { + // SIGSTOP was delivered to another thread in the same thread + // group, which initiated another group stop. Just ignore it. + continue + } + if sig == (syscallEvent | unix.SIGTRAP) { + t.dumpAndPanic(fmt.Sprintf("unexpected syscall event")) + } + break + } + if err := t.getRegs(regs); err != nil { + panic(fmt.Sprintf("ptrace get regs failed: %v", err)) + } +} + +func (s *subprocess) PullFullState(c *context, ac *arch.Context64) error { + // We do not support syscall trap in ARM64 so just get the fp state from the + // signal frame and we are done. + regs := &ac.StateData().Regs + sysThread, err := s.getSysmsgThread(regs, c, ac) + if err != nil { + return err + } + s.saveFPState(sysThread.msg, sysThread.fpuStateToMsgOffset, c, ac) + return nil +} + +func restoreArchSpecificState(regs *arch.Registers, t *thread, _ *sysmsgThread, msg *sysmsg.Msg, ac *arch.Context64) { + msg.TLS = uint64(ac.TLS()) +} + +func archSpecificSysThreadInit(sysThread *sysmsgThread, regs *arch.Registers) { +} + +func retrieveArchSpecificState(regs *arch.Registers, msg *sysmsg.Msg, t *thread, ac *arch.Context64) { + if !ac.SetTLS(uintptr(msg.TLS)) { + panic(fmt.Sprintf("ac.SetTLS(%+v) failed", msg.TLS)) + } +} diff --git a/pkg/sentry/platform/systrap/subprocess_arm64_unsafe.go b/pkg/sentry/platform/systrap/subprocess_arm64_unsafe.go new file mode 100644 index 000000000..a102a2416 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_arm64_unsafe.go @@ -0,0 +1,51 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package systrap + +import ( + "unsafe" + + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +// Signal frames for ARM64 include 8 byte magic header before the floating point +// context. +// +// See: arch/arm64/include/uapi/asm/sigcontext.h +const sigFrameMagicHeaderLen = 8 + +func (s *subprocess) restoreFPState(msg *sysmsg.Msg, fpuToMsgOffset uint64, c *context, ac *arch.Context64) { + // c.needRestoreFPState is changed only from the task goroutine, so it can + // be accessed without locks. + if !c.needRestoreFPState { + return + } + c.needRestoreFPState = false + fpState := ac.FloatingPointData().BytePointer() + src := unsafeSlice(uintptr(unsafe.Pointer(fpState)), c.fpLen) + dst := unsafeSlice(uintptr(unsafe.Pointer(msg))+uintptr(fpuToMsgOffset)+uintptr(sigFrameMagicHeaderLen), c.fpLen) + copy(dst, src) +} + +func (s *subprocess) saveFPState(msg *sysmsg.Msg, fpuToMsgOffset uint64, c *context, ac *arch.Context64) { + fpState := ac.FloatingPointData().BytePointer() + src := unsafeSlice(uintptr(unsafe.Pointer(msg))+uintptr(fpuToMsgOffset)+uintptr(sigFrameMagicHeaderLen), c.fpLen) + dst := unsafeSlice(uintptr(unsafe.Pointer(fpState)), c.fpLen) + copy(dst, src) +} diff --git a/pkg/sentry/platform/systrap/subprocess_linux.go b/pkg/sentry/platform/systrap/subprocess_linux.go new file mode 100644 index 000000000..3ac73bb82 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_linux.go @@ -0,0 +1,296 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux +// +build linux + +package systrap + +import ( + "fmt" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +const syscallEvent unix.Signal = 0x80 + +// createStub creates a fresh stub processes. +// +// Precondition: the runtime OS thread must be locked. +func createStub() (*thread, error) { + // When creating the new child process, we specify SIGKILL as the + // signal to deliver when the child exits. We never expect a subprocess + // to exit; they are pooled and reused. This is done to ensure that if + // a subprocess is OOM-killed, this process (and all other stubs, + // transitively) will be killed as well. It's simply not possible to + // safely handle a single stub getting killed: the exact state of + // execution is unknown and not recoverable. + return attachedThread(uintptr(unix.SIGKILL)|unix.CLONE_FILES, linux.SECCOMP_RET_TRAP) +} + +// attachedThread returns a new attached thread. +// +// Precondition: the runtime OS thread must be locked. +func attachedThread(flags uintptr, defaultAction linux.BPFAction) (*thread, error) { + // Create a BPF program that allows only the system calls needed by the + // stub and all its children. This is used to create child stubs + // (below), so we must include the ability to fork, but otherwise lock + // down available calls only to what is needed. + rules := []seccomp.RuleSet{} + if defaultAction != linux.SECCOMP_RET_ALLOW { + ruleSet := seccomp.RuleSet{ + Rules: seccomp.SyscallRules{ + unix.SYS_CLONE: []seccomp.Rule{ + // Allow creation of new subprocesses (used by the master). + {seccomp.EqualTo(unix.CLONE_FILES | unix.SIGKILL)}, + // Allow creation of new sysmsg thread. + {seccomp.EqualTo( + unix.CLONE_FILES | + unix.CLONE_FS | + unix.CLONE_VM | + unix.CLONE_PTRACE)}, + // Allow creation of new threads within a single address space (used by addresss spaces). + {seccomp.EqualTo( + unix.CLONE_FILES | + unix.CLONE_FS | + unix.CLONE_SIGHAND | + unix.CLONE_THREAD | + unix.CLONE_PTRACE | + unix.CLONE_VM)}, + }, + + // For the initial process creation. + unix.SYS_WAIT4: {}, + unix.SYS_EXIT: {}, + + // For the stub prctl dance (all). + unix.SYS_PRCTL: []seccomp.Rule{ + {seccomp.EqualTo(unix.PR_SET_PDEATHSIG), seccomp.EqualTo(unix.SIGKILL)}, + {seccomp.EqualTo(linux.PR_SET_NO_NEW_PRIVS), seccomp.EqualTo(1)}, + }, + unix.SYS_GETPPID: {}, + + // For the stub to stop itself (all). + unix.SYS_GETPID: {}, + unix.SYS_KILL: []seccomp.Rule{ + {seccomp.MatchAny{}, seccomp.EqualTo(unix.SIGSTOP)}, + }, + + // Injected to support the address space operations. + unix.SYS_MMAP: {}, + unix.SYS_MUNMAP: {}, + + // For sysmsg threads. Look at sysmsg/sighandler.c for more details. + unix.SYS_RT_SIGRETURN: {}, + unix.SYS_SCHED_YIELD: {}, + unix.SYS_FUTEX: { + seccomp.Rule{ + seccomp.MatchAny{}, + seccomp.EqualTo(linux.FUTEX_WAIT), + seccomp.MatchAny{}, + seccomp.MatchAny{}, + }, + seccomp.Rule{ + seccomp.MatchAny{}, + seccomp.EqualTo(linux.FUTEX_WAKE), + seccomp.MatchAny{}, + seccomp.MatchAny{}, + }, + }, + unix.SYS_SIGALTSTACK: {}, + unix.SYS_TKILL: { + {seccomp.MatchAny{}, seccomp.EqualTo(unix.SIGSTOP)}, + }, + unix.SYS_GETTID: {}, + seccomp.SYS_SECCOMP: { + {seccomp.EqualTo(linux.SECCOMP_SET_MODE_FILTER), seccomp.EqualTo(0), seccomp.MatchAny{}}, + }, + }, + Action: linux.SECCOMP_RET_ALLOW, + } + rules = append(rules, ruleSet) + rules = appendArchSeccompRules(rules) + } + instrs, err := seccomp.BuildProgram(rules, defaultAction, defaultAction) + if err != nil { + return nil, err + } + + return forkStub(flags, instrs) +} + +// In the child, this function must not acquire any locks, because they might +// have been locked at the time of the fork. This means no rescheduling, no +// malloc calls, and no new stack segments. For the same reason compiler does +// not race instrument it. +// +//go:norace +func forkStub(flags uintptr, instrs []linux.BPFInstruction) (*thread, error) { + // Declare all variables up front in order to ensure that there's no + // need for allocations between beforeFork & afterFork. + var ( + pid uintptr + ppid uintptr + errno unix.Errno + ) + + // Remember the current ppid for the pdeathsig race. + ppid, _, _ = unix.RawSyscall(unix.SYS_GETPID, 0, 0, 0) + + // Among other things, beforeFork masks all signals. + beforeFork() + + // Do the clone. + pid, _, errno = unix.RawSyscall6(unix.SYS_CLONE, flags, 0, 0, 0, 0, 0) + if errno != 0 { + afterFork() + return nil, errno + } + + // Is this the parent? + if pid != 0 { + // Among other things, restore signal mask. + afterFork() + + // Initialize the first thread. + t := &thread{ + tgid: int32(pid), + tid: int32(pid), + } + if sig := t.wait(stopped); sig != unix.SIGSTOP { + return nil, fmt.Errorf("wait failed: expected SIGSTOP, got %v", sig) + } + t.attach() + t.grabInitRegs() + _, err := t.syscallIgnoreInterrupt(&t.initRegs, unix.SYS_MUNMAP, + arch.SyscallArgument{Value: stubROMapEnd}, + arch.SyscallArgument{Value: maximumUserAddress - stubROMapEnd}) + if err != nil { + return nil, err + } + + return t, nil + } + + // Move the stub to a new session (and thus a new process group). This + // prevents the stub from getting PTY job control signals intended only + // for the sentry process. We must call this before restoring signal + // mask. + if _, _, errno := unix.RawSyscall(unix.SYS_SETSID, 0, 0, 0); errno != 0 { + unix.RawSyscall(unix.SYS_EXIT, uintptr(errno), 0, 0) + } + + // afterForkInChild resets all signals to their default dispositions + // and restores the signal mask to its pre-fork state. + afterForkInChild() + + if errno := sysmsgSigactions(stubSysmsgStart); errno != 0 { + unix.RawSyscall(unix.SYS_EXIT, uintptr(errno), 0, 0) + } + + // Explicitly unmask all signals to ensure that the tracer can see + // them. + if errno := unmaskAllSignals(); errno != 0 { + unix.RawSyscall(unix.SYS_EXIT, uintptr(errno), 0, 0) + } + + // Set an aggressive BPF filter for the stub and all it's children. See + // the description of the BPF program built above. + if errno := seccomp.SetFilterInChild(instrs); errno != 0 { + unix.RawSyscall(unix.SYS_EXIT, uintptr(errno), 0, 0) + } + + // Enable cpuid-faulting. + enableCpuidFault() + + // Call the stub; should not return. + stubCall(stubInitProcess, ppid) + panic("unreachable") +} + +// createStub creates a stub processes as a child of an existing subprocesses. +// +// Precondition: the runtime OS thread must be locked. +func (t *thread) createStub() (*thread, error) { + // There's no need to lock the runtime thread here, as this can only be + // called from a context that is already locked. + + // Pass the expected PPID to the child via R15. + regs := t.initRegs + initChildProcessPPID(®s, t.tgid) + + // Call fork in a subprocess. + // + // The new child must set up PDEATHSIG to ensure it dies if this + // process dies. Since this process could die at any time, this cannot + // be done via instrumentation from here. + // + // Instead, we create the child untraced, which will do the PDEATHSIG + // setup and then SIGSTOP itself for our attach below. + // + // See above re: SIGKILL. + pid, err := t.syscallIgnoreInterrupt( + ®s, + unix.SYS_CLONE, + arch.SyscallArgument{Value: uintptr(unix.SIGKILL | unix.CLONE_FILES)}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}) + if err != nil { + return nil, fmt.Errorf("creating stub process: %v", err) + } + + // Wait for child to enter group-stop, so we don't stop its + // bootstrapping work with t.attach below. + // + // We unfortunately don't have a handy part of memory to write the wait + // status. If the wait succeeds, we'll assume that it was the SIGSTOP. + // If the child actually exited, the attach below will fail. + _, err = t.syscallIgnoreInterrupt( + &t.initRegs, + unix.SYS_WAIT4, + arch.SyscallArgument{Value: uintptr(pid)}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: unix.WALL | unix.WUNTRACED}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}) + if err != nil { + return nil, fmt.Errorf("waiting on stub process: %v", err) + } + + childT := &thread{ + tgid: int32(pid), + tid: int32(pid), + } + + return childT, nil +} + +func (s *subprocess) createStub() (*thread, error) { + req := requestStub{} + req.done = make(chan *thread, 1) + s.requests <- req + + childT := <-req.done + childT.attach() + childT.grabInitRegs() + + return childT, nil +} diff --git a/pkg/sentry/platform/systrap/subprocess_linux_unsafe.go b/pkg/sentry/platform/systrap/subprocess_linux_unsafe.go new file mode 100644 index 000000000..a19e72625 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_linux_unsafe.go @@ -0,0 +1,52 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 || linux +// +build amd64 linux + +package systrap + +import ( + "sync" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +// maskPool contains reusable CPU masks for setting affinity. Unfortunately, +// runtime.NumCPU doesn't actually record the number of CPUs on the system, it +// just records the number of CPUs available in the scheduler affinity set at +// startup. This may a) change over time and b) gives a number far lower than +// the maximum indexable CPU. To prevent lots of allocation in the hot path, we +// use a pool to store large masks that we can reuse during bind. +var maskPool = sync.Pool{ + New: func() any { + const maxCPUs = 1024 // Not a hard limit; see below. + return make([]uintptr, maxCPUs/64) + }, +} + +// unmaskAllSignals unmasks all signals on the current thread. +// +// It is called in a child process after fork(), so the race instrumentation +// has to be disabled. +// +//go:nosplit +//go:norace +func unmaskAllSignals() unix.Errno { + var set linux.SignalSet + _, _, errno := unix.RawSyscall6(unix.SYS_RT_SIGPROCMASK, linux.SIG_SETMASK, uintptr(unsafe.Pointer(&set)), 0, linux.SignalSetSize, 0, 0) + return errno +} diff --git a/pkg/sentry/platform/systrap/subprocess_pool.go b/pkg/sentry/platform/systrap/subprocess_pool.go new file mode 100644 index 000000000..274f2f469 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_pool.go @@ -0,0 +1,78 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "sync" + + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/usertrap" +) + +// subprocessPool exists to solve these distinct problems: +// +// 1) Subprocesses can't always be killed properly (see subprocess.Release). +// In general it's helpful to be able to reuse subprocesses, but we must observe +// the subprocess lifecycle before we can do so (e.g. should wait for all +// contexts to be released). +// +// 2) Any seccomp filters that have been installed will apply to subprocesses +// created here. Therefore we use the intermediary (source), which is created +// on initialization of the platform. +// +// 3) Contexts are used in potentially many subprocesses, and upon +// context.Release their resources need to be cleaned up from each subprocess. +type subprocessPool struct { + mu sync.Mutex + source *subprocess + // available stores all subprocesses that are available for reuse. + // +checklocks:mu + available []*subprocess + // active stores all subprocesses that are currently active. + // +checklocks:mu + active subprocessList +} + +func (p *subprocessPool) add(s *subprocess) { + p.mu.Lock() + p.active.PushBack(s) + p.mu.Unlock() +} + +func (p *subprocessPool) release(s *subprocess) { + p.mu.Lock() + defer p.mu.Unlock() + s.mu.Lock() + defer s.mu.Unlock() + + s.released = true + if s.numContexts.Load() == 0 { + p.active.Remove(s) + p.available = append(p.available, s) + } +} + +func (p *subprocessPool) fetchAvailable() *subprocess { + p.mu.Lock() + defer p.mu.Unlock() + if len(p.available) > 0 { + sp := p.available[len(p.available)-1] + p.available = p.available[:len(p.available)-1] + p.active.PushBack(sp) + sp.usertrap = usertrap.New() + sp.released = false + return sp + } + return nil +} diff --git a/pkg/sentry/platform/systrap/subprocess_unsafe.go b/pkg/sentry/platform/systrap/subprocess_unsafe.go new file mode 100644 index 000000000..e7292ce13 --- /dev/null +++ b/pkg/sentry/platform/systrap/subprocess_unsafe.go @@ -0,0 +1,35 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.12 +// +build go1.12 + +// //go:linkname directives type-checked by checklinkname. Any other +// non-linkname assumptions outside the Go 1 compatibility guarantee should +// have an accompanied vet check or version guard build tag. + +package systrap + +import ( + _ "unsafe" // required for go:linkname. +) + +//go:linkname beforeFork syscall.runtime_BeforeFork +func beforeFork() + +//go:linkname afterFork syscall.runtime_AfterFork +func afterFork() + +//go:linkname afterForkInChild syscall.runtime_AfterForkInChild +func afterForkInChild() diff --git a/pkg/sentry/platform/systrap/syscall_thread.go b/pkg/sentry/platform/systrap/syscall_thread.go new file mode 100644 index 000000000..3f8c187f8 --- /dev/null +++ b/pkg/sentry/platform/systrap/syscall_thread.go @@ -0,0 +1,190 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "fmt" + "sync/atomic" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sentry/pgalloc" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" + "gvisor.dev/gvisor/pkg/sentry/usage" +) + +// The syscall message consists of sentry and stub messages. +const syscallThreadMessageSize = hostarch.PageSize * 2 + +// syscallThread implements the process of calling syscalls in a stub process. +// +// Each syscall thread owns a shared memory region to communicate with the +// Sentry. This region consists of two pages. The first page called +// sentryMessage is mapped as read-only in the stub address space. The second +// page called stubMessage is mapped as read-write in the stub process. +// +// Any memory regions that are mapped as read-write in a stub address space can +// be changed from a user code. This means that we can't trust the content of +// stubMessage, but it is used to receive a syscall return code. Therefore +// syscallThread can be used only in these cases: +// - If a system call never fails (e.g munmap). +// - If a system call has to return only one know value or if it fails, +// it doesn't not reveal any data (e.g. mmap). +type syscallThread struct { + // subproc is a link to the subprocess which is used to call native + // system calls and track when a sysmsg thread has to be recreated. + // Look at getSysmsgThread() for more details. + subproc *subprocess + + // thread is a thread identifier. + thread *thread + + // stackRange is the range for the sentry syscall message in the memory + // file. + stackRange memmap.FileRange + + // sentryAddr is the address of the shared memory region in the Sentry + // address space. + sentryAddr uintptr + // stubAddr is the address of the shared memory region in the stub + // address space. + stubAddr uintptr + + // sentryMessage is the first page of the share message that can't be + // modified by the stub thread. + sentryMessage *syscallSentryMessage + // stubMessage is the second page of the shared message that can be + // modified by the stub thread. + stubMessage *syscallStubMessage +} + +func (t *syscallThread) init() error { + // Allocate a new shared memory message. + opts := pgalloc.AllocOpts{ + Kind: usage.System, + Dir: pgalloc.TopDown, + } + fr, err := t.subproc.memoryFile.Allocate(syscallThreadMessageSize, opts) + if err != nil { + return err + } + + t.stackRange = fr + t.stubAddr = stubSysmsgStack + sysmsg.PerThreadMemSize*uintptr(t.thread.sysmsgStackID) + err = t.mapMessageIntoStub() + if err != nil { + t.destroy() + return err + } + + // Map the stack into the sentry. + sentryAddr, _, errno := unix.RawSyscall6( + unix.SYS_MMAP, + 0, + syscallThreadMessageSize, + unix.PROT_WRITE|unix.PROT_READ, + unix.MAP_SHARED|unix.MAP_FILE, + uintptr(t.subproc.memoryFile.FD()), uintptr(fr.Start)) + if errno != 0 { + t.destroy() + return fmt.Errorf("mmap failed: %v", errno) + } + t.sentryAddr = sentryAddr + + t.initRequestReplyAddresses(sentryAddr) + return nil +} + +func (t *syscallThread) destroy() { + if t.sentryAddr != 0 { + _, _, errno := unix.RawSyscall6( + unix.SYS_MUNMAP, + t.sentryAddr, + syscallThreadMessageSize, + 0, 0, 0, 0) + if errno != 0 { + panic(fmt.Sprintf("mumap failed: %v", errno)) + } + } + if t.stubAddr != 0 { + _, err := t.thread.syscallIgnoreInterrupt(&t.thread.initRegs, unix.SYS_MUNMAP, + arch.SyscallArgument{Value: t.stubAddr}, + arch.SyscallArgument{Value: uintptr(syscallThreadMessageSize)}) + if err != nil { + panic(fmt.Sprintf("munmap failed: %v", err)) + } + } + t.subproc.memoryFile.DecRef(t.stackRange) + t.subproc.sysmsgStackPool.Put(t.thread.sysmsgStackID) +} + +// mapMessageIntoStub maps the syscall message into the stub process address space. +func (t *syscallThread) mapMessageIntoStub() error { + // Map sentryMessage as read-only. + _, err := t.thread.syscallIgnoreInterrupt(&t.thread.initRegs, unix.SYS_MMAP, + arch.SyscallArgument{Value: t.stubAddr}, + arch.SyscallArgument{Value: uintptr(hostarch.PageSize)}, + arch.SyscallArgument{Value: uintptr(unix.PROT_READ)}, + arch.SyscallArgument{Value: unix.MAP_SHARED | unix.MAP_FILE | unix.MAP_FIXED}, + arch.SyscallArgument{Value: uintptr(t.subproc.memoryFile.FD())}, + arch.SyscallArgument{Value: uintptr(t.stackRange.Start)}) + if err != nil { + return err + } + // Map stubMessage as read-write. + _, err = t.thread.syscallIgnoreInterrupt(&t.thread.initRegs, unix.SYS_MMAP, + arch.SyscallArgument{Value: t.stubAddr + syscallStubMessageOffset}, + arch.SyscallArgument{Value: uintptr(hostarch.PageSize)}, + arch.SyscallArgument{Value: uintptr(unix.PROT_READ | unix.PROT_WRITE)}, + arch.SyscallArgument{Value: unix.MAP_SHARED | unix.MAP_FILE | unix.MAP_FIXED}, + arch.SyscallArgument{Value: uintptr(t.subproc.memoryFile.FD())}, + arch.SyscallArgument{Value: uintptr(t.stackRange.Start + hostarch.PageSize)}) + return err +} + +// attach attaches to the stub thread with ptrace and unlock signals. +func (t *syscallThread) attach() { + t.thread.attach() + // We need to unblock signals, because the TRAP signal is used to run + // syscalls via ptrace. + t.unmaskAllSignalsAttached() +} + +func (t *syscallThread) syscall(sysno uintptr, args ...arch.SyscallArgument) (uintptr, error) { + sentryMsg := t.sentryMessage + stubMsg := t.stubMessage + sentryMsg.sysno = uint64(sysno) + for i := 0; i < len(sentryMsg.args); i++ { + if i < len(args) { + sentryMsg.args[i] = uint64(args[i].Value) + } else { + sentryMsg.args[i] = 0 + } + } + + // Notify the syscall thread about a new syscall request. + atomic.AddUint32(&sentryMsg.state, 1) + futexWakeUint32(&sentryMsg.state) + + // Wait for reply. + // + // futex waits for sentryMsg.state that isn't changed, so it will + // returns only only when the other side will call FUTEX_WAKE. + futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state)) + + return uintptr(stubMsg.ret), nil +} diff --git a/pkg/sentry/platform/systrap/syscall_thread_amd64.go b/pkg/sentry/platform/systrap/syscall_thread_amd64.go new file mode 100644 index 000000000..fe81dd399 --- /dev/null +++ b/pkg/sentry/platform/systrap/syscall_thread_amd64.go @@ -0,0 +1,50 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package systrap + +import ( + "fmt" + "runtime" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +func (t *syscallThread) detach() { + p := t.thread + + // The syscall thread can't handle any signals and doesn't expect to + // receive anything. + t.maskAllSignalsAttached() + + regs := p.initRegs + regs.Rsp = 0 + regs.R12 = uint64(t.stubAddr) + regs.R13 = uint64(t.sentryMessage.state + 1) + regs.Rbx = _RUN_SYSCALL_LOOP + // Skip the syscall instruction. + regs.Rip += arch.SyscallWidth + if err := p.setRegs(®s); err != nil { + panic(fmt.Sprintf("ptrace set regs failed: %v", err)) + } + p.detach() + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(p.tgid), uintptr(p.tid), uintptr(unix.SIGCONT)); e != 0 { + panic(fmt.Sprintf("tkill failed: %v", e)) + } + runtime.UnlockOSThread() +} diff --git a/pkg/sentry/platform/systrap/syscall_thread_arm64.go b/pkg/sentry/platform/systrap/syscall_thread_arm64.go new file mode 100644 index 000000000..c9b4201b4 --- /dev/null +++ b/pkg/sentry/platform/systrap/syscall_thread_arm64.go @@ -0,0 +1,50 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package systrap + +import ( + "fmt" + "runtime" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +func (t *syscallThread) detach() { + p := t.thread + + // The syscall thread can't handle any signals and doesn't expect to + // receive anything. + t.maskAllSignalsAttached() + + regs := p.initRegs + regs.Sp = 0 + regs.Regs[12] = uint64(t.stubAddr) + regs.Regs[13] = uint64(t.sentryMessage.state + 1) + regs.Regs[9] = _RUN_SYSCALL_LOOP + // Skip the syscall instruction. + regs.Pc += arch.SyscallWidth + if err := p.setRegs(®s); err != nil { + panic(fmt.Sprintf("ptrace set regs failed: %v", err)) + } + p.detach() + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(p.tgid), uintptr(p.tid), uintptr(unix.SIGCONT)); e != 0 { + panic(fmt.Sprintf("tkill failed: %v", e)) + } + runtime.UnlockOSThread() +} diff --git a/pkg/sentry/platform/systrap/syscall_thread_defs.go b/pkg/sentry/platform/systrap/syscall_thread_defs.go new file mode 100644 index 000000000..96fc5438c --- /dev/null +++ b/pkg/sentry/platform/systrap/syscall_thread_defs.go @@ -0,0 +1,39 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "gvisor.dev/gvisor/pkg/hostarch" +) + +const syscallStubMessageOffset = hostarch.PageSize + +// syscallSentryMessage is a shared message that can be changed only from the +// Sentry and a stub process can only read it. +type syscallSentryMessage struct { + state uint32 + unused uint32 + sysno uint64 + args [6]uint64 +} + +// syscallStubMessage is a shared message that can be changed from a stub +// process. It is used to notify the Sentry that a requested system call has +// been executed. +// +// Attention: It can be compromised by user threads. +type syscallStubMessage struct { + ret uint64 +} diff --git a/pkg/sentry/platform/systrap/syscall_thread_unsafe.go b/pkg/sentry/platform/systrap/syscall_thread_unsafe.go new file mode 100644 index 000000000..73f7744da --- /dev/null +++ b/pkg/sentry/platform/systrap/syscall_thread_unsafe.go @@ -0,0 +1,91 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "fmt" + "sync/atomic" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +func (t *syscallThread) initRequestReplyAddresses(sentryStackAddr uintptr) { + // These are safe as these addresses are mmapped and never moved/gced. + sentryMessage := (*syscallSentryMessage)(unsafe.Pointer(sentryStackAddr)) + stubMessage := (*syscallStubMessage)(unsafe.Pointer(sentryStackAddr + syscallStubMessageOffset)) + atomic.StoreUint32(&sentryMessage.state, 0) + + t.sentryMessage = sentryMessage + t.stubMessage = stubMessage +} + +// maskAllSignals blocks all signals. +func (t *syscallThread) maskAllSignalsAttached() { + p := t.thread + + mask := ^uint64(0) + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, linux.PTRACE_SETSIGMASK, uintptr(p.tid), 8, uintptr(unsafe.Pointer(&mask)), 0, 0); errno != 0 { + panic(fmt.Sprintf("unable to setmask: %v", errno)) + } +} + +// unmaskAllSignals unblocks all signals. +func (t *syscallThread) unmaskAllSignalsAttached() { + p := t.thread + mask := uint64(0) + if _, _, errno := unix.RawSyscall6(unix.SYS_PTRACE, linux.PTRACE_SETSIGMASK, uintptr(p.tid), 8, uintptr(unsafe.Pointer(&mask)), 0, 0); errno != 0 { + panic(fmt.Sprintf("unable to setmask: %v", errno)) + } +} + +func futexWakeUint32(addr *uint32) error { + if _, _, e := unix.RawSyscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(addr)), linux.FUTEX_WAKE, 1, 0, 0, 0); e != 0 { + return fmt.Errorf("failed to FUTEX_WAKE: %v", e) + } + return nil +} + +func futexWaitForUint32(addr *uint32, targetValue uint32) error { + for { + val := atomic.LoadUint32(addr) + if val == targetValue { + break + } + + _, _, e := unix.Syscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(addr)), linux.FUTEX_WAIT, uintptr(val), 0, 0, 0) + if e != 0 && e != unix.EAGAIN && e != unix.EINTR { + return fmt.Errorf("failed to FUTEX_WAIT: %v", e) + } + } + return nil +} + +// futexWaitWake waits when other side will call FUTEX_WAKE. A value of the +// futex word has to be equal to futexValue and it must not be changed. +func futexWaitWake(futexAddr *uint32, futexValue uint32) error { + for { + _, _, e := unix.Syscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(futexAddr)), linux.FUTEX_WAIT, uintptr(futexValue), 0, 0, 0) + if e == 0 { + break + } + if e != unix.EAGAIN && e != unix.EINTR { + return fmt.Errorf("failed to FUTEX_WAIT: %v", e) + } + } + + return nil +} diff --git a/pkg/sentry/platform/systrap/sysmsg/BUILD b/pkg/sentry/platform/systrap/sysmsg/BUILD new file mode 100644 index 000000000..5c4d31661 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/BUILD @@ -0,0 +1,138 @@ +load("//tools:arch.bzl", "select_arch") +load("//tools:defs.bzl", "cc_flags_supplier", "cc_toolchain", "go_library") +load("build.bzl", "cc_pie_obj") + +package(licenses = ["notice"]) + +cc_pie_obj( + name = "sighandler", + srcs = select_arch( + amd64 = ["sighandler_amd64.c"], + arm64 = ["sighandler_arm64.c"], + ) + [ + "sysmsg.h", + "sysmsg_offsets.h", + ], + outs = ["sighandler.o"], + obj_src = select_arch( + amd64 = "sighandler_amd64.c", + arm64 = "sighandler_arm64.c", + ), +) + +cc_pie_obj( + name = "sysmsg_lib", + srcs = [ + "sysmsg.h", + "sysmsg_lib.c", + "sysmsg_offsets.h", + ], + outs = ["sysmsg_lib.o"], + obj_src = "sysmsg_lib.c", +) + +cc_pie_obj( + name = "sysrestorer", + srcs = select_arch( + amd64 = ["sigrestorer_amd64.S"], + arm64 = ["sigrestorer_arm64.S"], + ), + outs = ["sysrestorer.o"], + obj_src = select_arch( + amd64 = "sigrestorer_amd64.S", + arm64 = "sigrestorer_arm64.S", + ), +) + +cc_pie_obj( + name = "syshandler", + srcs = select_arch( + amd64 = ["syshandler_amd64.S"], + arm64 = ["syshandler_arm64.S"], + ) + [ + "sysmsg_offsets.h", + ], + outs = ["syshandler.o"], + obj_src = select_arch( + amd64 = "syshandler_amd64.S", + arm64 = "syshandler_arm64.S", + ), +) + +genrule( + name = "sighandler.built-in.object", + srcs = [ + "pie.lds.S", + ":sighandler", + ":syshandler", + ":sysrestorer", + ":sysmsg_lib", + ], + outs = ["sighandler.built-in.bin.o"], + cmd = "$(LD) " + + "-o $(location sighandler.built-in.bin.o) " + + "-pie -z noexecstack -T $(location pie.lds.S) " + + "$(location :sysrestorer) $(location :sighandler) " + + "$(location :syshandler) $(location :sysmsg_lib) ", + features = ["-pie"], + toolchains = [ + cc_toolchain, + ], +) + +genrule( + name = "sighandler.built-in.binary", + srcs = [ + "sighandler.built-in.bin.o", + ":sighandler.built-in.object", + ], + outs = ["sighandler.built-in.bin"], + cmd = "$(OBJCOPY) -O binary " + + "$(location :sighandler.built-in.object) $(location sighandler.built-in.bin) ", + toolchains = [ + cc_toolchain, + ], +) + +genrule( + name = "sighandler-golang", + srcs = [ + "sighandler.built-in.bin", + "sighandler.built-in.bin.o", + ":sighandler.built-in.object", + ], + outs = ["sighandler.go"], + cmd = "bash -x $(location gen_offsets_go.sh) sighandler Sighandler " + + "$(location :sighandler.built-in.object) " + + "> $(location sighandler.go) ", + tools = [ + "gen_offsets_go.sh", + ], +) + +cc_flags_supplier( + name = "no_pie_cc_flags", + features = ["-pie"], +) + +go_library( + name = "sysmsg", + srcs = [ + "sighandler.go", + "sysmsg.go", + "sysmsg_amd64.go", + "sysmsg_arm64.go", + ], + embedsrcs = ["sighandler.built-in.bin"], + visibility = ["//:sandbox"], + deps = [ + "//pkg/abi/linux", + "//pkg/abi/linux/errno", + "//pkg/atomicbitops", + "//pkg/cpuid", + "//pkg/errors", + "//pkg/hostarch", + "//pkg/sentry/arch", + "//pkg/usermem", + ], +) diff --git a/pkg/sentry/platform/systrap/sysmsg/build.bzl b/pkg/sentry/platform/systrap/sysmsg/build.bzl new file mode 100644 index 000000000..5803668d6 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/build.bzl @@ -0,0 +1,35 @@ +"""Sysmsg rules.""" + +load("//tools:arch.bzl", "select_arch") +load("//tools:defs.bzl", "cc_toolchain") + +def cc_pie_obj(name, srcs, outs, obj_src): + native.genrule( + name = name, + srcs = srcs, + outs = outs, + cmd = "$(CC) $(CC_FLAGS) " + + "-fpie " + + # -01 is required for clang to avoid making use of memcpy when + # building for ARM64. For some reason when no optimization is turned + # on clang makes use of memcpy to copy structures and when combined + # with -ffreestanding it means we need to provide our own version of + # memcpy. Using -01 causes clang to not make use of memcpy avoiding + # the need to supply our own memcpy version. + select_arch( + amd64 = "-O2", + arm64 = "-O1 -mno-outline-atomics ", + ) + + " -fno-builtin " + + "-ffreestanding " + + "-g " + + "-Wa,--noexecstack " + + "-fno-asynchronous-unwind-tables " + + "-fno-stack-protector -c " + + "$(location " + obj_src + ") " + + " -o $(location " + outs[0] + ")", + toolchains = [ + ":no_pie_cc_flags", + cc_toolchain, + ], + ) diff --git a/pkg/sentry/platform/systrap/sysmsg/gen_offsets_go.sh b/pkg/sentry/platform/systrap/sysmsg/gen_offsets_go.sh new file mode 100644 index 000000000..7328a8e4f --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/gen_offsets_go.sh @@ -0,0 +1,39 @@ +# Copyright 2020 The gVisor Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script generates a golang file which contains: +# * byte array with the sysmsg stub binary blob. +# * set of variables with addresses of exported symbols. + +#!/bin/bash + +set -e +set -u + +FILE=$1 +NAME=$2 + +PREFIX=${NAME}_blob_offset__ +BLOB=${NAME}_blob +OBJNAME=$3 + +AWK_CMD='$2 ~ /^[tBCTA]$/ { print "var '$PREFIX'" $3 " = 0x" $1 }' + +cat << EOF +/* Autogenerated by $0, do not edit */ +package sysmsg + +EOF + +nm "$OBJNAME" | grep "__export_" | tr . _ | awk "$AWK_CMD" diff --git a/pkg/sentry/platform/systrap/sysmsg/pie.lds.S b/pkg/sentry/platform/systrap/sysmsg/pie.lds.S new file mode 100644 index 000000000..9d7ad07ac --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/pie.lds.S @@ -0,0 +1,39 @@ +/* Copyright 2020 The gVisor Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +SECTIONS +{ + .crblob 0x0 : { + *(.head.text) + *(.text*) + . = ALIGN(32); + *(.data*) + . = ALIGN(32); + *(COMMON*) + . = ALIGN(32); + *(.rodata*) + . = ALIGN(32); + *(.bss*) + . = ALIGN(32); + *(.got*) + . = ALIGN(32); + *(.debug*) + . = ALIGN(32); + } =0x00000000, + + /DISCARD/ : { + *(.interp) *(.gnu.hash) *(.hash) *(.dynamic) *(.dynsym) *(.dynstr) *(.rela.dyn) *(.eh_frame) + } +} diff --git a/pkg/sentry/platform/systrap/sysmsg/sighandler_amd64.c b/pkg/sentry/platform/systrap/sysmsg/sighandler_amd64.c new file mode 100644 index 000000000..23c8a9c54 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sighandler_amd64.c @@ -0,0 +1,344 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sysmsg.h" +#include "sysmsg_offsets.h" + +long __syscall(long n, long a1, long a2, long a3, long a4, long a5, long a6) { + unsigned long ret; + register long r10 __asm__("r10") = a4; + register long r8 __asm__("r8") = a5; + register long r9 __asm__("r9") = a6; + __asm__ __volatile__("syscall" + : "=a"(ret) + : "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10), "r"(r8), + "r"(r9) + : "rcx", "r11", "memory"); + return ret; +} + +long sys_futex(uint32_t *addr, int op, int val, struct __kernel_timespec *tv, + uint32_t *addr2, int val3) { + return __syscall(__NR_futex, (long)addr, (long)op, (long)val, (long)tv, + (long)addr2, (long)val3); +} + +union csgsfs { + uint64_t csgsfs; // REG_CSGSFS + struct { + uint16_t cs; + uint16_t gs; + uint16_t fs; + uint16_t ss; + }; +}; + +static void gregs_to_ptregs(ucontext_t *ucontext, struct sysmsg *sysmsg) { + union csgsfs csgsfs = {.csgsfs = ucontext->uc_mcontext.gregs[REG_CSGSFS]}; + + // Set all registers except: + // * fs_base and gs_base, because they can be only changed by arch_prctl. + // * DS and ES are not used on x86_64. + + sysmsg->ptregs.r15 = ucontext->uc_mcontext.gregs[REG_R15]; + sysmsg->ptregs.r14 = ucontext->uc_mcontext.gregs[REG_R14]; + sysmsg->ptregs.r13 = ucontext->uc_mcontext.gregs[REG_R13]; + sysmsg->ptregs.r12 = ucontext->uc_mcontext.gregs[REG_R12]; + sysmsg->ptregs.rbp = ucontext->uc_mcontext.gregs[REG_RBP]; + sysmsg->ptregs.rbx = ucontext->uc_mcontext.gregs[REG_RBX]; + sysmsg->ptregs.r11 = ucontext->uc_mcontext.gregs[REG_R11]; + sysmsg->ptregs.r10 = ucontext->uc_mcontext.gregs[REG_R10]; + sysmsg->ptregs.r9 = ucontext->uc_mcontext.gregs[REG_R9]; + sysmsg->ptregs.r8 = ucontext->uc_mcontext.gregs[REG_R8]; + sysmsg->ptregs.rax = ucontext->uc_mcontext.gregs[REG_RAX]; + sysmsg->ptregs.rcx = ucontext->uc_mcontext.gregs[REG_RCX]; + sysmsg->ptregs.rdx = ucontext->uc_mcontext.gregs[REG_RDX]; + sysmsg->ptregs.rsi = ucontext->uc_mcontext.gregs[REG_RSI]; + sysmsg->ptregs.rdi = ucontext->uc_mcontext.gregs[REG_RDI]; + sysmsg->ptregs.rip = ucontext->uc_mcontext.gregs[REG_RIP]; + sysmsg->ptregs.eflags = ucontext->uc_mcontext.gregs[REG_EFL]; + sysmsg->ptregs.rsp = ucontext->uc_mcontext.gregs[REG_RSP]; + + sysmsg->ptregs.cs = csgsfs.cs; + sysmsg->ptregs.ss = csgsfs.ss; + sysmsg->ptregs.fs = csgsfs.fs; + sysmsg->ptregs.gs = csgsfs.gs; +} + +static void ptregs_to_gregs(ucontext_t *ucontext, struct sysmsg *sysmsg) { + union csgsfs csgsfs = {.csgsfs = ucontext->uc_mcontext.gregs[REG_CSGSFS]}; + + ucontext->uc_mcontext.gregs[REG_R15] = sysmsg->ptregs.r15; + ucontext->uc_mcontext.gregs[REG_R14] = sysmsg->ptregs.r14; + ucontext->uc_mcontext.gregs[REG_R13] = sysmsg->ptregs.r13; + ucontext->uc_mcontext.gregs[REG_R12] = sysmsg->ptregs.r12; + ucontext->uc_mcontext.gregs[REG_RBP] = sysmsg->ptregs.rbp; + ucontext->uc_mcontext.gregs[REG_RBX] = sysmsg->ptregs.rbx; + ucontext->uc_mcontext.gregs[REG_R11] = sysmsg->ptregs.r11; + ucontext->uc_mcontext.gregs[REG_R10] = sysmsg->ptregs.r10; + ucontext->uc_mcontext.gregs[REG_R9] = sysmsg->ptregs.r9; + ucontext->uc_mcontext.gregs[REG_R8] = sysmsg->ptregs.r8; + ucontext->uc_mcontext.gregs[REG_RAX] = sysmsg->ptregs.rax; + ucontext->uc_mcontext.gregs[REG_RCX] = sysmsg->ptregs.rcx; + ucontext->uc_mcontext.gregs[REG_RDX] = sysmsg->ptregs.rdx; + ucontext->uc_mcontext.gregs[REG_RSI] = sysmsg->ptregs.rsi; + ucontext->uc_mcontext.gregs[REG_RDI] = sysmsg->ptregs.rdi; + ucontext->uc_mcontext.gregs[REG_RIP] = sysmsg->ptregs.rip; + ucontext->uc_mcontext.gregs[REG_EFL] = sysmsg->ptregs.eflags; + ucontext->uc_mcontext.gregs[REG_RSP] = sysmsg->ptregs.rsp; + + csgsfs.cs = sysmsg->ptregs.cs; + csgsfs.ss = sysmsg->ptregs.ss; + csgsfs.fs = sysmsg->ptregs.fs; + csgsfs.gs = sysmsg->ptregs.gs; + + ucontext->uc_mcontext.gregs[REG_CSGSFS] = csgsfs.csgsfs; +} + +// get_fsbase writes the current thread's fsbase value to ptregs. +static void get_fsbase(struct user_regs_struct *ptregs) { + uint64_t fsbase; + if (__export_arch_state.fsgsbase) { + asm volatile("rdfsbase %0" : "=r"(fsbase)); + } else { + int ret = + __syscall(__NR_arch_prctl, ARCH_GET_FS, (long)&fsbase, 0, 0, 0, 0); + if (ret) { + panic(ret); + } + } + ptregs->fs_base = fsbase; +} + +// set_fsbase sets the current thread's fsbase to the fsbase value in ptregs. +static void set_fsbase(struct user_regs_struct *ptregs) { + uint64_t fsbase = ptregs->fs_base; + if (__export_arch_state.fsgsbase) { + asm volatile("wrfsbase %0" : : "r"(fsbase) : "memory"); + } else { + int ret = __syscall(__NR_arch_prctl, ARCH_SET_FS, fsbase, 0, 0, 0, 0); + if (ret) { + panic(ret); + } + } +} + +void __export_sighandler(int signo, siginfo_t *siginfo, void *_ucontext) { + ucontext_t *ucontext = _ucontext; + void *sp = sysmsg_sp(); + struct sysmsg *sysmsg = sysmsg_addr(sp); + + if (sysmsg != sysmsg->self) panic(0xdeaddead); + + if (signo == SIGCHLD) { + // If the current thread is in syshandler, an interrupt has to be postponed, + // because sysmsg can't be changed. + int32_t state; + state = __atomic_load_n(&sysmsg->state, __ATOMIC_ACQUIRE); + if ((state != SYSMSG_STATE_NONE) || + (ucontext->uc_mcontext.gregs[REG_RSP] > (unsigned long)sp)) { + __atomic_store_n(&sysmsg->interrupt, 1, __ATOMIC_RELEASE); + return; + } + } else if (signo == SIGILL && sysmsg->type == SYSMSG_INTERRUPT) { + // This is a postponed SignalInterrupt from syshandler. + signo = SIGCHLD; + siginfo->si_signo = SIGCHLD; + __atomic_store_n(&sysmsg->interrupt, 0, __ATOMIC_RELAXED); + // Skip the fault instruction. + ucontext->uc_mcontext.gregs[REG_RIP] = sysmsg->ret_addr; + } + + // Handle faults in syshandler. + if ((signo == SIGSEGV || signo == SIGBUS) && sysmsg->fault_jump) { + ucontext->uc_mcontext.gregs[REG_RIP] += sysmsg->fault_jump; + sysmsg->fault_jump = 0; + return; + } + + sysmsg->signo = signo; + gregs_to_ptregs(ucontext, sysmsg); + sysmsg->fpstate = + (unsigned long)ucontext->uc_mcontext.fpregs - (unsigned long)sysmsg; + sysmsg->siginfo = *siginfo; + switch (signo) { + case SIGSYS: { + int si_sysno = siginfo->si_syscall; + int i; + sysmsg->type = SYSMSG_SYSCALL; + + // Check whether this syscall can be replaced on a function call or not. + // If a syscall instruction set is "mov sysno, %eax, syscall", it can be + // replaced on a function call which works much faster. + // Look at pkg/sentry/usertrap for more details. + // + // Exclude all syscalls which requires a full thread state to be handled. + if (siginfo->si_arch == AUDIT_ARCH_X86_64 && si_sysno != __NR_execveat && + si_sysno != __NR_execve && si_sysno != __NR_fork && + si_sysno != __NR_clone && si_sysno != __NR_vfork && + si_sysno != __NR_rt_sigreturn && si_sysno != __NR_arch_prctl) { + uint8_t *rip = (uint8_t *)sysmsg->ptregs.rip; + // FIXME(b/144063246): Even if all five bytes before the syscall + // instruction match the "mov sysno, %eax" instruction, they can be a + // part of a longer instruction. Here is not easy way to decode x86 + // instructions in reverse. + uint64_t syscall_code_int[2]; + uint8_t *syscall_code = (uint8_t *)&syscall_code_int[0]; + + // We need to receive 5 bytes before the syscall instruction, but they + // are not aligned, so we can't read them atomically. Let's read them + // twice. If the second copy will not contain the FAULT_OPCODE, this + // will mean that the first copy is in the consistent state. + for (int i = 0; i < 2; i++) { + // fault_jump is set to the size of "mov (%rbx)" which is 3 bytes. + __atomic_store_n(&sysmsg->fault_jump, 3, __ATOMIC_RELEASE); + asm volatile("movq (%1), %0\n" + : "=a"(syscall_code_int[i]) + : "b"(rip - 8) + : "cc", "memory"); + __atomic_store_n(&sysmsg->fault_jump, 0, __ATOMIC_RELEASE); + } + // The mov instruction is 5 bytes: b8 . + // The syscall instruction is 2 bytes: 0f 05. + uint32_t sysno = *(uint32_t *)(syscall_code + 2); + int need_trap = *(syscall_code + 6) == 0x0f && // syscall + *(syscall_code + 7) == 0x05 && + *(syscall_code + 1) == 0xb8 && // mov sysno, %eax + sysno == siginfo->si_syscall && + sysno == sysmsg->ptregs.rax; + + // Restart syscall if it has been patched by another thread. When a + // syscall instruction set is replaced on a function call, all threads + // have to call it via the function call. Otherwise the syscall will not + // be restarted properly if it will be interrupted by signal. + syscall_code = (uint8_t *)&syscall_code_int[1]; + uint8_t syscall_opcode = *(syscall_code + 6); + + // A binary patch is built so that the first byte of the syscall + // instruction is changed on the invalid instuction. If we meet this + // case, this means that another thread has been patched this syscall + // and we need to restart it. + if (syscall_opcode == FAULT_OPCODE) { + ucontext->uc_mcontext.gregs[REG_RIP] -= 7; + return; + } + + if (need_trap) { + // This syscall can be replaced on the function call. + sysmsg->type = SYSMSG_SYSCALL_NEED_TRAP; + } + } + sysmsg->ptregs.orig_rax = sysmsg->ptregs.rax; + sysmsg->ptregs.rax = (unsigned long)-ENOSYS; + if (siginfo->si_arch != AUDIT_ARCH_X86_64) + // gVisor doesn't support x32 system calls, so let's change the syscall + // number so that it returns ENOSYS. + sysmsg->ptregs.orig_rax += 0x86000000; + break; + } + case SIGCHLD: + case SIGSEGV: + case SIGBUS: + case SIGFPE: + case SIGTRAP: + case SIGILL: + sysmsg->ptregs.orig_rax = -1; + sysmsg->type = SYSMSG_FAULT; + break; + default: + return; + } + get_fsbase(&sysmsg->ptregs); + long fs_base = sysmsg->ptregs.fs_base; + + wait_state(sysmsg, SYSMSG_STATE_EVENT); + + if (fs_base != sysmsg->ptregs.fs_base) { + set_fsbase(&sysmsg->ptregs); + } + ptregs_to_gregs(ucontext, sysmsg); + __atomic_store_n(&sysmsg->state, SYSMSG_STATE_NONE, __ATOMIC_RELEASE); +} + +// Function arguments: %rdi,%rsi, %rdx, %rcx, %r8 and %r9. +// http://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf +long __syshandler(long a1, long a2, long a3, long __unused, long a5, long a6) { + long sysno, a4, rip; + struct sysmsg *sysmsg; + asm volatile( + "movq %%rax, %0\n" + "movq %%r10, %1\n" + : "=m"(sysno), "=m"(a4) + : + :); + asm volatile("movq %%gs:0, %0\n" : "=r"(sysmsg) : :); + + BUILD_BUG_ON(offsetof_sysmsg_self != offsetof(struct sysmsg, self)); + BUILD_BUG_ON(offsetof_sysmsg_ret_addr != offsetof(struct sysmsg, ret_addr)); + BUILD_BUG_ON(offsetof_sysmsg_syshandler != + offsetof(struct sysmsg, syshandler)); + BUILD_BUG_ON(offsetof_sysmsg_syshandler_stack != + offsetof(struct sysmsg, syshandler_stack)); + BUILD_BUG_ON(offsetof_sysmsg_app_stack != offsetof(struct sysmsg, app_stack)); + BUILD_BUG_ON(offsetof_sysmsg_interrupt != offsetof(struct sysmsg, interrupt)); + BUILD_BUG_ON(offsetof_sysmsg_type != offsetof(struct sysmsg, type)); + BUILD_BUG_ON(offsetof_sysmsg_state != offsetof(struct sysmsg, state)); + BUILD_BUG_ON(kSYSMSG_SYSCALL != SYSMSG_SYSCALL); + BUILD_BUG_ON(kSYSMSG_INTERRUPT != SYSMSG_INTERRUPT); + + // SYSMSG_STATE_PREP is set to postpone interrupts. Look at + // __export_sighandler for more details. + int state = __atomic_load_n(&sysmsg->state, __ATOMIC_ACQUIRE); + if (state != SYSMSG_STATE_PREP) panic(state); + sysmsg->signo = SIGSYS; + sysmsg->ptregs.rax = sysno; + sysmsg->ptregs.rdi = a1; + sysmsg->ptregs.rsi = a2; + sysmsg->ptregs.rdx = a3; + sysmsg->ptregs.r10 = a4; + sysmsg->ptregs.r8 = a5; + sysmsg->ptregs.r9 = a6; + sysmsg->ptregs.rsp = sysmsg->app_stack; + sysmsg->ptregs.rip = sysmsg->ret_addr; + sysmsg->type = SYSMSG_SYSCALL_TRAP; + sysmsg->ptregs.orig_rax = sysmsg->ptregs.rax; + sysmsg->ptregs.rax = (unsigned long)-ENOSYS; + sysmsg->siginfo.si_addr = 0; + sysmsg->siginfo.si_syscall = sysno; + __atomic_store_n(&sysmsg->interrupt, 0, __ATOMIC_RELAXED); + + state = wait_state(sysmsg, SYSMSG_STATE_EVENT); + long sysret = sysmsg->ptregs.rax; + if (state == SYSMSG_STATE_SIGACT) { + sysmsg->type = SYSMSG_SYSCALL; + return -1; + } + + __atomic_store_n(&sysmsg->state, SYSMSG_STATE_NONE, __ATOMIC_RELEASE); + return sysret; +} diff --git a/pkg/sentry/platform/systrap/sysmsg/sighandler_arm64.c b/pkg/sentry/platform/systrap/sysmsg/sighandler_arm64.c new file mode 100644 index 000000000..b13cad228 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sighandler_arm64.c @@ -0,0 +1,128 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sysmsg.h" +#include "sysmsg_offsets.h" + +long __syscall(long n, long a1, long a2, long a3, long a4, long a5, long a6) { + // ARM64 syscall interface passes the syscall number in x8 and the 6 arguments + // in x0-x5. The return value is in x0. + // + // See: https://man7.org/linux/man-pages/man2/syscall.2.html + register long x8 __asm__("x8") = n; + register long x0 __asm__("x0") = a1; + register long x1 __asm__("x1") = a2; + register long x2 __asm__("x2") = a3; + register long x3 __asm__("x3") = a4; + register long x4 __asm__("x4") = a5; + register long x5 __asm__("x5") = a6; + __asm__ __volatile__("svc #0" + : "=r"(x0) + : "r"(x8), "0"(x0), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) + : "memory", "cc"); + return x0; +} + +static __inline void set_tls(uint64_t tls) { + __asm__("msr tpidr_el0,%0" : : "r"(tls)); +} + +static __inline uint64_t get_tls() { + uint64_t tls; + __asm__("mrs %0,tpidr_el0" : "=r"(tls)); + return tls; +} + +long sys_futex(uint32_t *addr, int op, int val, struct __kernel_timespec *tv, + uint32_t *addr2, int val3) { + return __syscall(__NR_futex, (long)addr, (long)op, (long)val, (long)tv, + (long)addr2, (long)val3); +} + +static void gregs_to_ptregs(ucontext_t *ucontext, struct sysmsg *sysmsg) { + // Set all registers. + for (int i = 0; i < 31; i++ ) { + sysmsg->ptregs.regs[i] = ucontext->uc_mcontext.regs[i]; + } + sysmsg->ptregs.sp = ucontext->uc_mcontext.sp; + sysmsg->ptregs.pc = ucontext->uc_mcontext.pc; + sysmsg->ptregs.pstate = ucontext->uc_mcontext.pstate; +} + +static void ptregs_to_gregs(ucontext_t *ucontext, struct sysmsg *sysmsg) { + for (int i = 0; i < 31; i++ ) { + ucontext->uc_mcontext.regs[i] = sysmsg->ptregs.regs[i]; + } + ucontext->uc_mcontext.sp = sysmsg->ptregs.sp; + ucontext->uc_mcontext.pc = sysmsg->ptregs.pc; + ucontext->uc_mcontext.pstate = sysmsg->ptregs.pstate; +} + +void __export_sighandler(int signo, siginfo_t *siginfo, void *_ucontext) { + ucontext_t *ucontext = _ucontext; + void *sp = sysmsg_sp(); + struct sysmsg *sysmsg = sysmsg_addr(sp); + + if (sysmsg != sysmsg->self) panic(0xdeaddead); + + sysmsg->signo = signo; + + gregs_to_ptregs(ucontext, sysmsg); + sysmsg->fpstate = + (uint64_t)(&ucontext->uc_mcontext.__reserved) - (uint64_t)sysmsg; + sysmsg->tls = get_tls(); + sysmsg->siginfo = *siginfo; + switch (signo) { + case SIGSYS: { + sysmsg->type = SYSMSG_SYSCALL; + if (siginfo->si_arch != AUDIT_ARCH_AARCH64) { + // gVisor doesn't support x32 system calls, so let's change the syscall + // number so that it returns ENOSYS. The value added here is just a + // random large number which is large enough to not match any existing + // syscall number in linux. + sysmsg->ptregs.regs[8] += 0x86000000; + } + break; + } + case SIGCHLD: + case SIGSEGV: + case SIGBUS: + case SIGFPE: + case SIGTRAP: + case SIGILL: + sysmsg->type = SYSMSG_FAULT; + break; + default: + return; + } + + wait_state(sysmsg, SYSMSG_STATE_EVENT); + + ptregs_to_gregs(ucontext, sysmsg); + set_tls(sysmsg->tls); + __atomic_store_n(&sysmsg->state, SYSMSG_STATE_NONE, __ATOMIC_RELEASE); +} diff --git a/pkg/sentry/platform/systrap/sysmsg/sigrestorer_amd64.S b/pkg/sentry/platform/systrap/sysmsg/sigrestorer_amd64.S new file mode 100644 index 000000000..71a3d6c62 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sigrestorer_amd64.S @@ -0,0 +1,22 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +.global __export_restore_rt; +.type __export_restore_rt, @function; +__export_restore_rt: + movq $__NR_rt_sigreturn, %rax + syscall +.size __export_restore_rt,.-__export_restore_rt diff --git a/pkg/sentry/platform/systrap/sysmsg/sigrestorer_arm64.S b/pkg/sentry/platform/systrap/sysmsg/sigrestorer_arm64.S new file mode 100644 index 000000000..b95b2476f --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sigrestorer_arm64.S @@ -0,0 +1,22 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +.global __export_restore_rt; +.type __export_restore_rt, @function; +__export_restore_rt: + mov x8, __NR_rt_sigreturn + svc #0 +.size __export_restore_rt,.-__export_restore_rt diff --git a/pkg/sentry/platform/systrap/sysmsg/syshandler_amd64.S b/pkg/sentry/platform/systrap/sysmsg/syshandler_amd64.S new file mode 100644 index 000000000..bd48ea9eb --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/syshandler_amd64.S @@ -0,0 +1,78 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sysmsg_offsets.h" + +.globl __export_syshandler; +.type __export_syshandler, @function; +.align 4, 0x00; +__export_syshandler: + // The start of this function is in a usertrap trampoline: + // mov %rsp,%gs:0x20 + // mov %gs:0x18,%rs + // movabs $ret_addr, %rax + // mov %rax, %fs:0x8 + // mov sysno, %eax + // jmpq *%gs,0x10 + + // Save registers which are not preserved across function calls. + // http://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf + push %rbp + push %r11 + push %r10 + push %r9 + push %r8 + push %rdi + push %rsi + push %rdx + push %rcx + + // We have to avoid races with sighandler, so if sysmsg isn't equal to + // SYSMSG_STATE_NONE, we can't fault on a user stack. + // + // We can fault on a user stack, what a page isn't mapped yet or when a + // process is dieing and a process address spaces has been cleaned up (see + // subprocess.Release). + callq __syshandler + + // Restore registers and return back to a guest code. + pop %rcx + pop %rdx + pop %rsi + pop %rdi + pop %r8 + pop %r9 + pop %r10 + pop %r11 + pop %rbp + + movq %gs:offsetof_sysmsg_app_stack,%rsp + + cmpl $kSYSMSG_SYSCALL, %gs:offsetof_sysmsg_type // msg->type + jne skipsyscall + mov $0xffff, %eax // any syscall which isn't allowed by seccomp. + // nop is here to avoid matching the `mov sysno, %eax; syscall` pattern that + // we are substituting with function calls. + nop + syscall + jmp skipint +skipsyscall: + cmpl $0, %gs:offsetof_sysmsg_interrupt // msg->interrupt + je skipint + movl $kSYSMSG_INTERRUPT, %gs:offsetof_sysmsg_type + .byte FAULT_OPCODE // Re-trigger the interrupt. +skipint: + jmp *%gs:offsetof_sysmsg_ret_addr // msg->ret_addr + +.size __export_syshandler, . - __export_syshandler diff --git a/pkg/sentry/platform/systrap/sysmsg/syshandler_arm64.S b/pkg/sentry/platform/systrap/sysmsg/syshandler_arm64.S new file mode 100644 index 000000000..448f0ebba --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/syshandler_arm64.S @@ -0,0 +1,28 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sysmsg_offsets.h" + +.globl __export_syshandler; +.type __export_syshandler, @function; +.align 4, 0x00; +// syshandler is not implemented for ARM64 yet. +__export_syshandler: + // BRK will generate an Debug Exception which cannot be masked. + // See: https://developer.arm.com/documentation/102120/0100/Debug-exceptions + // The immediate unsigned operand needs to be <= 0xffff. + // See: https://developer.arm.com/documentation/dui0802/b/A64-General-Instructions/BRK + BRK #0xdead + +.size __export_syshandler, . - __export_syshandler diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg.go b/pkg/sentry/platform/systrap/sysmsg/sysmsg.go new file mode 100644 index 000000000..a4eecde7e --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg.go @@ -0,0 +1,251 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package sysmsg provides a stub signal handler and a communication protocol +// between stub threads and the Sentry. +// +// Note that this package is allowlisted for use of sync/atomic. +// +// +checkalignedignore +package sysmsg + +import ( + _ "embed" + "fmt" + "strings" + "sync/atomic" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/abi/linux/errno" + "gvisor.dev/gvisor/pkg/errors" + "gvisor.dev/gvisor/pkg/hostarch" +) + +// LINT.IfChange +// Per-thread stack layout: +// +// *------------* +// | guard page | +// |------------| +// | | +// | sysstack | +// | | +// *------------* +// | guard page | +// |------------| +// | | +// | ^ | +// | / \ | +// | | | +// | altstack | +// |------------| +// | sysmsg | +// *------------* +const ( + // PerThreadMemSize is the size of a per-thread memory region. + PerThreadMemSize = 8 * hostarch.PageSize + // GuardSize is the size of an unmapped region which is placed right + // before the signal stack. + GuardSize = hostarch.PageSize + PerThreadPrivateStackOffset = GuardSize + PerThreadPrivateStackSize = 2 * hostarch.PageSize + // PerThreadStackSharedSize is the size of a per-thread stack region. + PerThreadSharedStackSize = 4 * hostarch.PageSize + PerThreadSharedStackOffset = 4 * hostarch.PageSize + // MsgOffsetFromStack is the offset of the Msg structure on + // the thread stack. + MsgOffsetFromSharedStack = PerThreadMemSize - hostarch.PageSize - PerThreadSharedStackOffset +) + +// StackAddrToMsg returns an address of a sysmsg structure. +func StackAddrToMsg(sp uintptr) uintptr { + return sp + MsgOffsetFromSharedStack +} + +// StackAddrToSyshandlerStack returns an address of a syshandler stack. +func StackAddrToSyshandlerStack(sp uintptr) uintptr { + return sp + PerThreadPrivateStackOffset + PerThreadPrivateStackSize +} + +// MsgToStackAddr returns a start address of a stack. +func MsgToStackAddr(msg uintptr) uintptr { + return msg - MsgOffsetFromSharedStack +} + +// State is used to store a state of Msg. +type State uint32 + +// Set atomicaly sets the state value. +func (s *State) Set(state State) { + atomic.StoreUint32((*uint32)(s), uint32(state)) +} + +// Get returns the current state value. +// +//go:nosplit +func (s *State) Get() State { + return State(atomic.LoadUint32((*uint32)(s))) +} + +const ( + // StateNone is the invalid state that isn't used. + StateNone State = iota + // StateDone means that last event has been handled + // and a stub thread can be resumed. + StateDone + // StateEvent means that there is a new event + // which has to be handled by Sentry. + StateEvent + // StateSigact means that the Sentry requests the full state of the + // stub thread. + // + // When a stub thread is in a syscall function call + // (EventTypeSyscallTrap), the Sentry knows only syscall arguments + // without a full set of registers and an FPU state. This is enough to + // handle a system call, but in some cases like signal handling, the + // Sentry needs to know the full state. + StateSigact + // StatePrep means that syshandler started filling the sysmsg struct. + StatePrep +) + +// EventType defines event types. +type EventType uint32 + +// Event types. +const ( + EventTypeNone EventType = iota + EventTypeSyscall + EventTypeFault + // EventTypeSyscallTrap means that the syscall event is triggered from + // a function call (syshandler). + EventTypeSyscallTrap + // EventTypeSyscallCanBePatched means that the syscall can be replaced + // with a function call. + EventTypeSyscallCanBePatched +) + +// Msg contains the current state of the sysmsg thread. +type Msg struct { + // The next batch of fields is used to call the syshandler stub + // function. A system call can be replaced with a function call. When + // a function call is executed, it can't change the current process + // stack, so it needs to save stack and instruction registers, switch + // on its syshandler stack and call the jmp instruction to the syshandler + // address. + // + // Self is a pointer to itself in a process address space. + Self uint64 + // RetAddr is a return address from the syshandler function. + RetAddr uint64 + // Syshandler is an address of the syshandler function. + Syshandler uint64 + // SyshandlerStack is an address of the thread syshandler stack. + SyshandlerStack uint64 + // AppStack is a value of the stack register before calling the syshandler function. + AppStack uint64 + // interrupt is non-zero if there is a postponed interrupt. + interrupt uint32 + // FaultJump is the size of a faulted instruction. + FaultJump int32 + Type EventType + State State + + Signo int32 + Err int32 + Line int32 + debug uint64 + Regs linux.PtraceRegs + fpState uint64 + SignalInfo linux.SignalInfo + // TLS is a pointer to a thread local storage. + // It is is only populated on ARM64. + TLS uint64 + + // The fast path is the mode when a thread is polling msg->state to + // wait for a required state instead of calling FUTEX_WAIT. + // + // If core tagging is supported by the kernel, the Sentry thread, and a + // stub thread share the same cookie and run on two associated + // hyper-threads. The thread which is polling msg->state calls the + // pause instruction, so the second thread gets almost the entire core + // to run its workload. + // + // If core tagging isn't supported, a polling thread calls sched_yield + // to let other processes to run. + + // StubFastPath is set if a stub process uses the fast path to wait for + // events. Only the stub thread can set it before switching to the + // sentry, but the Sentry can clear it. + stubFastPath uint32 + sentryFastPath uint32 + AckedEvents uint32 +} + +// LINT.ThenChange(sysmsg.h) + +// Init initializes the message. +func (m *Msg) Init() { + m.Err = 0 + m.Line = -1 + m.stubFastPath = 0 + m.sentryFastPath = 1 +} + +// StubFastPath returns true if the stub thread in the polling mode. +func (m *Msg) StubFastPath() bool { + return atomic.LoadUint32(&m.stubFastPath) != 0 +} + +// DisableStubFastPath disables the polling mode for the stub thread. +func (m *Msg) DisableStubFastPath() { + atomic.StoreUint32(&m.stubFastPath, 0) +} + +// EnableSentryFastPath enables the polling mode for the Sentry. It has to be +// called before switching controls to the stub process. +func (m *Msg) EnableSentryFastPath() { + m.sentryFastPath = 1 +} + +// DisableSentryFastPath disables the polling mode for the Sentry. +func (m *Msg) DisableSentryFastPath() { + atomic.StoreUint32(&m.sentryFastPath, 0) +} + +// FPUStateOffset returns the offset of a saved FPU state to the msg. +func (m *Msg) FPUStateOffset() (uint64, error) { + offset := m.fpState + if int64(offset) > -MsgOffsetFromSharedStack && int64(offset) < 0 { + return offset, nil + } + return 0, errors.New(errno.EFAULT, fmt.Sprintf("FPU offset has been corrupted: %x", offset)) +} + +func (m *Msg) String() string { + var b strings.Builder + fmt.Fprintf(&b, "sysmsg.Msg{msg: %x type %d", m.Self, m.Type) + fmt.Fprintf(&b, " fault addr %x syscall %d", m.SignalInfo.Addr(), m.SignalInfo.Syscall()) + fmt.Fprintf(&b, " err %x line %d debug %x", m.Err, m.Line, m.debug) + fmt.Fprintf(&b, " ip %x sp %x ret addr %x app stack %x", m.Regs.InstructionPointer(), m.Regs.StackPointer(), m.RetAddr, m.AppStack) + fmt.Fprintf(&b, " signo: %d, siginfo: %+v", m.Signo, m.SignalInfo) + b.WriteString("}") + + return b.String() +} + +// SighandlerBlob contains the compiled code of the sysmsg signal handler. +// +//go:embed sighandler.built-in.bin +var SighandlerBlob []byte diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg.h b/pkg/sentry/platform/systrap/sysmsg/sysmsg.h new file mode 100644 index 000000000..ae5c4f8ca --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg.h @@ -0,0 +1,139 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_H_ +#define THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_H_ + +#include +#include + +#if defined(__x86_64__) +// LINT.IfChange +struct arch_state { + uint32_t xsave_mode; + uint32_t fp_len; + uint32_t fsgsbase; +}; +// LINT.ThenChange(sysmsg_amd64.go) +#else +// LINT.IfChange +struct arch_state {}; +// LINT.ThenChange(sysmsg_arm64.go) +#endif + +// LINT.IfChange +enum { + SYSMSG_STATE_NONE, + SYSMSG_STATE_DONE, + SYSMSG_STATE_EVENT, + SYSMSG_STATE_SIGACT, + SYSMSG_STATE_PREP, +}; + +enum sysmsg_type { + SYSMSG_NONE, + SYSMSG_SYSCALL, + SYSMSG_FAULT, + SYSMSG_SYSCALL_TRAP, + SYSMSG_SYSCALL_NEED_TRAP, + SYSMSG_INTERRUPT, +}; + +struct sysmsg { + struct sysmsg *self; + uint64_t ret_addr; + uint64_t syshandler; + uint64_t syshandler_stack; + uint64_t app_stack; + uint32_t interrupt; + int32_t fault_jump; + uint32_t type; + uint32_t state; + + int32_t signo; + int32_t err; + int32_t err_line; + uint64_t debug; + struct user_regs_struct ptregs; + uint64_t fpstate; + siginfo_t siginfo; + // tls is only populated on ARM64. + uint64_t tls; + uint32_t stub_fast_path; + uint32_t sentry_fast_path; + uint32_t acked_events; +}; + +#ifndef PAGE_SIZE +#define PAGE_SIZE 4096 +#endif +#define PER_THREAD_MEM_SIZE (8 * PAGE_SIZE) +#define GUARD_SIZE (PAGE_SIZE) +#define MSG_OFFSET_FROM_START (PER_THREAD_MEM_SIZE - PAGE_SIZE) + +// LINT.ThenChange(sysmsg.go) + +#define FAULT_OPCODE 0x06 // "push %es" on x32 and invalid opcode on x64. + +#define __stringify_1(x...) #x +#define __stringify(x...) __stringify_1(x) +#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2 * !!(condition)])) + +extern uint64_t __export_pr_sched_core; +extern uint64_t __export_deep_sleep_timeout; +extern struct arch_state __export_arch_state; + +// NOLINTBEGIN(runtime/int) +static void *sysmsg_sp() { + volatile int p; + void *sp = + (struct sysmsg *)(((long)&p) / PER_THREAD_MEM_SIZE * PER_THREAD_MEM_SIZE); + + _Static_assert( + sizeof(struct sysmsg) < (PER_THREAD_MEM_SIZE - MSG_OFFSET_FROM_START), + "The sysmsg structure is too big."); + return sp; +} + +static struct sysmsg *sysmsg_addr(void *sp) { + return (struct sysmsg *)(sp + MSG_OFFSET_FROM_START); +} + +long __syscall(long n, long a1, long a2, long a3, long a4, long a5, long a6); + +struct __kernel_timespec; +long sys_futex(uint32_t *addr, int op, int val, struct __kernel_timespec *tv, + uint32_t *addr2, int val3); + +static void __panic(int err, long line) { + void *sp = sysmsg_sp(); + struct sysmsg *sysmsg = sysmsg_addr(sp); + sysmsg->err = err; + sysmsg->err_line = line; + __atomic_store_n(&sysmsg->state, SYSMSG_STATE_EVENT, __ATOMIC_RELEASE); + sys_futex(&sysmsg->state, FUTEX_WAKE, 1, NULL, NULL, 666); + // crash the stub process. + // + // Normal user processes cannot map addresses lower than vm.mmap_min_addr + // which is usually > 4K. So writing to an address <4K should crash the + // process with a segfault. + *(int *)(line % 4096) = err; +} + +int wait_state(struct sysmsg *sysmsg, uint32_t state); + +#define panic(err) __panic(err, __LINE__) +// NOLINTEND(runtime/int) + +#endif // THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_H_ diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg_amd64.go b/pkg/sentry/platform/systrap/sysmsg/sysmsg_amd64.go new file mode 100644 index 000000000..13283f58b --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg_amd64.go @@ -0,0 +1,68 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sysmsg + +import ( + "fmt" + "strings" + + "gvisor.dev/gvisor/pkg/cpuid" +) + +// ArchState defines variables specific to the architecture being +// used. +type ArchState struct { + xsaveMode uint32 + fpLen uint32 + fsgsbase uint32 +} + +const ( + fxsave = iota + xsave + xsaveopt + xsavec +) + +// Init initializes the arch specific state. +func (s *ArchState) Init() { + fs := cpuid.HostFeatureSet() + + fpLenUint, _ := fs.ExtendedStateSize() + s.fpLen = uint32(fpLenUint) + if fs.UseXsavec() { + s.xsaveMode = xsavec + } else if fs.UseXsaveopt() { + s.xsaveMode = xsaveopt + } else if fs.UseXsave() { + s.xsaveMode = xsave + } else { + s.xsaveMode = fxsave + } + + if fs.UseFSGSBASE() { + s.fsgsbase = 1 + } +} + +func (s *ArchState) String() string { + var b strings.Builder + fmt.Fprintf(&b, "sysmsg.ArchState{") + fmt.Fprintf(&b, " xsaveMode %d", s.xsaveMode) + fmt.Fprintf(&b, " fpLen %d", s.fpLen) + b.WriteString(" }") + + return b.String() +} diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg_arm64.go b/pkg/sentry/platform/systrap/sysmsg/sysmsg_arm64.go new file mode 100644 index 000000000..abbcef5c4 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg_arm64.go @@ -0,0 +1,24 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sysmsg + +// ArchState defines variables specific to the architecture being +// used. +type ArchState struct{} + +// Init initializes the arch specific state. +func (s *ArchState) Init() {} + +func (s *ArchState) String() string { return "sysmsg.ArchState{}" } diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg_lib.c b/pkg/sentry/platform/systrap/sysmsg/sysmsg_lib.c new file mode 100644 index 000000000..4ce72f6f7 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg_lib.c @@ -0,0 +1,127 @@ +// Copyright 2022 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "sysmsg.h" + +// __export_deep_sleep_timeout is the timeout after which the stub thread stops +// polling and fall asleep. +uint64_t __export_deep_sleep_timeout; +uint64_t __export_handshake_timeout; +struct arch_state __export_arch_state; + +// A per-thread memory region is always align to STACK_SIZE. +// *------------* +// | guard page | +// |------------| +// | syshandler | +// | stack | +// | | +// |------------| +// | guard page | +// |------------| +// | | +// | ^ | +// | / \ | +// | | | +// | altstack | +// |------------| +// | sysmsg | +// *------------* + +#if defined(__x86_64__) +static __inline__ unsigned long rdtsc(void) { + unsigned h, l; + __asm__ __volatile__("rdtsc" : "=a"(l), "=d"(h)); + return ((unsigned long)l) | (((unsigned long)h) << 32); +} + +static __inline__ void spinloop(void) { asm("pause"); } +#elif defined(__aarch64__) +static __inline__ unsigned long rdtsc(void) { + long val; + asm volatile("mrs %0, cntvct_el0" : "=r"(val)); + return val; +} + +static __inline__ void spinloop(void) { asm volatile("yield" : : : "memory"); } +#endif + +int wait_state(struct sysmsg *sysmsg, uint32_t state) { + unsigned long handshake_timeout; + uint64_t acked_events_prev; + unsigned long start; + int ret, v, fast_path; + + acked_events_prev = __atomic_load_n(&sysmsg->acked_events, __ATOMIC_SEQ_CST); + // stub_fast_path can be changed non-atomically before we change the state and + // wake up the Sentry. + sysmsg->stub_fast_path = 1; + __atomic_store_n(&sysmsg->state, state, __ATOMIC_SEQ_CST); + + fast_path = __atomic_load_n(&sysmsg->sentry_fast_path, __ATOMIC_SEQ_CST); + if (!fast_path) { + ret = sys_futex(&sysmsg->state, FUTEX_WAKE, 1, NULL, NULL, 0); + if (ret < 0) panic(ret); + } + + v = __atomic_load_n(&sysmsg->state, __ATOMIC_ACQUIRE); + if (v == SYSMSG_STATE_DONE || v == SYSMSG_STATE_SIGACT) goto out; + + handshake_timeout = __export_handshake_timeout; + start = rdtsc(); + while (1) { + v = __atomic_load_n(&sysmsg->state, __ATOMIC_ACQUIRE); + if (v == SYSMSG_STATE_DONE || v == SYSMSG_STATE_SIGACT) goto out; + + // The Sentry can change stub_fast_path to zero if it finds out that the + // user task has to sleep. + fast_path = __atomic_load_n(&sysmsg->stub_fast_path, __ATOMIC_ACQUIRE); + if (fast_path) { + unsigned long delta = rdtsc() - start; + + if (delta > __export_deep_sleep_timeout) { + fast_path = 0; + __atomic_store_n(&sysmsg->stub_fast_path, 0, __ATOMIC_SEQ_CST); + } + if (handshake_timeout != 0) { + if (__atomic_load_n(&sysmsg->acked_events, __ATOMIC_SEQ_CST) != + acked_events_prev) { + handshake_timeout = 0; + } else if (delta > handshake_timeout) { + __syscall(__NR_sched_yield, 0, 0, 0, 0, 0, 0); + handshake_timeout += __export_handshake_timeout; + continue; + } + } + } + + if (fast_path) { + spinloop(); + } else { + sys_futex(&sysmsg->state, FUTEX_WAIT, v, NULL, NULL, 0); + } + } +out: + __atomic_fetch_add(&sysmsg->acked_events, 1, __ATOMIC_SEQ_CST); + return v; +} diff --git a/pkg/sentry/platform/systrap/sysmsg/sysmsg_offsets.h b/pkg/sentry/platform/systrap/sysmsg/sysmsg_offsets.h new file mode 100644 index 000000000..84cfaf342 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg/sysmsg_offsets.h @@ -0,0 +1,41 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_OFFSETS_H_ +#define THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_OFFSETS_H_ + +// FAULT_OPCODE is the opcode of the invalid instruction that is used to replace +// the first byte of the syscall instruction. More details in the description +// for the pkg/sentry/platform/systrap/usertrap package. +#define FAULT_OPCODE 0x06 + +// LINT.IfChange + +// Define offsets in the struct sysmsg to use them in assembly files. +// Each offset has to have BUILD_BUG_ON in sighandler.c. +#define offsetof_sysmsg_self 0x0 +#define offsetof_sysmsg_ret_addr 0x8 +#define offsetof_sysmsg_syshandler 0x10 +#define offsetof_sysmsg_syshandler_stack 0x18 +#define offsetof_sysmsg_app_stack 0x20 +#define offsetof_sysmsg_interrupt 0x28 +#define offsetof_sysmsg_type 0x30 +#define offsetof_sysmsg_state 0x34 + +#define kSYSMSG_SYSCALL 1 +#define kSYSMSG_INTERRUPT 5 + +// LINT.ThenChange(sysmsg.h, sighandler.c) + +#endif // THIRD_PARTY_GVISOR_PKG_SENTRY_PLATFORM_SYSTRAP_SYSMSG_SYSMSG_OFFSETS_H_ diff --git a/pkg/sentry/platform/systrap/sysmsg_thread.go b/pkg/sentry/platform/systrap/sysmsg_thread.go new file mode 100644 index 000000000..64fd2771c --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg_thread.go @@ -0,0 +1,216 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "fmt" + "sync/atomic" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sentry/platform" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +// sysmsgThread describes a sysmsg stub thread which isn't traced +// and communicates with the Sentry via the sysmsg protocol. +// +// This type of thread is used to execute user processes. +type sysmsgThread struct { + // subproc is a link to the subprocess which is used to call native + // system calls and track when a sysmsg thread has to be recreated. + // Look at getSysmsgThread() for more details. + subproc *subprocess + + // thread is a thread identifier. + thread *thread + + // msg is a pointer to a shared sysmsg structure in the Sentry address + // space which is used to communicate with the thread. + msg *sysmsg.Msg + + // context is the last context that ran on this thread. + context *context + + // gsBase contains previous values of gs_base register to follow + // changes, because it's not restored by the kernel from a signal frame. + gsBase uint64 + + // stackRange is a sysmsg stack in the memory file. + stackRange memmap.FileRange + + // fpuStateToMsgOffset is the offset of a thread fpu state relative to sysmsg. + fpuStateToMsgOffset uint64 +} + +// sysmsgStackAddr returns a sysmsg stack address in the thread address space. +func (p *sysmsgThread) sysmsgPerThreadMemAddr() uintptr { + return stubSysmsgStack + sysmsg.PerThreadMemSize*uintptr(p.thread.sysmsgStackID) +} + +func (p *sysmsgThread) destroy() { + t := p.thread + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(t.tgid), uintptr(t.tid), uintptr(unix.SIGKILL)); e != 0 { + panic(fmt.Sprintf("failed to kill the BPF process %d:%d: %v", t.tgid, t.tid, e)) + } + _, err := p.subproc.syscall( + unix.SYS_WAIT4, + arch.SyscallArgument{Value: uintptr(t.tid)}, + arch.SyscallArgument{Value: 0}, // siginfo + arch.SyscallArgument{Value: linux.WALL}, // options + arch.SyscallArgument{Value: 0}, // rusage + ) + if err != nil { + // We never expect this to happen. + panic(fmt.Sprintf("failed to wait %d:%d: %v", t.tid, linux.WEXITED|linux.WALL, err)) + } + stackAddr := p.sysmsgPerThreadMemAddr() + _, err = p.subproc.syscall(unix.SYS_MUNMAP, + arch.SyscallArgument{Value: stackAddr}, + arch.SyscallArgument{Value: sysmsg.PerThreadMemSize}) + if err != nil { + panic(fmt.Sprintf("munmap filed: %v", err)) + } + p.subproc.sysmsgStackPool.Put(p.thread.sysmsgStackID) + p.unmapStackFromSentry() + p.subproc.memoryFile.DecRef(p.stackRange) +} + +// mapStack maps a sysmsg stack into the thread address space. +func (p *sysmsgThread) mapStack(addr uintptr, readOnly bool) error { + prot := uintptr(unix.PROT_READ) + if !readOnly { + prot |= unix.PROT_WRITE + } + _, err := p.thread.syscallIgnoreInterrupt(&p.thread.initRegs, unix.SYS_MMAP, + arch.SyscallArgument{Value: addr}, + arch.SyscallArgument{Value: uintptr(p.stackRange.Length())}, + arch.SyscallArgument{Value: prot}, + arch.SyscallArgument{Value: unix.MAP_SHARED | unix.MAP_FILE | unix.MAP_FIXED}, + arch.SyscallArgument{Value: uintptr(p.subproc.memoryFile.FD())}, + arch.SyscallArgument{Value: uintptr(p.stackRange.Start)}) + return err +} + +// mapPrivateStack maps a private stack into the thread address space. +func (p *sysmsgThread) mapPrivateStack(addr uintptr, size uintptr) error { + prot := uintptr(unix.PROT_READ | unix.PROT_WRITE) + _, err := p.thread.syscallIgnoreInterrupt(&p.thread.initRegs, unix.SYS_MMAP, + arch.SyscallArgument{Value: addr}, + arch.SyscallArgument{Value: size}, + arch.SyscallArgument{Value: prot}, + arch.SyscallArgument{Value: unix.MAP_PRIVATE | unix.MAP_ANONYMOUS | unix.MAP_FIXED}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}) + return err +} + +func (p *sysmsgThread) waitEvent(switchToState sysmsg.State) { + msg := p.msg + wakeup := false + acked := atomic.LoadUint32(&msg.AckedEvents) + if switchToState != sysmsg.StateNone { + msg.State.Set(switchToState) + wakeup = msg.StubFastPath() == false + } else { + acked-- + } + + if errno := futexWaitForState(msg, sysmsg.StateEvent, wakeup, acked); errno != 0 { + panic(fmt.Sprintf("error waiting for state: %v", errno)) + } +} + +// NotifyInterrupt implements interrupt.Receiver.NotifyInterrupt. +func (p *sysmsgThread) NotifyInterrupt() { + t := p.thread + if _, _, e := unix.RawSyscall(unix.SYS_TGKILL, uintptr(t.tgid), uintptr(t.tid), uintptr(platform.SignalInterrupt)); e != 0 { + panic(fmt.Sprintf("failed to interrupt the child process %d: %v", t.tid, e)) + } +} + +func (p *sysmsgThread) Debugf(format string, v ...any) { + if !log.IsLogging(log.Debug) { + return + } + msg := p.msg + postfix := fmt.Sprintf(": %s", msg) + p.thread.Debugf(format+postfix, v...) +} + +func sysmsgThreadRules(stubStart uintptr) []linux.BPFInstruction { + rules := []seccomp.RuleSet{} + rules = appendSysThreadArchSeccompRules(rules) + rules = append(rules, []seccomp.RuleSet{ + // Allow instructions from the sysmsg code stub, which is limited by one page. + { + Rules: seccomp.SyscallRules{ + unix.SYS_FUTEX: { + { + seccomp.GreaterThan(stubStart), + seccomp.EqualTo(linux.FUTEX_WAKE), + seccomp.EqualTo(1), + seccomp.EqualTo(0), + seccomp.EqualTo(0), + seccomp.EqualTo(0), + seccomp.GreaterThan(stubStart), // rip + }, + { + seccomp.GreaterThan(stubStart), + seccomp.EqualTo(linux.FUTEX_WAIT), + seccomp.MatchAny{}, + seccomp.EqualTo(0), + seccomp.EqualTo(0), + seccomp.EqualTo(0), + seccomp.GreaterThan(stubStart), // rip + }, + }, + unix.SYS_RT_SIGRETURN: { + { + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.GreaterThan(stubStart), // rip + }, + }, + unix.SYS_SCHED_YIELD: { + { + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.GreaterThan(stubStart), // rip + }, + }, + }, + Action: linux.SECCOMP_RET_ALLOW, + }, + }...) + instrs, err := seccomp.BuildProgram(rules, linux.SECCOMP_RET_TRAP, linux.SECCOMP_RET_TRAP) + if err != nil { + panic(fmt.Sprintf("failed to build rules for sysmsg threads: %v", err)) + } + + return instrs +} diff --git a/pkg/sentry/platform/systrap/sysmsg_thread_amd64.go b/pkg/sentry/platform/systrap/sysmsg_thread_amd64.go new file mode 100644 index 000000000..13aa17201 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg_thread_amd64.go @@ -0,0 +1,61 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" +) + +func appendSysThreadArchSeccompRules(rules []seccomp.RuleSet) []seccomp.RuleSet { + return append(rules, []seccomp.RuleSet{ + { + // Rules for trapping vsyscall access. + Rules: seccomp.SyscallRules{ + unix.SYS_GETTIMEOFDAY: {}, + unix.SYS_TIME: {}, + unix.SYS_GETCPU: {}, // SYS_GETCPU was not defined in package syscall on amd64. + }, + Action: linux.SECCOMP_RET_TRAP, + Vsyscall: true, + }, + { + Rules: seccomp.SyscallRules{ + unix.SYS_ARCH_PRCTL: { + { + seccomp.EqualTo(linux.ARCH_SET_FS), + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.GreaterThan(stubStart), // rip + }, + { + seccomp.EqualTo(linux.ARCH_GET_FS), + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.GreaterThan(stubStart), // rip + }, + }, + }, + Action: linux.SECCOMP_RET_ALLOW, + }, + }...) +} diff --git a/pkg/sentry/platform/systrap/sysmsg_thread_arm64.go b/pkg/sentry/platform/systrap/sysmsg_thread_arm64.go new file mode 100644 index 000000000..76d43e48a --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg_thread_arm64.go @@ -0,0 +1,21 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import "gvisor.dev/gvisor/pkg/seccomp" + +func appendSysThreadArchSeccompRules(rules []seccomp.RuleSet) []seccomp.RuleSet { + return rules +} diff --git a/pkg/sentry/platform/systrap/sysmsg_thread_unsafe.go b/pkg/sentry/platform/systrap/sysmsg_thread_unsafe.go new file mode 100644 index 000000000..094b55b88 --- /dev/null +++ b/pkg/sentry/platform/systrap/sysmsg_thread_unsafe.go @@ -0,0 +1,190 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "fmt" + "runtime" + "sync/atomic" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" +) + +func (p *sysmsgThread) unmapStackFromSentry() { + _, _, errno := unix.RawSyscall(unix.SYS_MUNMAP, sysmsg.MsgToStackAddr(uintptr(unsafe.Pointer(p.msg))), sysmsg.PerThreadSharedStackSize, 0) + if errno != 0 { + panic("failed to unmap: " + errno.Error()) + } +} + +func (p *sysmsgThread) setMsg(addr uintptr) { + // add is always from the stub mapping which is mapped once and never + // moved, so it is safe to use unsafe.Pointer here. + p.msg = (*sysmsg.Msg)(unsafe.Pointer(addr)) +} + +func (p *sysmsgThread) init(sentryAddr, guestAddr uintptr) { + t := p.thread + + // Set the parent death signal to SIGKILL. + _, err := t.syscallIgnoreInterrupt(&t.initRegs, unix.SYS_PRCTL, + arch.SyscallArgument{Value: linux.PR_SET_PDEATHSIG}, + arch.SyscallArgument{Value: uintptr(unix.SIGKILL)}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + ) + if err != nil { + panic(fmt.Sprintf("prctl: %v", err)) + } + + // Set the sysmsg signal stack. + // + // sentryAddr is from the stub mapping which is mapped once and never + // moved, so it is safe to use unsafe.Pointer here. + alt := (*linux.SignalStack)(unsafe.Pointer(sentryAddr)) + *alt = linux.SignalStack{} + alt.Addr = uint64(guestAddr) + alt.Size = uint64(sysmsg.MsgOffsetFromSharedStack) + _, err = t.syscallIgnoreInterrupt(&t.initRegs, unix.SYS_SIGALTSTACK, + arch.SyscallArgument{Value: guestAddr}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + arch.SyscallArgument{Value: 0}, + ) + if err != nil { + panic(fmt.Sprintf("sigaltstack: %v", err)) + } +} + +func futexWake(msg *sysmsg.Msg) syscall.Errno { + _, _, e := unix.RawSyscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(&msg.State)), linux.FUTEX_WAKE, 1, 0, 0, 0) + return e +} + +//go:linkname cputicks runtime.cputicks +func cputicks() int64 + +// spinloop is implemented in assembly. +func spinloop() + +//go:linkname entersyscall runtime.entersyscall +func entersyscall() + +//go:linkname exitsyscall runtime.exitsyscall +func exitsyscall() + +// deep_sleep_timeout is the timeout after which we stops polling and fall asleep. +// +// The value is 40µs for 2GHz CPU. This timeout matches the sentry<->stub round +// trip in the pure deep sleep case. +const deepSleepTimeout = uint64(80000) +const handshakeTimeout = uint64(1000) + +func futexWaitForState(msg *sysmsg.Msg, state sysmsg.State, wakeup bool, acked uint32) syscall.Errno { + slowPath := false + errno := syscall.Errno(0) + start := cputicks() + htimeout := handshakeTimeout + handshake := false + for { + curState := msg.State.Get() + if curState == state { + break + } + if wakeup { + if errno = futexWake(msg); errno != 0 { + break + } + wakeup = false + continue + } + + if !slowPath { + delta := uint64(cputicks() - start) + if delta > deepSleepTimeout { + msg.DisableSentryFastPath() + slowPath = true + continue + } + + if !handshake { + if acked != atomic.LoadUint32(&msg.AckedEvents) { + handshake = true + continue + } + if delta > htimeout { + htimeout += handshakeTimeout + runtime.Gosched() + } + } + } + + if slowPath { + _, _, errno = unix.Syscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(&msg.State)), + linux.FUTEX_WAIT, uintptr(curState), 0, 0, 0) + if errno != 0 && errno != unix.EAGAIN && errno != unix.EINTR { + break + } + errno = 0 + } else { + spinloop() + } + } + atomic.AddUint32(&msg.AckedEvents, 1) + return errno +} + +// sysmsgSigactions installs signal handles for signals which can be triggered +// by stubProcess and have to be handled by Sentry. +// +// It is called in a child process after fork(), so the race instrumentation +// has to be disabled. +// +//go:nosplit +//go:norace +func sysmsgSigactions(stubSysmsgStart uintptr) unix.Errno { + act := linux.SigAction{ + Handler: uint64(stubSysmsgStart) + uint64(sysmsg.Sighandler_blob_offset____export_sighandler), + Flags: linux.SA_ONSTACK | linux.SA_RESTORER | linux.SA_SIGINFO, + Restorer: uint64(stubSysmsgStart) + uint64(sysmsg.Sighandler_blob_offset____export_restore_rt), + Mask: 1<<(linux.SIGCHLD-1) | 1<<(linux.SIGSYS-1), + } + + for _, s := range []unix.Signal{ + unix.SIGSYS, + unix.SIGBUS, + unix.SIGFPE, + unix.SIGILL, + unix.SIGCHLD, + unix.SIGTRAP, + unix.SIGSEGV, + } { + _, _, errno := unix.RawSyscall6(unix.SYS_RT_SIGACTION, uintptr(s), uintptr(unsafe.Pointer(&act)), 0, 8, 0, 0) + if errno != 0 { + return errno + } + } + + return 0 +} diff --git a/pkg/sentry/platform/systrap/systrap.go b/pkg/sentry/platform/systrap/systrap.go new file mode 100644 index 000000000..c40cf21c0 --- /dev/null +++ b/pkg/sentry/platform/systrap/systrap.go @@ -0,0 +1,404 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package systrap provides a seccomp-based implementation of the platform +// interface. +// +// In a nutshell, it works as follows: +// +// The creation of a new address space creates a new child processes. +// +// The creation of a new stub thread creates a new system thread with a +// specified address space. To initialize this thread, the following action +// will be done: +// - install a signal stack which is shared with the Sentry. +// - install a signal handler for SYS, BUS, FPE, CHLD, TRAP, SEGV signals. +// This signal handler is a key part of the systrap platform. Any stub event +// which has to be handled in a privilege mode (by the Sentry) triggers one of +// previous signals. The signal handler is running on the separate stack which +// is shared with the Sentry. There is the sysmsg structure to synchronize the +// Sentry and a stub thread. +// - install seccomp filters to trap user system calls. +// - send a fake SIGSEGV to stop the thread in the signal handler. +// +// A context is just a collection of temporary variables. Calling Switch on a +// context does the following: +// +// Set up proper registers and an FPU state on a stub signal frame. +// Wake up a stub thread by changing sysmsg->stage and calling FUTEX_WAKE. +// Wait for new stub event by polling sysmsg->stage. +// +// Lock order: +// +// subprocessPool.mu +// subprocess.mu +// context.mu +// +// +checkalignedignore +package systrap + +import ( + "fmt" + "os" + "sync" + + "gvisor.dev/gvisor/pkg/abi/linux" + pkgcontext "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/cpuid" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/memutil" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/pgalloc" + "gvisor.dev/gvisor/pkg/sentry/platform" + "gvisor.dev/gvisor/pkg/sentry/platform/interrupt" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/usertrap" +) + +var ( + // stubStart is the link address for our stub, and determines the + // maximum user address. This is valid only after a call to stubInit. + // + // We attempt to link the stub here, and adjust downward as needed. + stubStart uintptr = stubInitAddress + + stubInitProcess uintptr + + stubSysmsgStack uintptr + stubSysmsgStart uintptr + stubSysmsgEnd uintptr + // The memory blob with precompiled seccomp rules. + stubSysmsgRules uintptr + stubSysmsgRulesLen uintptr + + // stubROMapEnd is the end address of the read-only stub region that + // contains the code and precompiled seccomp rules. + stubROMapEnd uintptr + + // stubEnd is the first byte past the end of the stub, as with + // stubStart this is valid only after a call to stubInit. + stubEnd uintptr + + // stubInitialized controls one-time stub initialization. + stubInitialized sync.Once +) + +// context is an implementation of the platform context. +type context struct { + // signalInfo is the signal info, if and when a signal is received. + signalInfo linux.SignalInfo + + // interrupt is the interrupt context. + interrupt interrupt.Forwarder + + // subprocess is the current subprocess used to execute the context. + // It is only updated in Switch, and used in Release, so only on the task + // goroutine. + subprocess *subprocess + + // mu protects the following fields. + mu sync.Mutex + + // If lastFaultSP is non-nil, the last context switch was due to a fault + // received while executing lastFaultSP. Only context.Switch may set + // lastFaultSP to a non-nil value. + lastFaultSP *subprocess + + // lastFaultAddr is the last faulting address; this is only meaningful if + // lastFaultSP is non-nil. + lastFaultAddr hostarch.Addr + + // lastFaultIP is the address of the last faulting instruction; + // this is also only meaningful if lastFaultSP is non-nil. + lastFaultIP hostarch.Addr + + // sysmsgThread is a sysmsg thread descriptor which is used to execute + // application code. + sysmsgThread *sysmsgThread + + // fpLen is the size of the floating point context. + fpLen int + + // needRestoreFPState indicates that the FPU state has been changed by + // the Sentry and has to be updated on the stub thread. + needRestoreFPState bool + + // needToPullFullState indicates that the Sentry doesn't have a full + // state of the thread. + needToPullFullState bool +} + +// PullFullState implements platform.Context.PullFullState. +func (c *context) PullFullState(as platform.AddressSpace, ac *arch.Context64) error { + if !c.needToPullFullState { + return nil + } + s := as.(*subprocess) + if err := s.PullFullState(c, ac); err != nil { + return err + } + c.needToPullFullState = false + return nil +} + +// FullStateChanged implements platform.Context.FullStateChanged. +func (c *context) FullStateChanged() { + c.needRestoreFPState = true + c.needToPullFullState = false +} + +// Switch runs the provided context in the given address space. +func (c *context) Switch(ctx pkgcontext.Context, mm platform.MemoryManager, ac *arch.Context64, cpu int32) (*linux.SignalInfo, hostarch.AccessType, error) { + c.needToPullFullState = true + + as := mm.AddressSpace() + s := as.(*subprocess) + + if s != c.subprocess { + c.subprocess.unregisterContext(c) + c.subprocess = s + s.numContexts.Add(1) + } +restart: + isSyscall, needPatch, err := s.switchToApp(c, ac) + if err != nil { + return nil, hostarch.NoAccess, err + } + if needPatch { + restart, _ := s.usertrap.PatchSyscall(ctx, ac, mm) + if restart { + goto restart + } + } + if !isSyscall && linux.Signal(c.signalInfo.Signo) == linux.SIGILL { + err := s.usertrap.HandleFault(ctx, ac, mm) + if err == usertrap.ErrFaultSyscall { + isSyscall = true + } else if err == usertrap.ErrFaultRestart { + goto restart + } else if err != nil { + ctx.Warningf("usertrap.HandleFault failed: %v", err) + } + } + var ( + faultSP *subprocess + faultAddr hostarch.Addr + faultIP hostarch.Addr + ) + if !isSyscall && linux.Signal(c.signalInfo.Signo) == linux.SIGSEGV { + faultSP = s + faultAddr = hostarch.Addr(c.signalInfo.Addr()) + faultIP = hostarch.Addr(ac.IP()) + } + + // Update the context to reflect the outcome of this context switch. + c.mu.Lock() + lastFaultSP := c.lastFaultSP + lastFaultAddr := c.lastFaultAddr + lastFaultIP := c.lastFaultIP + // At this point, c may not yet be in s.contexts, so c.lastFaultSP won't be + // updated by s.Unmap(). This is fine; we only need to synchronize with + // calls to s.Unmap() that occur after the handling of this fault. + c.lastFaultSP = faultSP + c.lastFaultAddr = faultAddr + c.lastFaultIP = faultIP + c.mu.Unlock() + + // Update subprocesses to reflect the outcome of this context switch. + if lastFaultSP != faultSP { + if lastFaultSP != nil { + lastFaultSP.mu.Lock() + delete(lastFaultSP.contexts, c) + lastFaultSP.mu.Unlock() + } + if faultSP != nil { + faultSP.mu.Lock() + faultSP.contexts[c] = struct{}{} + faultSP.mu.Unlock() + } + } + + if isSyscall { + return nil, hostarch.NoAccess, nil + } + + si := c.signalInfo + if faultSP == nil { + // Non-fault signal. + return &si, hostarch.NoAccess, platform.ErrContextSignal + } + + // See if this can be handled as a CPUID exception. + if linux.Signal(si.Signo) == linux.SIGSEGV && platform.TryCPUIDEmulate(ctx, mm, ac) { + goto restart + } + + // Got a page fault. Ideally, we'd get real fault type here, but ptrace + // doesn't expose this information. Instead, we use a simple heuristic: + // + // It was an instruction fault iff the faulting addr == instruction + // pointer. + // + // It was a write fault if the fault is immediately repeated. + at := hostarch.Read + if faultAddr == faultIP { + at.Execute = true + } + if lastFaultSP == faultSP && + lastFaultAddr == faultAddr && + lastFaultIP == faultIP { + at.Write = true + } + + // Handle as a signal. + return &si, at, platform.ErrContextSignal +} + +// Interrupt interrupts the running guest application associated with this context. +func (c *context) Interrupt() { + c.interrupt.NotifyInterrupt() +} + +// Release releases all platform resources used by the context. +func (c *context) Release() { + if c.sysmsgThread != nil { + c.sysmsgThread.destroy() + } + c.subprocess.unregisterContext(c) +} + +// PrepareSleep implements platform.Context.platform.PrepareSleep. +func (c *context) PrepareSleep() { + if c.sysmsgThread != nil { + c.sysmsgThread.msg.DisableStubFastPath() + } +} + +// Systrap represents a collection of seccomp subprocesses. +type Systrap struct { + platform.NoCPUPreemptionDetection + platform.UseHostGlobalMemoryBarrier + + // memoryFile is used to create a stub sysmsg stack + // which is shared with the Sentry. + memoryFile *pgalloc.MemoryFile +} + +// MinUserAddress implements platform.MinUserAddress. +func (*Systrap) MinUserAddress() hostarch.Addr { + return platform.SystemMMapMinAddr() +} + +// New returns a new seccomp-based implementation of the platform interface. +func New() (*Systrap, error) { + mf, err := createMemoryFile() + if err != nil { + return nil, err + } + + stubInitialized.Do(func() { + // Initialize the stub. + stubInit() + + // Create the source process for the global pool. This must be + // done before initializing any other processes. + source, err := newSubprocess(createStub, mf) + if err != nil { + // Should never happen. + panic("unable to initialize systrap source: " + err.Error()) + } + + globalPool.source = source + }) + + return &Systrap{memoryFile: mf}, nil +} + +// SupportsAddressSpaceIO implements platform.Platform.SupportsAddressSpaceIO. +func (*Systrap) SupportsAddressSpaceIO() bool { + return false +} + +// CooperativelySchedulesAddressSpace implements platform.Platform.CooperativelySchedulesAddressSpace. +func (*Systrap) CooperativelySchedulesAddressSpace() bool { + return false +} + +// MapUnit implements platform.Platform.MapUnit. +func (*Systrap) MapUnit() uint64 { + // The host kernel manages page tables and arbitrary-sized mappings + // have effectively the same cost. + return 0 +} + +// MaxUserAddress returns the first address that may not be used by user +// applications. +func (*Systrap) MaxUserAddress() hostarch.Addr { + return hostarch.Addr(maxStubUserAddress) +} + +// NewAddressSpace returns a new subprocess. +func (p *Systrap) NewAddressSpace(any) (platform.AddressSpace, <-chan struct{}, error) { + as, err := newSubprocess(globalPool.source.createStub, p.memoryFile) + return as, nil, err +} + +// NewContext returns an interruptible context. +func (*Systrap) NewContext(ctx pkgcontext.Context) platform.Context { + fs := cpuid.FromContext(ctx) + fpLen, _ := fs.ExtendedStateSize() + return &context{ + fpLen: int(fpLen), + needRestoreFPState: true, + needToPullFullState: false, + } +} + +type constructor struct{} + +func (*constructor) New(_ *os.File) (platform.Platform, error) { + return New() +} + +func (*constructor) OpenDevice(_ string) (*os.File, error) { + return nil, nil +} + +// Flags implements platform.Constructor.Flags(). +func (*constructor) Requirements() platform.Requirements { + // TODO(b/75837838): Also set a new PID namespace so that we limit + // access to other host processes. + return platform.Requirements{ + RequiresCapSysPtrace: true, + RequiresCurrentPIDNS: true, + } +} + +func init() { + platform.Register("systrap", &constructor{}) +} + +func createMemoryFile() (*pgalloc.MemoryFile, error) { + const memfileName = "systrap-memory" + fd, err := memutil.CreateMemFD(memfileName, 0) + if err != nil { + return nil, fmt.Errorf("error creating memfd: %v", err) + } + memfile := os.NewFile(uintptr(fd), memfileName) + mf, err := pgalloc.NewMemoryFile(memfile, pgalloc.MemoryFileOpts{}) + if err != nil { + memfile.Close() + return nil, fmt.Errorf("error creating pgalloc.MemoryFile: %v", err) + } + return mf, nil +} diff --git a/pkg/sentry/platform/systrap/systrap_amd64.go b/pkg/sentry/platform/systrap/systrap_amd64.go new file mode 100644 index 000000000..eac9dfc1c --- /dev/null +++ b/pkg/sentry/platform/systrap/systrap_amd64.go @@ -0,0 +1,37 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +func stackPointer(r *arch.Registers) uintptr { + return uintptr(r.Rsp) +} + +// x86 use the fs_base register to store the TLS pointer which can be +// get/set in "func (t *thread) get/setRegs(regs *arch.Registers)". +// So both of the get/setTLS() operations are noop here. + +// getTLS gets the thread local storage register. +func (t *thread) getTLS(tls *uint64) error { + return nil +} + +// setTLS sets the thread local storage register. +func (t *thread) setTLS(tls *uint64) error { + return nil +} diff --git a/pkg/sentry/platform/systrap/systrap_arm64.go b/pkg/sentry/platform/systrap/systrap_arm64.go new file mode 100644 index 000000000..a5895ea2d --- /dev/null +++ b/pkg/sentry/platform/systrap/systrap_arm64.go @@ -0,0 +1,23 @@ +// Copyright 2019 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +func stackPointer(r *arch.Registers) uintptr { + return uintptr(r.Sp) +} diff --git a/pkg/sentry/platform/systrap/systrap_arm64_unsafe.go b/pkg/sentry/platform/systrap/systrap_arm64_unsafe.go new file mode 100644 index 000000000..eead7768d --- /dev/null +++ b/pkg/sentry/platform/systrap/systrap_arm64_unsafe.go @@ -0,0 +1,63 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package systrap + +import ( + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" +) + +// getTLS gets the thread local storage register. +func (t *thread) getTLS(tls *uint64) error { + iovec := unix.Iovec{ + Base: (*byte)(unsafe.Pointer(tls)), + Len: uint64(unsafe.Sizeof(*tls)), + } + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_GETREGSET, + uintptr(t.tid), + linux.NT_ARM_TLS, + uintptr(unsafe.Pointer(&iovec)), + 0, 0) + if errno != 0 { + return errno + } + return nil +} + +// setTLS sets the thread local storage register. +func (t *thread) setTLS(tls *uint64) error { + iovec := unix.Iovec{ + Base: (*byte)(unsafe.Pointer(tls)), + Len: uint64(unsafe.Sizeof(*tls)), + } + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_SETREGSET, + uintptr(t.tid), + linux.NT_ARM_TLS, + uintptr(unsafe.Pointer(&iovec)), + 0, 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/pkg/sentry/platform/systrap/systrap_unsafe.go b/pkg/sentry/platform/systrap/systrap_unsafe.go new file mode 100644 index 000000000..5fd679da5 --- /dev/null +++ b/pkg/sentry/platform/systrap/systrap_unsafe.go @@ -0,0 +1,139 @@ +// Copyright 2018 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package systrap + +import ( + "unsafe" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/sentry/arch" +) + +// getRegs gets the general purpose register set. +func (t *thread) getRegs(regs *arch.Registers) error { + iovec := unix.Iovec{ + Base: (*byte)(unsafe.Pointer(regs)), + Len: uint64(unsafe.Sizeof(*regs)), + } + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_GETREGSET, + uintptr(t.tid), + linux.NT_PRSTATUS, + uintptr(unsafe.Pointer(&iovec)), + 0, 0) + if errno != 0 { + return errno + } + return nil +} + +// setRegs sets the general purpose register set. +func (t *thread) setRegs(regs *arch.Registers) error { + iovec := unix.Iovec{ + Base: (*byte)(unsafe.Pointer(regs)), + Len: uint64(unsafe.Sizeof(*regs)), + } + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_SETREGSET, + uintptr(t.tid), + linux.NT_PRSTATUS, + uintptr(unsafe.Pointer(&iovec)), + 0, 0) + if errno != 0 { + return errno + } + return nil +} + +// getSignalInfo retrieves information about the signal that caused the stop. +func (t *thread) getSignalInfo(si *linux.SignalInfo) error { + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_GETSIGINFO, + uintptr(t.tid), + 0, + uintptr(unsafe.Pointer(si)), + 0, 0) + if errno != 0 { + return errno + } + return nil +} + +// clone creates a new sysmsg thread from this one. +// +// The returned thread will be stopped and available for any system thread to +// call attach on it. +// +// Precondition: the OS thread must be locked and own t. +func (t *thread) clone() (*thread, error) { + r, ok := hostarch.Addr(stackPointer(&t.initRegs)).RoundUp() + if !ok { + return nil, unix.EINVAL + } + var flags uintptr + // Create a sysmsg thread. + // + // CLONE_THREAD isn't set, because a stub process has SIGSTOP + // in its queue. A sysmsg thread will not be traced by ptrace, + // so it will be stopped immediately if it will share signal + // queue with its stub process. + flags = uintptr( + unix.CLONE_FILES | + unix.CLONE_FS | + unix.CLONE_PTRACE | + unix.CLONE_VM) + rval, err := t.syscallIgnoreInterrupt( + &t.initRegs, + unix.SYS_CLONE, + arch.SyscallArgument{Value: flags}, + // The stack pointer is just made up, but we have it be + // something sensible so the kernel doesn't think we're + // up to no good. Which we are. + arch.SyscallArgument{Value: uintptr(r)}, + arch.SyscallArgument{}, + arch.SyscallArgument{}, + // We use these registers initially, but really they + // could be anything. We're going to stop immediately. + arch.SyscallArgument{Value: uintptr(unsafe.Pointer(&t.initRegs))}) + if err != nil { + return nil, err + } + + return &thread{ + tgid: int32(rval), + tid: int32(rval), + }, nil +} + +// getEventMessage retrieves a message about the ptrace event that just happened. +func (t *thread) getEventMessage() (uintptr, error) { + var msg uintptr + _, _, errno := unix.RawSyscall6( + unix.SYS_PTRACE, + unix.PTRACE_GETEVENTMSG, + uintptr(t.tid), + 0, + uintptr(unsafe.Pointer(&msg)), + 0, 0) + if errno != 0 { + return msg, errno + } + return msg, nil +} diff --git a/pkg/sentry/platform/systrap/usertrap/BUILD b/pkg/sentry/platform/systrap/usertrap/BUILD new file mode 100644 index 000000000..731bef64e --- /dev/null +++ b/pkg/sentry/platform/systrap/usertrap/BUILD @@ -0,0 +1,27 @@ +load("//tools:defs.bzl", "go_library") + +package(licenses = ["notice"]) + +go_library( + name = "usertrap", + srcs = [ + "usertrap.go", + "usertrap_amd64.go", + "usertrap_amd64_unsafe.go", + "usertrap_arm64.go", + ], + marshal = True, + visibility = ["//pkg/sentry/platform/systrap:__pkg__"], + deps = [ + "//pkg/context", + "//pkg/hostarch", + "//pkg/marshal/primitive", + "//pkg/sentry/arch", + "//pkg/sentry/kernel", + "//pkg/sentry/memmap", + "//pkg/sentry/platform/systrap/sysmsg", + "//pkg/sync", + "//pkg/usermem", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/pkg/sentry/platform/systrap/usertrap/usertrap.go b/pkg/sentry/platform/systrap/usertrap/usertrap.go new file mode 100644 index 000000000..fb5b41e8b --- /dev/null +++ b/pkg/sentry/platform/systrap/usertrap/usertrap.go @@ -0,0 +1,49 @@ +// Copyright 2021 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package usertrap implements the library to replace syscall instructions with +// function calls. +// +// The most often used pattern of performing a system call is a sequence of two +// instruction: mov sysno, %eax; syscall. The size of the mov instruction is 5 +// bytes and the size of the syscall instruction is 2 bytes. These two +// instruction can be replaced with a single jmp instruction with an absolute +// address below 2 gigabytes. +// +// Here is a few tricks: +// - The GS register is used to access a per-thread memory. +// - The syscall instruction is replaced with the "jmp *%ds:offset" instruction. +// On x86_64, ds is always zero. offset is a 32-bit signed integer. This +// means that a service mapping for a table with syscall trampolines has to +// be mapped below 2GB. +// - We can't touch a process stack, so we have to use the jmp instruction +// instead of callq and generate a new function call for each replaced +// instruction. Each trampoline contains a syscall number and an return +// address. +// - The address for the syscall table is set so that the syscall instruction +// is replaced on an invalid instruction. This allows us to handle races +// when two threads are executing the same syscall concurrently. And this +// allows us to restart a syscall if it has been interrupted by a signal. +// +// +checkalignedignore +package usertrap + +import "fmt" + +var ( + // ErrFaultRestart indicates that the current stub thread has to be restarted. + ErrFaultRestart = fmt.Errorf("need to restart stub thread") + // ErrFaultSyscall indicates that the current fault has to be handled as a system call. + ErrFaultSyscall = fmt.Errorf("need to handle as syscall") +) diff --git a/pkg/sentry/platform/systrap/usertrap/usertrap_amd64.go b/pkg/sentry/platform/systrap/usertrap/usertrap_amd64.go new file mode 100644 index 000000000..3b3396d76 --- /dev/null +++ b/pkg/sentry/platform/systrap/usertrap/usertrap_amd64.go @@ -0,0 +1,349 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package usertrap + +import ( + "encoding/binary" + "fmt" + "math/rand" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sync" + "gvisor.dev/gvisor/pkg/usermem" +) + +// trapNR is the maximum number of traps what can fit in the trap table. +const trapNR = 256 + +// trapSize is the size of one trap. +const trapSize = 80 + +var ( + // jmpInst is the binary code of "jmp *addr". + jmpInst = [7]byte{0xff, 0x24, 0x25, 0, 0, 0, 0} + jmpInstOpcodeLen = 3 + // faultInst is the single byte invalid instruction. + faultInst = [1]byte{0x6} + // faultInstOffset is the offset of the syscall instruction. + faultInstOffset = uintptr(5) +) + +type memoryManager interface { + usermem.IO + MMap(ctx context.Context, opts memmap.MMapOpts) (hostarch.Addr, error) + FindVMAByName(ar hostarch.AddrRange, hint string) (hostarch.Addr, uint64, error) +} + +// State represents the current state of the trap table. +// +// +stateify savable +type State struct { + mu sync.RWMutex `state:"nosave"` + nextTrap uint32 + tableAddr hostarch.Addr +} + +// New returns the new state structure. +func New() *State { + return &State{} +} + +// +marshal +type header struct { + nextTrap uint32 +} + +func (s *State) trapAddr(trap uint32) hostarch.Addr { + return s.tableAddr + hostarch.Addr(trapSize*trap) +} + +// newTrapLocked allocates a new trap entry. +// +// Preconditions: s.mu must be locked. +func (s *State) newTrapLocked(ctx context.Context, mm memoryManager) (hostarch.Addr, error) { + var hdr header + task := kernel.TaskFromContext(ctx) + if task == nil { + return 0, fmt.Errorf("no task found") + } + + // s.nextTrap is zero if it isn't initialized. Here are three cases + // when this can happen: + // * A usertrap vma has not been mapped yet. + // * The address space has been forked. + // * The address space has been restored. + // nextTrap is saved on the usertrap vma to handle the third and second + // cases. + if s.nextTrap == 0 { + addr, off, err := mm.FindVMAByName(trapTableAddrRange, tableHint) + if off != 0 { + return 0, fmt.Errorf("the usertrap vma has been overmounted") + } + if err != nil { + // The usertrap table has not been mapped yet. + addr := hostarch.Addr(rand.Int63n(int64(trapTableAddrRange.Length()-trapTableSize))).RoundDown() + trapTableAddrRange.Start + ctx.Debugf("Map a usertrap vma at %x", addr) + if err := loadUsertrap(ctx, mm, addr); err != nil { + return 0, err + } + // The first cell in the table is used to save an index of a + // next unused trap. + s.nextTrap = 1 + s.tableAddr = addr + } else if _, err := hdr.CopyIn(task.OwnCopyContext(usermem.IOOpts{AddressSpaceActive: false}), addr); err != nil { + return 0, err + } else { + // Read an index of a next unused trap. + s.nextTrap = hdr.nextTrap + s.tableAddr = addr + } + } + ctx.Debugf("Allocate a new trap: %p %d", s, s.nextTrap) + if s.nextTrap >= trapNR { + ctx.Warningf("No space in the trap table") + return 0, fmt.Errorf("no space in the trap table") + } + trap := s.nextTrap + s.nextTrap++ + + // An entire trap has to be on the same page to avoid memory faults. + addr := s.trapAddr(trap) + if addr/hostarch.PageSize != (addr+trapSize)/hostarch.PageSize { + trap = s.nextTrap + s.nextTrap++ + } + hdr = header{ + nextTrap: s.nextTrap, + } + if _, err := hdr.CopyOut(task.OwnCopyContext(usermem.IOOpts{IgnorePermissions: true}), s.tableAddr); err != nil { + return 0, err + } + return s.trapAddr(trap), nil +} + +// trapTableAddrRange is the range where a trap table can be placed. +// +// The value has to be below 2GB and the high two bytes has to be an invalid +// instruction. In case of 0x60000, the high two bytes is 0x6. This is "push +// es" in x86 and the bad instruction on x64. +var trapTableAddrRange = hostarch.AddrRange{Start: 0x60000, End: 0x70000} + +const ( + trapTableSize = hostarch.Addr(trapNR * trapSize) + + tableHint = "[usertrap]" +) + +// LoadUsertrap maps the usertrap table into the address space. +func loadUsertrap(ctx context.Context, mm memoryManager, addr hostarch.Addr) error { + size, _ := hostarch.Addr(trapTableSize).RoundUp() + // Force is true because Addr is below MinUserAddress. + _, err := mm.MMap(ctx, memmap.MMapOpts{ + Force: true, + Unmap: true, + Fixed: true, + Addr: addr, + Length: uint64(size), + Private: true, + Hint: tableHint, + MLockMode: memmap.MLockEager, + Perms: hostarch.AccessType{ + Write: false, + Read: true, + Execute: true, + }, + MaxPerms: hostarch.AccessType{ + Write: true, + Read: true, + Execute: true, + }, + }) + if err != nil { + return err + } + + return nil +} + +// PatchSyscall changes the syscall instruction into a function call. +// +// Returns true if the thread has to be restarted. +func (s *State) PatchSyscall(ctx context.Context, ac *arch.Context64, mm memoryManager) (bool, error) { + task := kernel.TaskFromContext(ctx) + if task == nil { + return false, fmt.Errorf("no task found") + } + + s.mu.Lock() + defer s.mu.Unlock() + + sysno := ac.SyscallNo() + patchAddr := ac.IP() - uintptr(len(jmpInst)) + + prevCode := make([]uint8, len(jmpInst)) + if _, err := primitive.CopyUint8SliceIn(task.OwnCopyContext(usermem.IOOpts{AddressSpaceActive: false}), hostarch.Addr(patchAddr), prevCode); err != nil { + return false, err + } + + // Check that another thread has not patched this syscall yet. + // 0xb8 is the first byte of "mov sysno, %eax". + if prevCode[0] == uint8(0xb8) { + ctx.Debugf("Found the pattern at ip %x:sysno %d", patchAddr, sysno) + + trapAddr, err := s.addTrapLocked(ctx, ac, mm, uint32(sysno)) + if trapAddr == 0 || err != nil { + ctx.Warningf("Failed to add a new trap: %v", err) + return false, nil + } + + // Replace "mov sysno, %eax; syscall" with "jmp trapAddr". + newCode := make([]uint8, len(jmpInst)) + copy(newCode[:jmpInstOpcodeLen], jmpInst[:jmpInstOpcodeLen]) + binary.LittleEndian.PutUint32(newCode[jmpInstOpcodeLen:], uint32(trapAddr)) + + ctx.Debugf("Apply the binary patch addr %x trap addr %x (%v -> %v)", patchAddr, trapAddr, prevCode, newCode) + + ignorePermContext := task.OwnCopyContext(usermem.IOOpts{IgnorePermissions: true}) + + // The patch can't be applied atomically, so we need to + // guarantee that in each moment other threads will read a + // valid set of instructions, detect any inconsistent states + // and restart the patched code if so. + // + // A subtle aspect is the address at which the user trap table + // is always mapped which is 0x60000. The first byte of this is + // 0x06 which is an invalid opcode. That’s why when we + // overwrite all the bytes but the first 1 in the second step + // it works fine since the jump address still writes a 0x6 at + // the location of the first byte of syscall instruction that + // we are removing and any threads reading the instructions + // will still fault at the same place. + // + // Another subtle aspect is the second step is done using a + // regular non-atomic write which means a thread decoding the + // mov instruction could read a garbage value of the immediate + // operand for the ‘mov sysyno, %eax” instruction. But it + // doesn’t matter since we don’t change the first byte which is + // the one that contains the opcode. Also since the thread will + // fault on the 0x6 right after and will be restarted with the + // patched code the mov reading a garbage immediate operand + // doesn’t impact correctness. + + // The patch is applied in three steps: + // + // The first step is to replace the first byte of the syscall + // instruction by one-byte invalid instruction (0x06), so that + // other threads which have passed the mov instruction fault on + // the invalid instruction and restart a patched code. + faultInstB := primitive.ByteSlice(faultInst[:]) + if _, err := faultInstB.CopyOut(ignorePermContext, hostarch.Addr(patchAddr+faultInstOffset)); err != nil { + return false, err + } + // The second step is to replace all bytes except the first one + // which is the opcode of the mov instruction, so that the first + // five bytes remain "mov XXX, %rax". + if _, err := primitive.CopyUint8SliceOut(ignorePermContext, hostarch.Addr(patchAddr+1), newCode[1:]); err != nil { + return false, err + } + // The final step is to replace the first byte of the patch. + // After this point, all threads will read the valid jmp + // instruction. + if _, err := primitive.CopyUint8SliceOut(ignorePermContext, hostarch.Addr(patchAddr), newCode[0:1]); err != nil { + return false, err + } + } + ac.RestartSyscall() + ac.SetIP(patchAddr) + return true, nil +} + +// HandleFault handles a fault on a patched syscall instruction. +// +// When we replace a system call with a function call, we replace two +// instructions with one instruction. This means that here can be a thread +// which called the first instruction, then another thread applied a binary +// patch and the first thread calls the second instruction. +// +// To handle this case, the function call (jmp) instruction is constructed so +// that the first byte of the syscall instruction is changed with the one-byte +// invalid instruction (0x6). And in case of the race, the first thread will +// fault on the invalid instruction and HandleFault will restart the function +// call. +func (s *State) HandleFault(ctx context.Context, ac *arch.Context64, mm memoryManager) error { + task := kernel.TaskFromContext(ctx) + if task == nil { + return fmt.Errorf("no task found") + } + + s.mu.RLock() + defer s.mu.RUnlock() + + code := make([]uint8, len(jmpInst)) + ip := ac.IP() - faultInstOffset + if _, err := primitive.CopyUint8SliceIn(task.OwnCopyContext(usermem.IOOpts{AddressSpaceActive: false}), hostarch.Addr(ip), code); err != nil { + return err + } + + for i := 0; i < jmpInstOpcodeLen; i++ { + if code[i] != jmpInst[i] { + return nil + } + } + for i := 0; i < len(faultInst); i++ { + if code[i+int(faultInstOffset)] != faultInst[i] { + return nil + } + } + + regs := &ac.StateData().Regs + if regs.Rax == uint64(unix.SYS_RESTART_SYSCALL) { + // restart_syscall is usually set by the Sentry to restart a + // system call after interruption by a stop signal. The Sentry + // sets RAX and moves RIP back on the size of the syscall + // instruction. + // + // RAX can't be set to SYS_RESTART_SYSCALL due to a race with + // injecting a function call, because neither of the two first + // bytes are equal to proper bytes of jmpInst. + regs.Orig_rax = regs.Rax + regs.Rip += arch.SyscallWidth + return ErrFaultSyscall + } + + ac.SetIP(ip) + return ErrFaultRestart +} + +// PreFork locks the trap table for reading. This call guarantees that the trap +// table will not be changed before the next PostFork call. +// +checklocksacquireread:s.mu +func (s *State) PreFork() { + s.mu.RLock() +} + +// PostFork unlocks the trap table. +// +checklocksreleaseread:s.mu +func (s *State) PostFork() { + s.mu.RUnlock() +} diff --git a/pkg/sentry/platform/systrap/usertrap/usertrap_amd64_unsafe.go b/pkg/sentry/platform/systrap/usertrap/usertrap_amd64_unsafe.go new file mode 100644 index 000000000..e93a27304 --- /dev/null +++ b/pkg/sentry/platform/systrap/usertrap/usertrap_amd64_unsafe.go @@ -0,0 +1,91 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 +// +build amd64 + +package usertrap + +import ( + "encoding/binary" + "unsafe" + + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/marshal/primitive" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/platform/systrap/sysmsg" + "gvisor.dev/gvisor/pkg/usermem" +) + +// addTrapLocked constructs a trampoline for a specified syscall. +// +// mm.UserTrap.Lock has to be taken. +func (s *State) addTrapLocked(ctx context.Context, ac *arch.Context64, mm memoryManager, sysno uint32) (uint64, error) { + trapAddr, err := s.newTrapLocked(ctx, mm) + if err != nil { + return 0, err + } + + // First eight bytes is an address which points to the 9th byte, they + // are used as an argument for the jmp instruction. + // + // Then here is the code of the syscall trampoline. + // First, we need to lock the sysmsg struct by setting StatePrep. This + // is used to synchronise with sighandler which uses the same struct + // sysmsg. And we need to guarantee that the current thread will not be + // interrupted in syshandler, because the sysmsg struct isn't saved on + // S/R. + // A thread stack can't be change, so the call instruction can't be + // used and we need to save values of stack and instruction registers, + // switch to the syshandler stack and call the jmp instruction to + // syshandler: + // mov sysmsg.StatePrep, %gs:offset(msg.State) + // mov %rsp,%gs:0x20 // msg.AppStack + // mov %gs:0x18,%rsp // msg.SyshandlerStack + // movabs $ret_addr, %rax + // mov %rax,%gs:0x8 // msg.RetAddr + // mov sysno,%eax + // jmpq *%gs:0x10 // msg.Syshandler + trap := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // msg.State = sysmsg.StatePrep + /*08*/ 0x65, 0xc7, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov $X, %gs:OFFSET + /*20*/ 0x65, 0x48, 0x89, 0x24, 0x25, 0x20, 0x00, 0x00, 0x00, // mov %rsp,%gs:0x20 + /*29*/ 0x65, 0x48, 0x8b, 0x24, 0x25, 0x18, 0x00, 0x00, 0x00, // mov %gs:0x18,%rsp + /*38*/ 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $ret_addr, %rax + /*48*/ 0x65, 0x48, 0x89, 0x04, 0x25, 0x08, 0x00, 0x00, 0x00, // mov %rax,%gs:0x8 + /*57*/ 0xb8, 0x00, 0x00, 0x00, 0x00, // mov sysno,%eax + /*62*/ 0x65, 0xff, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, // jmpq *%gs:0x10 + } + binary.LittleEndian.PutUint64(trap[40:48], uint64(ac.IP())) + binary.LittleEndian.PutUint32(trap[58:62], sysno) + binary.LittleEndian.PutUint64(trap[:8], uint64(trapAddr)+8) + + var msg *sysmsg.Msg + binary.LittleEndian.PutUint32(trap[12:16], uint32(unsafe.Offsetof(msg.State))) + binary.LittleEndian.PutUint32(trap[16:20], uint32(sysmsg.StatePrep)) + binary.LittleEndian.PutUint32(trap[25:29], uint32(unsafe.Offsetof(msg.AppStack))) + binary.LittleEndian.PutUint32(trap[34:38], uint32(unsafe.Offsetof(msg.SyshandlerStack))) + binary.LittleEndian.PutUint32(trap[53:57], uint32(unsafe.Offsetof(msg.RetAddr))) + binary.LittleEndian.PutUint32(trap[66:70], uint32(unsafe.Offsetof(msg.Syshandler))) + + iocc := usermem.IOCopyContext{ + Ctx: ctx, + IO: mm, + Opts: usermem.IOOpts{ + IgnorePermissions: true, + }, + } + _, err = primitive.CopyByteSliceOut(&iocc, trapAddr, trap[:]) + return uint64(trapAddr), err +} diff --git a/pkg/sentry/platform/systrap/usertrap/usertrap_arm64.go b/pkg/sentry/platform/systrap/usertrap/usertrap_arm64.go new file mode 100644 index 000000000..b33b28800 --- /dev/null +++ b/pkg/sentry/platform/systrap/usertrap/usertrap_arm64.go @@ -0,0 +1,70 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 +// +build arm64 + +package usertrap + +import ( + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/usermem" +) + +// trapNR is the maximum number of traps what can fit in the trap table. +const trapNR = 256 + +// trapSize is the size of one trap. +const trapSize = 80 + +// TrapTableSize returns the maximum size of a trap table. +func TrapTableSize() uintptr { + return uintptr(trapNR * trapSize) +} + +type memoryManager interface { + usermem.IO + MMap(ctx context.Context, opts memmap.MMapOpts) (hostarch.Addr, error) +} + +// State represents the current state of the trap table. +// +// +stateify savable +type State struct { +} + +// New returns the new state structure. +func New() *State { + return &State{} +} + +func (*State) PatchSyscall(ctx context.Context, ac *arch.Context64, mm memoryManager) (restart bool, err error) { + return false /* restart */, nil +} + +// HandleFault handles a fault on a patched syscall instruction. +func (*State) HandleFault(ctx context.Context, ac *arch.Context64, mm memoryManager) error { + return nil +} + +// PreFork does nothing on arm64 as syscall trapping is not supported. +func (*State) PreFork() { +} + +// PostFork does nothing on arm64 as syscall trapping is not supported. +func (*State) PostFork() { +} diff --git a/runsc/boot/platforms/platforms.go b/runsc/boot/platforms/platforms.go index 0e543d51c..eebd96b87 100644 --- a/runsc/boot/platforms/platforms.go +++ b/runsc/boot/platforms/platforms.go @@ -22,4 +22,5 @@ import ( // Import platforms that runsc might use. _ "gvisor.dev/gvisor/pkg/sentry/platform/kvm" _ "gvisor.dev/gvisor/pkg/sentry/platform/ptrace" + _ "gvisor.dev/gvisor/pkg/sentry/platform/systrap" ) diff --git a/test/util/test_util.h b/test/util/test_util.h index 449f85241..72ab84fee 100644 --- a/test/util/test_util.h +++ b/test/util/test_util.h @@ -219,6 +219,7 @@ constexpr char kNative[] = "native"; constexpr char kPtrace[] = "ptrace"; constexpr char kKVM[] = "kvm"; constexpr char kFuchsia[] = "fuchsia"; +constexpr char kSystrap[] = "systrap"; } // namespace Platform bool IsRunningOnGvisor(); diff --git a/tools/bazeldefs/platforms.bzl b/tools/bazeldefs/platforms.bzl index 59001e134..fefb47201 100644 --- a/tools/bazeldefs/platforms.bzl +++ b/tools/bazeldefs/platforms.bzl @@ -4,6 +4,7 @@ platforms = { "ptrace": [], "kvm": [], + "systrap": [], } # Capabilities that platforms may or may not support. @@ -24,6 +25,13 @@ platform_capabilities = { _CAPABILITY_INT3: True, _CAPABILITY_VSYSCALL: True, }, + "systrap": { + _CAPABILITY_32BIT: False, + _CAPABILITY_ALIGNMENT_CHECK: True, + _CAPABILITY_MULTIPROCESS: True, + _CAPABILITY_INT3: True, + _CAPABILITY_VSYSCALL: True, + }, "kvm": { _CAPABILITY_32BIT: False, _CAPABILITY_ALIGNMENT_CHECK: True,