Files
Tianyu Zhou fc1afc9de5 platform: add SlimVM platform
SlimVM is a lightweight VMX-based platform for gVisor that accelerates
sentry system calls by eliminating unnecessary user/kernel context
switches.

In a traditional gVisor + KVM setup, every guest syscall triggers a
full VM exit to userspace, then re-enters the kernel via a regular
syscall:

  gr0 -> hr0 (vm_exit) -> hr3 (ioctl return) -> hr0 (re-invoke syscall)

SlimVM eliminates this overhead. When the guest executes a VMCALL, the
VM exits into VMX root mode where SlimVM directly invokes the host
kernel's syscall handler, then immediately resumes the guest:

  gr0 -> hr0 (vmcall + direct function call)

SlimVM requires a companion kernel module that provides the /dev/slimvm
device interface. See https://github.com/antgroup/slimvm for the
kernel module and full documentation.

A sentry->host getpid microbenchmark (Xeon 6982P-C, host kernel 5.10.134)
shows the effect:

            │  ns/op  │ vs base │
  base          82.5       1.0x
  slimvm       420.9       5.1x
  kvm         3566.1      43.2x

slimvm is ~8.5x faster than the kvm platform on this path. See the issue
for details.

Updates: https://github.com/google/gvisor/issues/13330

Co-developed-by: Chenggang <[email protected]>
Co-developed-by: Robin Luk <[email protected]>
Co-developed-by: Tiwei Bie <[email protected]>
Co-developed-by: Jianfeng Tan <[email protected]>
Co-developed-by: liushi.ls <[email protected]>
Co-developed-by: Yong He <[email protected]>
Co-developed-by: Aaron Lu <[email protected]>
Co-developed-by: chris.zn <[email protected]>
Co-developed-by: Lai Jiangshan <[email protected]>
Co-developed-by: Lingfu <[email protected]>
Co-developed-by: Min Le <[email protected]>
Co-developed-by: Quan Xu <[email protected]>
Co-developed-by: Zhang Haoyu <[email protected]>
Co-developed-by: Dawei Shen <[email protected]>
Signed-off-by: Tianyu Zhou <[email protected]>
2026-06-25 10:27:28 +08:00

252 lines
8.7 KiB
Go

