Files
Kevin ZhaoandgVisor bot 51b87d918a sentry: use cryptographic PRNG for security-sensitive randomization
Replace `math/rand` with `gvisor.dev/gvisor/pkg/rand` (backed by `crypto/rand`
or `getrandom(2)`) in 5 security-sensitive code paths that currently use the
non-cryptographic `math/rand`:

| File | Usage | Risk |
|------|-------|------|
| `mm/syscalls.go` | Stack ASLR | Stack address prediction aids ROP |
| `systrap/stub_unsafe.go` | Stub code layout | Sentry code location prediction |
| `systrap/usertrap/usertrap_amd64.go` | Trap table layout | Syscall dispatch hijacking |
| `netlink/port/port.go` | Port allocation | Port collision/hijacking |
| `inet/abstract_socket_namespace.go` | Socket autobind | Socket name prediction |

`math/rand` is not cryptographically secure — its output is deterministic given
the internal state, which can be recovered from observed outputs (e.g., by
observing one's own stack address via `/proc/self/maps` and deducing future
randomization outputs for other processes in the same sandbox).

`gvisor.dev/gvisor/pkg/rand` already provides matching API (`Uint32`, `Uint64`,
`Int63n`) backed by `getrandom(2)` / `crypto/rand.Reader`.

**Related**: CVE-2024-10026 and CVE-2024-10603 fixed the same class of weakness
(`math/rand` for network identifiers) in the TCP/IP stack. These 5 files were
not addressed by those fixes.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/12695 from KevinZhao:fix/weak-prng-security-sensitive d6428ea368fd34d95d395a06bf99879678db2ac7
PiperOrigin-RevId: 892755849
2026-04-01 00:16:04 -07:00

343 lines
11 KiB
Go

