From 192bfb03fb2f8f869d834885716a1b904f5c930d Mon Sep 17 00:00:00 2001 From: Andrei Vagin Date: Wed, 22 Feb 2023 18:18:47 -0800 Subject: [PATCH] Open-sourcing the systrap platform. The systrap platform like the ptrace platform uses stub processes to manage the user address space. The difference is how they intercept system calls and other events like memory faults, exceptions, etc. In case of systrap, all events that have to be handled by the Sentry trigger signals that are handled by a custom signal handler installed on stub processes. The signal handler switches control to the Sentry. Here are a few other optimizations: * On x86, system calls can be replaced with a function call to remove overhead of signals. * For fast interactions of sentry and stub processes, futex wait/wake can be a bottle neck, so we use a polling mode. The platform is launched for the purpose of testing and gathering initial feedback. It is not yet ready for use in production. PiperOrigin-RevId: 511650064 --- .buildkite/pipeline.yaml | 8 + Makefile | 5 + nogo.yaml | 3 + pkg/sentry/kernel/task_run.go | 6 + pkg/sentry/platform/systrap/BUILD | 103 +++ pkg/sentry/platform/systrap/README.md | 33 + pkg/sentry/platform/systrap/filters.go | 78 ++ pkg/sentry/platform/systrap/filters_amd64.go | 27 + pkg/sentry/platform/systrap/filters_arm64.go | 42 + pkg/sentry/platform/systrap/lib_amd64.s | 20 + pkg/sentry/platform/systrap/lib_arm64.s | 20 + pkg/sentry/platform/systrap/stub_amd64.s | 211 +++++ pkg/sentry/platform/systrap/stub_arm64.s | 205 +++++ pkg/sentry/platform/systrap/stub_defs.go | 27 + pkg/sentry/platform/systrap/stub_unsafe.go | 219 +++++ pkg/sentry/platform/systrap/subprocess.go | 861 ++++++++++++++++++ .../platform/systrap/subprocess_amd64.go | 315 +++++++ .../systrap/subprocess_amd64_unsafe.go | 45 + .../platform/systrap/subprocess_arm64.go | 213 +++++ .../systrap/subprocess_arm64_unsafe.go | 51 ++ .../platform/systrap/subprocess_linux.go | 296 ++++++ .../systrap/subprocess_linux_unsafe.go | 52 ++ .../platform/systrap/subprocess_pool.go | 78 ++ .../platform/systrap/subprocess_unsafe.go | 35 + pkg/sentry/platform/systrap/syscall_thread.go | 190 ++++ .../platform/systrap/syscall_thread_amd64.go | 50 + .../platform/systrap/syscall_thread_arm64.go | 50 + .../platform/systrap/syscall_thread_defs.go | 39 + .../platform/systrap/syscall_thread_unsafe.go | 91 ++ pkg/sentry/platform/systrap/sysmsg/BUILD | 138 +++ pkg/sentry/platform/systrap/sysmsg/build.bzl | 35 + .../platform/systrap/sysmsg/gen_offsets_go.sh | 39 + pkg/sentry/platform/systrap/sysmsg/pie.lds.S | 39 + .../systrap/sysmsg/sighandler_amd64.c | 344 +++++++ .../systrap/sysmsg/sighandler_arm64.c | 128 +++ .../systrap/sysmsg/sigrestorer_amd64.S | 22 + .../systrap/sysmsg/sigrestorer_arm64.S | 22 + .../systrap/sysmsg/syshandler_amd64.S | 78 ++ .../systrap/sysmsg/syshandler_arm64.S | 28 + pkg/sentry/platform/systrap/sysmsg/sysmsg.go | 251 +++++ pkg/sentry/platform/systrap/sysmsg/sysmsg.h | 139 +++ .../platform/systrap/sysmsg/sysmsg_amd64.go | 68 ++ .../platform/systrap/sysmsg/sysmsg_arm64.go | 24 + .../platform/systrap/sysmsg/sysmsg_lib.c | 127 +++ .../platform/systrap/sysmsg/sysmsg_offsets.h | 41 + pkg/sentry/platform/systrap/sysmsg_thread.go | 216 +++++ .../platform/systrap/sysmsg_thread_amd64.go | 61 ++ .../platform/systrap/sysmsg_thread_arm64.go | 21 + .../platform/systrap/sysmsg_thread_unsafe.go | 190 ++++ pkg/sentry/platform/systrap/systrap.go | 404 ++++++++ pkg/sentry/platform/systrap/systrap_amd64.go | 37 + pkg/sentry/platform/systrap/systrap_arm64.go | 23 + .../platform/systrap/systrap_arm64_unsafe.go | 63 ++ pkg/sentry/platform/systrap/systrap_unsafe.go | 139 +++ pkg/sentry/platform/systrap/usertrap/BUILD | 27 + .../platform/systrap/usertrap/usertrap.go | 49 + .../systrap/usertrap/usertrap_amd64.go | 349 +++++++ .../systrap/usertrap/usertrap_amd64_unsafe.go | 91 ++ .../systrap/usertrap/usertrap_arm64.go | 70 ++ runsc/boot/platforms/platforms.go | 1 + test/util/test_util.h | 1 + tools/bazeldefs/platforms.bzl | 8 + 62 files changed, 6646 insertions(+) create mode 100644 pkg/sentry/platform/systrap/BUILD create mode 100644 pkg/sentry/platform/systrap/README.md create mode 100644 pkg/sentry/platform/systrap/filters.go create mode 100644 pkg/sentry/platform/systrap/filters_amd64.go create mode 100644 pkg/sentry/platform/systrap/filters_arm64.go create mode 100644 pkg/sentry/platform/systrap/lib_amd64.s create mode 100644 pkg/sentry/platform/systrap/lib_arm64.s create mode 100644 pkg/sentry/platform/systrap/stub_amd64.s create mode 100644 pkg/sentry/platform/systrap/stub_arm64.s create mode 100644 pkg/sentry/platform/systrap/stub_defs.go create mode 100644 pkg/sentry/platform/systrap/stub_unsafe.go create mode 100644 pkg/sentry/platform/systrap/subprocess.go create mode 100644 pkg/sentry/platform/systrap/subprocess_amd64.go create mode 100644 pkg/sentry/platform/systrap/subprocess_amd64_unsafe.go create mode 100644 pkg/sentry/platform/systrap/subprocess_arm64.go create mode 100644 pkg/sentry/platform/systrap/subprocess_arm64_unsafe.go create mode 100644 pkg/sentry/platform/systrap/subprocess_linux.go create mode 100644 pkg/sentry/platform/systrap/subprocess_linux_unsafe.go create mode 100644 pkg/sentry/platform/systrap/subprocess_pool.go create mode 100644 pkg/sentry/platform/systrap/subprocess_unsafe.go create mode 100644 pkg/sentry/platform/systrap/syscall_thread.go create mode 100644 pkg/sentry/platform/systrap/syscall_thread_amd64.go create mode 100644 pkg/sentry/platform/systrap/syscall_thread_arm64.go create mode 100644 pkg/sentry/platform/systrap/syscall_thread_defs.go create mode 100644 pkg/sentry/platform/systrap/syscall_thread_unsafe.go create mode 100644 pkg/sentry/platform/systrap/sysmsg/BUILD create mode 100644 pkg/sentry/platform/systrap/sysmsg/build.bzl create mode 100644 pkg/sentry/platform/systrap/sysmsg/gen_offsets_go.sh create mode 100644 pkg/sentry/platform/systrap/sysmsg/pie.lds.S create mode 100644 pkg/sentry/platform/systrap/sysmsg/sighandler_amd64.c create mode 100644 pkg/sentry/platform/systrap/sysmsg/sighandler_arm64.c create mode 100644 pkg/sentry/platform/systrap/sysmsg/sigrestorer_amd64.S create mode 100644 pkg/sentry/platform/systrap/sysmsg/sigrestorer_arm64.S create mode 100644 pkg/sentry/platform/systrap/sysmsg/syshandler_amd64.S create mode 100644 pkg/sentry/platform/systrap/sysmsg/syshandler_arm64.S create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg.go create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg.h create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg_amd64.go create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg_arm64.go create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg_lib.c create mode 100644 pkg/sentry/platform/systrap/sysmsg/sysmsg_offsets.h create mode 100644 pkg/sentry/platform/systrap/sysmsg_thread.go create mode 100644 pkg/sentry/platform/systrap/sysmsg_thread_amd64.go create mode 100644 pkg/sentry/platform/systrap/sysmsg_thread_arm64.go create mode 100644 pkg/sentry/platform/systrap/sysmsg_thread_unsafe.go create mode 100644 pkg/sentry/platform/systrap/systrap.go create mode 100644 pkg/sentry/platform/systrap/systrap_amd64.go create mode 100644 pkg/sentry/platform/systrap/systrap_arm64.go create mode 100644 pkg/sentry/platform/systrap/systrap_arm64_unsafe.go create mode 100644 pkg/sentry/platform/systrap/systrap_unsafe.go create mode 100644 pkg/sentry/platform/systrap/usertrap/BUILD create mode 100644 pkg/sentry/platform/systrap/usertrap/usertrap.go create mode 100644 pkg/sentry/platform/systrap/usertrap/usertrap_amd64.go create mode 100644 pkg/sentry/platform/systrap/usertrap/usertrap_amd64_unsafe.go create mode 100644 pkg/sentry/platform/systrap/usertrap/usertrap_arm64.go 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,