// Copyright 2026 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 slimvm
import (
"unsafe"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/hostsyscall"
"gvisor.dev/gvisor/pkg/sentry/arch"
)
var (
// Memory reserved for triggering OOM in HR3 when slimvm returns ENOMEM
dummyBytes []byte
// We will try to recover when OOM happened only if we have at least
// 100MB memory available.
nrDummyBytes = 100 << 20
)
//go:linkname throw runtime.throw
func throw(string)
var hexSyms = []byte("0123456789abcdef")
// printHex writes title followed by val (as a 16-digit hex number) to stderr
// using only a raw write(2). It is async-signal-safe: it allocates nothing,
// takes no lock, and may run on the signal stack.
//
//go:nosplit
func printHex(title []byte, val uint64) {
var str [19]byte
str[0] = ' '
for i := 0; i < 16; i++ {
str[16-i] = hexSyms[val&0xf]
val = val >> 4
}
str[17] = '\n'
hostsyscall.RawSyscallErrno(unix.SYS_WRITE, uintptr(unix.Stderr), uintptr(unsafe.Pointer(&title[0])), uintptr(len(title)))
hostsyscall.RawSyscallErrno(unix.SYS_WRITE, uintptr(unix.Stderr), uintptr(unsafe.Pointer(&str[0])), 18)
}
// bluepillDieCleanly reports a fatal condition encountered inside
// bluepillHandler and terminates the process.
//
// It must be used instead of throw() from within the signal handler context:
// runtime.throw acquires the runtime print lock (debuglock), which is not
// async-signal-safe and deadlocks when reached from the bluepill SIGSEGV
// handler (see the BenchmarkApplicationSyscall hang). This helper only issues
// raw system calls: it writes a diagnostic line to stderr and then exits the
// whole process with status 99.
//
//go:nosplit
func bluepillDieCleanly(msg []byte, status uint64) {
hostsyscall.RawSyscallErrno(unix.SYS_WRITE, uintptr(unix.Stderr), uintptr(unsafe.Pointer(&msg[0])), uintptr(len(msg)))
printHex(dieStatusTitle, status)
hostsyscall.RawSyscallErrno(unix.SYS_EXIT_GROUP, 99, 0, 0)
}
var dieStatusTitle = []byte("slimvm: fatal in bluepillHandler, status =")
var (
dieMsgInvalidState = []byte("slimvm: invalid vCPU state in bluepillHandler\n")
dieMsgNMIInjection = []byte("slimvm: NMI injection failed\n")
dieMsgOOMRelease = []byte("slimvm: OOM: failed to release dummy bytes\n")
dieMsgOOMTime = []byte("slimvm: OOM: failed to get current time\n")
dieMsgOOMRepeat = []byte("slimvm: OOM: ENOMEM happened more than 5 times on this vCPU in last minute\n")
dieMsgRunFailed = []byte("slimvm: run ioctl failed\n")
dieMsgException = []byte("slimvm: unexpected exception exit\n")
dieMsgIO = []byte("slimvm: unexpected I/O exit\n")
dieMsgHypercall = []byte("slimvm: unexpected hypercall exit\n")
dieMsgDebug = []byte("slimvm: unexpected debug exit\n")
dieMsgMMIO = []byte("slimvm: VM exit MMIO, maybe a physical address is out of range\n")
dieMsgShutdown = []byte("slimvm: unexpected shutdown exit\n")
dieMsgFailEntry = []byte("slimvm: VM entry failed\n")
dieMsgMSRWrite = []byte("slimvm: write msr failed\n")
dieMsgUnknown = []byte("slimvm: unknown VM exit status\n")
)
// vCPUPtr returns a CPU for the given address.
//
//go:nosplit
func vCPUPtr(addr uintptr) *vCPU {
return (*vCPU)(unsafe.Pointer(addr))
}
// bytePtr returns a bytePtr for the given address.
//
//go:nosplit
func bytePtr(addr uintptr) *byte {
return (*byte)(unsafe.Pointer(addr))
}
// uintptrValue returns a uintptr for the given address.
//
//go:nosplit
func uintptrValue(addr *byte) uintptr {
return (uintptr)(unsafe.Pointer(addr))
}
// bluepillArchContext returns the arch-specific context.
//
//go:nosplit
func bluepillArchContext(context unsafe.Pointer) *arch.SignalContext64 {
return &((*arch.UContext64)(context).MContext)
}
// bluepillHandler is called from the signal stub.
//
// The world may be stopped while this is executing, and it executes on the
// signal stack. It should only execute raw system calls and functions that are
// explicitly marked go:nosplit.
//
//go:nosplit
func bluepillHandler(context unsafe.Pointer) {
// Sanitize the registers; interrupts must always be disabled.
c := bluepillArchEnter(bluepillArchContext(context))
// Mark this as guest mode.
switch c.state.Swap(vCPUGuest | vCPUUser) {
case vCPUUser: // Expected case.
case vCPUUser | vCPUWaiter:
c.notify()
default:
bluepillDieCleanly(dieMsgInvalidState, uint64(c.state.Load()))
}
for {
switch errno := hostsyscall.RawSyscallErrno(unix.SYS_IOCTL, slimvmFD, _SLIMVM_RUN, uintptr(unsafe.Pointer(&c.vmxConfig))); errno {
case 0: // Expected case.
case unix.EINTR:
// _SLIMVM_RUN can be interrupted by host signals such as
// SIGPROF. SIG_BOUNCE is consumed and injected by the SlimVM
// kernel module, including interrupt-window handling, so HR3
// should not dequeue it here.
continue // Rerun vCPU.
case unix.EFAULT:
// If a fault is not serviceable due to the host
// backing pages having page permissions, instead of an
// MMIO exit we receive EFAULT from the run ioctl. We
// always inject an NMI here since we may be in kernel
// mode and have interrupts disabled.
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_IOCTL,
slimvmFD,
_SLIMVM_NMI, uintptr(unsafe.Pointer(&c.vmxConfig.vcpu))); errno != 0 {
bluepillDieCleanly(dieMsgNMIInjection, uint64(errno))
}
continue // Rerun vCPU.
case unix.ENOMEM:
// OOM happened. Trigger the OOM killer in HR3.
for i := 0; i < nrDummyBytes; i += 4096 {
dummyBytes[i] = 0xff
}
// We failed to trigger the OOM killer. It's possible that we have
// the enough memory now. Release the dummy bytes and try again.
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_MADVISE,
uintptr(unsafe.Pointer(&dummyBytes[0])),
uintptr(nrDummyBytes),
unix.MADV_DONTNEED); errno != 0 {
bluepillDieCleanly(dieMsgOOMRelease, uint64(errno))
}
var now unix.Timeval
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_GETTIMEOFDAY,
uintptr(unsafe.Pointer(&now)),
0, 0); errno != 0 {
bluepillDieCleanly(dieMsgOOMTime, uint64(errno))
}
c.OOMCount++
elapsed := now.Sec - c.OOMLastTS
if elapsed > 60 {
// Reset the counter if it has expired (more than 60s).
c.OOMCount = 1
c.OOMLastTS = now.Sec
} else if c.OOMCount > 5 {
bluepillDieCleanly(dieMsgOOMRepeat, uint64(c.OOMCount))
}
continue // Rerun vCPU.
default:
bluepillDieCleanly(dieMsgRunFailed, uint64(errno))
}
switch c.vmxConfig.status {
case _SLIMVM_EXIT_EXCEPTION:
bluepillDieCleanly(dieMsgException, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_IO:
bluepillDieCleanly(dieMsgIO, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_INTERNAL_ERROR:
// An internal error is typically thrown when emulation
// fails. This can occur via the MMIO path below (and
// it might fail because we have multiple regions that
// are not mapped). We would actually prefer that no
// emulation occur, and don't mind at all if it fails.
case _SLIMVM_EXIT_HYPERCALL:
bluepillDieCleanly(dieMsgHypercall, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_DEBUG:
bluepillDieCleanly(dieMsgDebug, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_HLT:
// Increment our counter.
c.guestExits.Add(1)
// Copy out registers.
bluepillArchExit(c, bluepillArchContext(context))
// Return to the vCPUReady state; notify any waiters.
user := c.state.Load() & vCPUUser
switch c.state.Swap(user) {
case user | vCPUGuest: // Expected case.
case user | vCPUGuest | vCPUWaiter:
c.notify()
default:
bluepillDieCleanly(dieMsgInvalidState, uint64(c.state.Load()))
}
return
case _SLIMVM_EXIT_MMIO:
bluepillDieCleanly(dieMsgMMIO, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_SHUTDOWN:
bluepillDieCleanly(dieMsgShutdown, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_FAIL_ENTRY:
bluepillDieCleanly(dieMsgFailEntry, uint64(c.vmxConfig.status))
case _SLIMVM_EXIT_INTR:
/* Signal Handler */
case _SLIMVM_EXIT_MSR_WRITE:
bluepillDieCleanly(dieMsgMSRWrite, uint64(c.vmxConfig.status))
default:
bluepillDieCleanly(dieMsgUnknown, uint64(c.vmxConfig.status))
}
}
}
func init() {
dummyBytes, _ = unix.Mmap(-1, 0, nrDummyBytes, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANONYMOUS|unix.MAP_PRIVATE)
}