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:
Lai Jiangshan
2020-08-06 21:31:51 +08:00
committed by Lai Jiangshan
parent 6ce10c3c2f
commit 9cae407b27
9 changed files with 130 additions and 33 deletions
+1 -9
View File
@@ -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{
+5 -11
View File
@@ -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
+40
View File
@@ -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)
}
}
+13
View File
@@ -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
+4 -1
View File
@@ -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
+16 -6
View File
@@ -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 {
+48 -3
View File
@@ -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()
+2 -2
View File
@@ -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())
+1 -1
View File
@@ -104,7 +104,7 @@ const (
VirtualizationException
SecurityException = 0x1e
SyscallInt80 = 0x80
_NR_INTERRUPTS = SyscallInt80 + 1
_NR_INTERRUPTS = 0x100
)
// System call vectors.