mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
amd64: implement KPTI for gvisor
Actually, gvisor has KPTI (Kernel PageTable Isolation) between gr0 and gr3. But the upper half of the userCR3 contains the whole sentry kernel which makes the kernel vulnerable to gr3 APP through CPU bugs. This patch implement full KPTI functionality for gvisor. It doesn't map the whole kernel in the upper. It maps only the text section of the binary and the entry area required by the ISA. The entry area contains the global idt, the percpu gdt/tss etc. The entry area packs all these together which is less than 350k for 512 vCPUs. The text section is normally nonsensitive. It is possible to map only the entry functions (interrupt handler etc.) only. But it requires some hacks. Signed-off-by: Lai Jiangshan <jiangshan.ljs@antfin.com> Signed-off-by: Lai Jiangshan <laijs@linux.alibaba.com>
This commit is contained in:
committed by
Lai Jiangshan
parent
6ce10c3c2f
commit
9cae407b27
@@ -156,15 +156,7 @@ func (*KVM) MaxUserAddress() usermem.Addr {
|
||||
func (k *KVM) NewAddressSpace(_ interface{}) (platform.AddressSpace, <-chan struct{}, error) {
|
||||
// Allocate page tables and install system mappings.
|
||||
pageTables := pagetables.New(newAllocator())
|
||||
applyPhysicalRegions(func(pr physicalRegion) bool {
|
||||
// Map the kernel in the upper half.
|
||||
pageTables.Map(
|
||||
usermem.Addr(ring0.KernelStartAddress|pr.virtual),
|
||||
pr.length,
|
||||
pagetables.MapOpts{AccessType: usermem.AnyAccess},
|
||||
pr.physical)
|
||||
return true // Keep iterating.
|
||||
})
|
||||
k.machine.mapUpperHalf(pageTables)
|
||||
|
||||
// Return the new address space.
|
||||
return &addressSpace{
|
||||
|
||||
@@ -152,7 +152,7 @@ func (m *machine) newVCPU() *vCPU {
|
||||
fd: int(fd),
|
||||
machine: m,
|
||||
}
|
||||
c.CPU.Init(&m.kernel, c)
|
||||
c.CPU.Init(&m.kernel, c.id, c)
|
||||
m.vCPUsByID[c.id] = c
|
||||
|
||||
// Ensure the signal mask is correct.
|
||||
@@ -180,9 +180,6 @@ func newMachine(vm int) (*machine, error) {
|
||||
// Create the machine.
|
||||
m := &machine{fd: vm}
|
||||
m.available.L = &m.mu
|
||||
m.kernel.Init(ring0.KernelOpts{
|
||||
PageTables: pagetables.New(newAllocator()),
|
||||
})
|
||||
|
||||
maxVCPUs, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, uintptr(m.fd), _KVM_CHECK_EXTENSION, _KVM_CAP_MAX_VCPUS)
|
||||
if errno != 0 {
|
||||
@@ -195,6 +192,9 @@ func newMachine(vm int) (*machine, error) {
|
||||
// Create the vCPUs map/slices.
|
||||
m.vCPUsByTID = make(map[uint64]*vCPU)
|
||||
m.vCPUsByID = make([]*vCPU, m.maxVCPUs)
|
||||
m.kernel.Init(ring0.KernelOpts{
|
||||
PageTables: pagetables.New(newAllocator()),
|
||||
}, m.maxVCPUs)
|
||||
|
||||
// Apply the physical mappings. Note that these mappings may point to
|
||||
// guest physical addresses that are not actually available. These
|
||||
@@ -207,15 +207,9 @@ func newMachine(vm int) (*machine, error) {
|
||||
pagetables.MapOpts{AccessType: usermem.AnyAccess},
|
||||
pr.physical)
|
||||
|
||||
// And keep everything in the upper half.
|
||||
m.kernel.PageTables.Map(
|
||||
usermem.Addr(ring0.KernelStartAddress|pr.virtual),
|
||||
pr.length,
|
||||
pagetables.MapOpts{AccessType: usermem.AnyAccess},
|
||||
pr.physical)
|
||||
|
||||
return true // Keep iterating.
|
||||
})
|
||||
m.mapUpperHalf(m.kernel.PageTables)
|
||||
|
||||
var physicalRegionsReadOnly []physicalRegion
|
||||
var physicalRegionsAvailable []physicalRegion
|
||||
|
||||
@@ -346,3 +346,43 @@ func rdonlyRegionsForSetMem() (phyRegions []physicalRegion) {
|
||||
func availableRegionsForSetMem() (phyRegions []physicalRegion) {
|
||||
return physicalRegions
|
||||
}
|
||||
|
||||
var execRegions []region
|
||||
|
||||
func init() {
|
||||
applyVirtualRegions(func(vr virtualRegion) {
|
||||
if excludeVirtualRegion(vr) || vr.filename == "[vsyscall]" {
|
||||
return
|
||||
}
|
||||
|
||||
if vr.accessType.Execute {
|
||||
execRegions = append(execRegions, vr.region)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *machine) mapUpperHalf(pageTable *pagetables.PageTables) {
|
||||
for _, r := range execRegions {
|
||||
physical, length, ok := translateToPhysical(r.virtual)
|
||||
if !ok || length < r.length {
|
||||
panic("impossilbe translation")
|
||||
}
|
||||
pageTable.Map(
|
||||
usermem.Addr(ring0.KernelStartAddress|r.virtual),
|
||||
r.length,
|
||||
pagetables.MapOpts{AccessType: usermem.Execute},
|
||||
physical)
|
||||
}
|
||||
for start, end := range m.kernel.EntryRegions() {
|
||||
regionLen := end - start
|
||||
physical, length, ok := translateToPhysical(start)
|
||||
if !ok || length < regionLen {
|
||||
panic("impossible translation")
|
||||
}
|
||||
pageTable.Map(
|
||||
usermem.Addr(ring0.KernelStartAddress|start),
|
||||
regionLen,
|
||||
pagetables.MapOpts{AccessType: usermem.ReadWrite},
|
||||
physical)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package kvm
|
||||
import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/arch"
|
||||
"gvisor.dev/gvisor/pkg/sentry/platform"
|
||||
"gvisor.dev/gvisor/pkg/sentry/platform/ring0"
|
||||
"gvisor.dev/gvisor/pkg/sentry/platform/ring0/pagetables"
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
@@ -48,6 +49,18 @@ const (
|
||||
poolPCIDs = 8
|
||||
)
|
||||
|
||||
func (m *machine) mapUpperHalf(pageTable *pagetables.PageTables) {
|
||||
applyPhysicalRegions(func(pr physicalRegion) bool {
|
||||
pageTable.Map(
|
||||
usermem.Addr(ring0.KernelStartAddress|pr.virtual),
|
||||
pr.length,
|
||||
pagetables.MapOpts{AccessType: usermem.AnyAccess},
|
||||
pr.physical)
|
||||
|
||||
return true // Keep iterating.
|
||||
})
|
||||
}
|
||||
|
||||
// Get all read-only physicalRegions.
|
||||
func rdonlyRegionsForSetMem() (phyRegions []physicalRegion) {
|
||||
var rdonlyRegions []region
|
||||
|
||||
@@ -76,8 +76,11 @@ type KernelOpts struct {
|
||||
type KernelArchState struct {
|
||||
KernelOpts
|
||||
|
||||
// cpuEntries is array of kernelEntry for all cpus
|
||||
cpuEntries []kernelEntry
|
||||
|
||||
// globalIDT is our set of interrupt gates.
|
||||
globalIDT idt64
|
||||
globalIDT *idt64
|
||||
}
|
||||
|
||||
// kernelEntry contains minimal CPU-specific arch state
|
||||
|
||||
@@ -19,8 +19,8 @@ package ring0
|
||||
// N.B. that constraints on KernelOpts must be satisfied.
|
||||
//
|
||||
//go:nosplit
|
||||
func (k *Kernel) Init(opts KernelOpts) {
|
||||
k.init(opts)
|
||||
func (k *Kernel) Init(opts KernelOpts, maxCPUs int) {
|
||||
k.init(opts, maxCPUs)
|
||||
}
|
||||
|
||||
// Halt halts execution.
|
||||
@@ -49,6 +49,11 @@ func (defaultHooks) KernelException(Vector) {
|
||||
|
||||
// kernelSyscall is a trampoline.
|
||||
//
|
||||
// When in amd64, it is called with %rip on the upper half, so it can
|
||||
// NOT access to any global data which is not mapped on upper and must
|
||||
// call to function pointers or interfaces to switch to the lower half
|
||||
// so that callee can access to global data.
|
||||
//
|
||||
// +checkescape:hard,stack
|
||||
//
|
||||
//go:nosplit
|
||||
@@ -58,6 +63,11 @@ func kernelSyscall(c *CPU) {
|
||||
|
||||
// kernelException is a trampoline.
|
||||
//
|
||||
// When in amd64, it is called with %rip on the upper half, so it can
|
||||
// NOT access to any global data which is not mapped on upper and must
|
||||
// call to function pointers or interfaces to switch to the lower half
|
||||
// so that callee can access to global data.
|
||||
//
|
||||
// +checkescape:hard,stack
|
||||
//
|
||||
//go:nosplit
|
||||
@@ -68,10 +78,10 @@ func kernelException(c *CPU, vector Vector) {
|
||||
// Init initializes a new CPU.
|
||||
//
|
||||
// Init allows embedding in other objects.
|
||||
func (c *CPU) Init(k *Kernel, hooks Hooks) {
|
||||
c.self = c // Set self reference.
|
||||
c.kernel = k // Set kernel reference.
|
||||
c.init() // Perform architectural init.
|
||||
func (c *CPU) Init(k *Kernel, cpuID int, hooks Hooks) {
|
||||
c.self = c // Set self reference.
|
||||
c.kernel = k // Set kernel reference.
|
||||
c.init(cpuID) // Perform architectural init.
|
||||
|
||||
// Require hooks.
|
||||
if hooks != nil {
|
||||
|
||||
@@ -18,13 +18,42 @@ package ring0
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"reflect"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/usermem"
|
||||
)
|
||||
|
||||
// init initializes architecture-specific state.
|
||||
func (k *Kernel) init(opts KernelOpts) {
|
||||
func (k *Kernel) init(opts KernelOpts, maxCPUs int) {
|
||||
// Save the root page tables.
|
||||
k.PageTables = opts.PageTables
|
||||
|
||||
entrySize := reflect.TypeOf(kernelEntry{}).Size()
|
||||
var (
|
||||
entries []kernelEntry
|
||||
padding = 1
|
||||
)
|
||||
for {
|
||||
entries = make([]kernelEntry, maxCPUs + padding - 1)
|
||||
totalSize := entrySize * uintptr(maxCPUs + padding - 1)
|
||||
addr := reflect.ValueOf(&entries[0]).Pointer()
|
||||
if addr&(usermem.PageSize-1) == 0 && totalSize >= usermem.PageSize {
|
||||
// The runtime forces power-of-2 alignment for allocations, and we are therefore
|
||||
// safe once the first address is aligned and the chunk is at least a full page.
|
||||
break
|
||||
}
|
||||
padding = padding << 1
|
||||
}
|
||||
k.cpuEntries = entries
|
||||
|
||||
k.globalIDT = &idt64{}
|
||||
if reflect.TypeOf(idt64{}).Size() != usermem.PageSize {
|
||||
panic("Size of globalIDT should be PageSize")
|
||||
}
|
||||
if reflect.ValueOf(k.globalIDT).Pointer() & (usermem.PageSize-1) != 0 {
|
||||
panic("Allocated globalIDT should be page aligned")
|
||||
}
|
||||
|
||||
// Setup the IDT, which is uniform.
|
||||
for v, handler := range handlers {
|
||||
// Allow Breakpoint and Overflow to be called from all
|
||||
@@ -39,9 +68,25 @@ func (k *Kernel) init(opts KernelOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kernel) EntryRegions() map[uintptr]uintptr {
|
||||
regions := make(map[uintptr]uintptr)
|
||||
|
||||
addr := reflect.ValueOf(&k.cpuEntries[0]).Pointer()
|
||||
size := reflect.TypeOf(kernelEntry{}).Size() * uintptr(len(k.cpuEntries))
|
||||
end, _ := usermem.Addr(addr + size).RoundUp()
|
||||
regions[uintptr(usermem.Addr(addr).RoundDown())] = uintptr(end)
|
||||
|
||||
addr = reflect.ValueOf(k.globalIDT).Pointer()
|
||||
size = reflect.TypeOf(idt64{}).Size()
|
||||
end, _ = usermem.Addr(addr + size).RoundUp()
|
||||
regions[uintptr(usermem.Addr(addr).RoundDown())] = uintptr(end)
|
||||
|
||||
return regions
|
||||
}
|
||||
|
||||
// init initializes architecture-specific state.
|
||||
func (c *CPU) init() {
|
||||
c.kernelEntry = &kernelEntry{}
|
||||
func (c *CPU) init(cpuID int) {
|
||||
c.kernelEntry = &c.kernel.cpuEntries[cpuID]
|
||||
c.cpuSelf = c
|
||||
// Null segment.
|
||||
c.gdt[0].setNull()
|
||||
|
||||
@@ -25,13 +25,13 @@ func HaltAndResume()
|
||||
func HaltEl1SvcAndResume()
|
||||
|
||||
// init initializes architecture-specific state.
|
||||
func (k *Kernel) init(opts KernelOpts) {
|
||||
func (k *Kernel) init(opts KernelOpts, maxCPUs int) {
|
||||
// Save the root page tables.
|
||||
k.PageTables = opts.PageTables
|
||||
}
|
||||
|
||||
// init initializes architecture-specific state.
|
||||
func (c *CPU) init() {
|
||||
func (c *CPU) init(cpuID int) {
|
||||
// Set the kernel stack pointer(virtual address).
|
||||
c.registers.Sp = uint64(c.StackTop())
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ const (
|
||||
VirtualizationException
|
||||
SecurityException = 0x1e
|
||||
SyscallInt80 = 0x80
|
||||
_NR_INTERRUPTS = SyscallInt80 + 1
|
||||
_NR_INTERRUPTS = 0x100
|
||||
)
|
||||
|
||||
// System call vectors.
|
||||
|
||||
Reference in New Issue
Block a user