// 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"
"io/ioutil"
"strconv"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bpf"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/hostsyscall"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/rand"
"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)
// 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,
stubSyscallRules, stubSyscallRulesLen uintptr) {
instrs := sysmsgThreadRules(stubSysmsgStart)
copySeccompRulesToStub(instrs, stubSysmsgRules, stubSysmsgRulesLen)
instrs = sysmsgSyscallNotifyRules()
copySeccompRulesToStub(instrs, stubSyscallRules, stubSyscallRulesLen)
}
func copySeccompRulesToStub(instrs []bpf.Instruction, stubAddr, size uintptr) {
progLen := len(instrs) * int(unsafe.Sizeof(bpf.Instruction{}))
progPtr := stubAddr + unsafe.Sizeof(linux.SockFprog{})
if progLen+int(unsafe.Sizeof(linux.SockFprog{})) > int(size) {
panic("not enough space for sysmsg seccomp rules")
}
targetSlice := unsafe.Slice((*bpf.Instruction)(unsafe.Pointer(progPtr)), len(instrs))
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(stubAddr))
sockProg.Len = uint16(len(instrs))
sockProg.Filter = (*linux.BPFInstruction)(unsafe.Pointer(progPtr))
// Make the seccomp rules stub read-only.
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_MPROTECT,
stubAddr,
size,
unix.PROT_READ); errno != 0 {
panic("mprotect failed: " + errno.Error())
}
}
type mapsRegion struct {
start, end uintptr
}
// parseMaps parses the /proc/self/maps file and returns regions where the stub
// region can be mapped.
func parseMaps(stubAreaStart, stubAreaEnd, minLen uintptr) ([]mapsRegion, error) {
data, err := ioutil.ReadFile("/proc/self/maps")
if err != nil {
return nil, err
}
regions := []mapsRegion{}
rstart := stubAreaStart
for _, line := range strings.Split(string(data), "\n") {
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
addrRange := strings.Split(fields[0], "-")
if len(addrRange) != 2 {
continue
}
start, err := strconv.ParseUint(addrRange[0], 16, 64)
if err != nil {
return nil, err
}
end, err := strconv.ParseUint(addrRange[1], 16, 64)
if err != nil {
return nil, err
}
if uintptr(end) < rstart {
continue
}
if uintptr(start) > stubAreaEnd {
break
}
if rstart+minLen < uintptr(start) {
regions = append(regions, mapsRegion{
start: uintptr(rstart),
end: uintptr(start),
})
}
rstart = uintptr(end)
}
if rstart+minLen <= stubAreaEnd {
regions = append(regions, mapsRegion{
start: uintptr(rstart),
end: uintptr(stubAreaEnd),
})
}
log.Debugf("Stub regions:")
for _, r := range regions {
log.Debugf("%16x-%016x", r.start, r.end)
}
return regions, nil
}
// 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. |
// |----------stubContextQueue----------|
// | Shared ringbuffer queue for stubs |
// | to select the next context. |
// |--------stubThreadContextRegion-----|
// | Reserved space for thread contexts |
// *------------------------------------*
// Grab the existing stub.
procStubBegin := addrOfInitStubProcess()
procStubLen := int(safecopy.FindEndAddress(procStubBegin) - procStubBegin)
procStubSlice := unsafe.Slice((*byte)(unsafe.Pointer(procStubBegin)), procStubLen)
mapLen, _ := hostarch.PageRoundUp(uintptr(procStubLen))
stubSysmsgStart = mapLen
stubSysmsgLen := len(sysmsg.SighandlerBlob)
mapLen, _ = hostarch.PageRoundUp(mapLen + uintptr(stubSysmsgLen))
stubExecMapEnd := mapLen
stubSysmsgRules = mapLen
stubSysmsgRulesLen = hostarch.PageSize * 2
mapLen += stubSysmsgRulesLen
stubSyscallRules = mapLen
stubSyscallRulesLen = hostarch.PageSize
mapLen += stubSyscallRulesLen
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, _ = hostarch.PageRoundUp(mapLen + sysmsg.PerThreadMemSize*(uintptr(maxChildThreads+1)))
// Allocate context queue region
stubContextQueueRegion = mapLen
stubContextQueueRegionLen, _ = hostarch.PageRoundUp(unsafe.Sizeof(contextQueue{}))
mapLen += stubContextQueueRegionLen
stubSpinningThreadQueueAddr = mapLen
mapLen += sysmsg.SpinningQueueMemSize
// Allocate thread context region
stubContextRegion = mapLen
stubContextRegionLen = sysmsg.AllocatedSizeofThreadContextStruct * (maxGuestContexts + 1)
mapLen, _ = hostarch.PageRoundUp(mapLen + stubContextRegionLen)
// Try a few times to avoid cases when new mappings are created.
for i := 0; i < 32; i++ {
if i == 0 { // fast path
// Randomize stubStart address.
randomOffset := uintptr(rand.Uint64() * hostarch.PageSize)
maxRandomOffset := maxRandomOffsetOfStubAddress - mapLen
stubStart = maxStubUserAddress + randomOffset%maxRandomOffset
} else {
regions, err := parseMaps(maxStubUserAddress, maximumUserAddress-mapLen+stubROMapEnd, stubROMapEnd)
if err != nil {
panic(fmt.Sprintf("failed to parse /proc/self/maps: %s", err))
}
n := len(regions)
if n == 0 {
panic("failed to map stub code")
}
r := regions[rand.Uint64()%uint64(n)]
stubStart = r.start + uintptr(rand.Uint64())*hostarch.PageSize%(r.end-r.start-stubROMapEnd)
}
// Map the target address for the stub.
addr, _ := hostsyscall.RawSyscall6(
unix.SYS_MMAP,
stubStart,
stubROMapEnd,
unix.PROT_WRITE|unix.PROT_READ,
unix.MAP_PRIVATE|unix.MAP_ANONYMOUS|unix.MAP_FIXED_NOREPLACE,
0 /* fd */, 0 /* offset */)
if addr == stubStart {
break
}
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)
stubContextQueueRegion += uintptr(gap)
stubContextRegion += uintptr(gap)
// Copy the stub to the address.
targetSlice := unsafe.Slice((*byte)(unsafe.Pointer(stubStart)), procStubLen)
copy(targetSlice, procStubSlice)
stubInitProcess = stubStart
stubSysmsgStart += stubStart
stubSysmsgStack += stubStart
stubROMapEnd += stubStart
stubExecMapEnd += stubStart
stubContextQueueRegion += stubStart
stubSpinningThreadQueueAddr += stubStart
stubContextRegion += 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
stubSyscallRules += stubStart
targetSlice = unsafe.Slice((*byte)(unsafe.Pointer(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_context_region)))
*p = uint64(stubContextRegion)
p = (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_stub_start)))
*p = uint64(stubStart)
archState := (*sysmsg.ArchState)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_arch_state)))
archState.Init()
p = (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_context_queue_addr)))
*p = uint64(stubContextQueueRegion)
p = (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_spinning_queue_addr)))
*p = uint64(stubSpinningThreadQueueAddr)
if disableSyscallPatching {
p = (*uint64)(unsafe.Pointer(stubSysmsgStart + uintptr(sysmsg.Sighandler_blob_offset____export_disable_syscall_patching)))
*p = 1
}
prepareSeccompRules(stubSysmsgStart,
stubSysmsgRules, stubSysmsgRulesLen,
stubSyscallRules, stubSyscallRulesLen)
// Make the stub executable.
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_MPROTECT,
stubStart,
stubExecMapEnd-stubStart,
unix.PROT_EXEC|unix.PROT_READ); errno != 0 {
panic("mprotect failed: " + errno.Error())
}
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_MPROTECT,
stubExecMapEnd,
stubROMapEnd-stubExecMapEnd,
unix.PROT_READ); errno != 0 {
panic("mprotect failed: " + errno.Error())
}
if errno := hostsyscall.RawSyscallErrno(
unix.SYS_MSEAL,
stubStart,
stubROMapEnd-stubStart,
0); errno != 0 && errno != unix.ENOSYS {
panic("mseal failed: " + errno.Error())
}
// Set the end.
stubEnd = stubStart + mapLen + uintptr(gap)
log.Debugf("stubStart %x stubSysmsgStart %x stubSysmsgStack %x, stubContextQueue %x, stubThreadContextRegion %x, mapLen %x", stubStart, stubSysmsgStart, stubSysmsgStack, stubContextQueueRegion, stubContextRegion, mapLen)
log.Debugf("%s", archState.String())
}