Files
adb473c778 kvm/arm64: disable host PAC to fix VDSO crash in sentry guest
On ARM64 with Pointer Authentication (PAC), the sentry's Go runtime runs inside
the KVM guest at Guest EL1. When the runtime calls a VDSO function such as
_kernel_getrandom, the VDSO's paciasp signs the return address using whatever
PAC state the guest happens to have. The subsequent SVC traps to El1_sync, the
vCPU exits, and bluepillArchExit copies the VDSO PC into the host's signal
context. After sigreturn resumes the host thread inside the VDSO, the VDSO
epilogue's autiasp tries to verify the (now-stale) return address with the
host's PAC keys. Because host and guest PAC keys are independent (and gVisor
does not enable KVM_ARM_VCPU_PTRAUTH*), verification fails and the kernel raises
SIGILL. gVisor's process-wide SIGILL handler then misinterprets R8 (which still
contains VDSO state, not a vCPU pointer) and crashes with SIGSEGV.

The crash is 100% reproducible on PAC-capable ARM64 hardware with workloads that
do many fork/exec operations, e.g.:

```
  runsc --platform=kvm do /bin/bash -c \
   'for i in $(seq 1 500); do /bin/echo x > /dev/null; done'

```

PAC keys are per-process secrets and cannot be read from
userspace, so there is no way to synchronise them between Guest EL1 and Host
EL0. Instead, disable the host's address-authentication PAC keys via
PR_PAC_SET_ENABLED_KEYS on every host thread that ever holds a vCPU. With
keys disabled, the host's paciasp/autiasp execute as NOPs and no longer try
to verify the (mismatched) tag set by the guest, removing the crash.

Only the four address keys (IA, IB, DA, DB) are passed; the generic key (APGA)
is rejected by the kernel for this prctl and is unused by the VDSO.

The prctl is invoked from loadSegments(), which runs every time a vCPU is bound
to a (potentially new) host thread. Each thread that ever hosts a vCPU therefore
has PAC disabled before it can trigger the bluepill SIGILL path. The prctl is
per-thread and idempotent at the kernel level, so calling it again on a thread
that already has PAC disabled is harmless.

Fixes #12917.

Co-authored-by: Konstantin Bogomolov <[email protected]>
PiperOrigin-RevId: 926345394
2026-06-03 16:45:41 -07:00

169 lines
3.8 KiB
ArmAsm

// 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 "textflag.h"
// VCPU_CPU is the location of the CPU in the vCPU struct.
//
// This is guaranteed to be zero.
#define VCPU_CPU 0x0
// CPU_SELF is the self reference in ring0's percpu.
//
// This is guaranteed to be zero.
#define CPU_SELF 0x0
// Context offsets.
//
// Only limited use of the context is done in the assembly stub below, most is
// done in the Go handlers.
#define SIGINFO_SIGNO 0x0
#define SIGINFO_CODE 0x8
#define CONTEXT_PC 0x1B8
#define CONTEXT_R0 0xB8
#define SYS_MMAP 222
// getTLS returns the value of TPIDR_EL0 register.
TEXT ·getTLS(SB),NOSPLIT,$0-8
MRS TPIDR_EL0, R1
MOVD R1, value+0(FP)
RET
// setTLS writes the TPIDR_EL0 value.
TEXT ·setTLS(SB),NOSPLIT,$0-8
MOVD value+0(FP), R1
MSR R1, TPIDR_EL0
RET
// See bluepill.go.
TEXT ·bluepill(SB),NOSPLIT,$0
begin:
MOVD c+0(FP), R8
MOVD $VCPU_CPU(R8), R9
ORR $0xffff000000000000, R9, R9
// Trigger sigill.
// In ring0.Start(), the value of R8 will be stored into tpidr_el1.
// When the context was loaded into vcpu successfully,
// we will check if the value of R10 and R9 are the same.
WORD $0xd538d08a // MRS TPIDR_EL1, R10
check_vcpu:
CMP R10, R9
BEQ right_vCPU
wrong_vcpu:
CALL ·redpill(SB)
B begin
right_vCPU:
RET
// sighandler: see bluepill.go for documentation.
//
// The arguments are the following:
//
// R0 - The signal number.
// R1 - Pointer to siginfo_t structure.
// R2 - Pointer to ucontext structure.
//
TEXT ·sighandler(SB),NOSPLIT,$0
// si_signo should be sigill.
MOVD SIGINFO_SIGNO(R1), R7
CMPW $4, R7
BNE fallback
MOVD CONTEXT_PC(R2), R7
CMPW $0, R7
BEQ fallback
MOVD R2, 8(RSP)
BL ·bluepillHandler(SB) // Call the handler.
RET
fallback:
// Jump to the previous signal handler.
MOVD ·savedHandler(SB), R7
B (R7)
// func addrOfSighandler() uintptr
TEXT ·addrOfSighandler(SB), $0-8
MOVD $·sighandler(SB), R0
MOVD R0, ret+0(FP)
RET
// The arguments are the following:
//
// R0 - The signal number.
// R1 - Pointer to siginfo_t structure.
// R2 - Pointer to ucontext structure.
//
TEXT ·sigsysHandler(SB),NOSPLIT,$0
// si_code should be SYS_SECCOMP.
MOVD SIGINFO_CODE(R1), R7
CMPW $1, R7
BNE fallback
CMPW $SYS_MMAP, R8
BNE fallback
MOVD R2, 8(RSP)
BL ·seccompMmapHandler(SB) // Call the handler.
RET
fallback:
// Jump to the previous signal handler.
MOVD ·savedHandler(SB), R7
B (R7)
// func addrOfSighandler() uintptr
TEXT ·addrOfSigsysHandler(SB), $0-8
MOVD $·sigsysHandler(SB), R0
MOVD R0, ret+0(FP)
RET
// dieTrampoline: see bluepill.go, bluepill_arm64_unsafe.go for documentation.
TEXT ·dieTrampoline(SB),NOSPLIT,$0
// R0: Fake the old PC as caller
// R1: First argument (vCPU)
MOVD.P R1, 8(RSP) // R1: First argument (vCPU)
MOVD.P R0, 8(RSP) // R0: Fake the old PC as caller
B ·dieHandler(SB)
// func addrOfDieTrampoline() uintptr
TEXT ·addrOfDieTrampoline(SB), $0-8
MOVD $·dieTrampoline(SB), R0
MOVD R0, ret+0(FP)
RET
// currentEL returns the current exception level.
TEXT ·currentEL(SB),NOSPLIT,$0-8
MRS CurrentEL, R1
UBFX $2, R1, $2, R1
MOVD R1, ret+0(FP)
RET
// func checkPAC() bool
TEXT ·checkPAC(SB),NOSPLIT,$0-1
MOVD R30, R0 // Save LR
WORD $0xd503233f // PACIASP (signs R30)
CMP R0, R30
MOVD $0, R1
BEQ same
MOVD $1, R1
same:
MOVD R0, R30 // Restore LR
MOVB R1, ret+0(FP)
RET