fix(verify): arm64 stack save, adaptive canary and host gating

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-19 23:49:19 +02:00
parent 87b1081c53
commit 375182ef1f
25 changed files with 651 additions and 57 deletions
+36 -6
View File
@@ -16,21 +16,51 @@ import (
// canary below SP, returning both the argument block (with results) and an
// ABIReport.
//
// The callee is assumed to declare no local frame ($0 in its TEXT
// directive); use CallCheckedFrame, or Kernel.CallFuncChecked which reads
// the frame from the image, for a callee with a frame. A callee whose
// frame extends past the fixed call margin would otherwise trip the canary
// with perfectly legal writes.
//
// Not safe for concurrent use: only one JIT call may be in flight at a
// time, the trampolines keep the saved registers in package globals.
//
// The architecture-specific parts live in abi_<arch>.s: enterJITChecked
// plants sentinels in the registers the Go ABI fixes across calls (the
// frame pointer and the goroutine pointer) before switching to the
// prepared stack, and the raw return trampoline leaveJITCheckedRaw
// compares them and records violations in abiResult.
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
return CallCheckedFrame(fnAddr, args, 0)
}
// CallCheckedFrame is CallChecked with the canary gap sized for a callee
// that declares a local frame of frame bytes: the canary window is placed
// below the deepest write a legal kernel may make, its own frame plus the
// call margin, so only writes that go below the declared frame trip it.
// Callers that already hold the function layout pass asm.FuncLayout.Frame.
//
// Not safe for concurrent use: only one JIT call may be in flight at a
// time, the trampolines keep the saved registers in package globals.
func CallCheckedFrame(fnAddr uintptr, args []byte, frame int) ([]byte, ABIReport, error) {
report := ABIReport{}
// Reset the global result.
abiResult = 0
// Prepare the stack: [canary][padding][leaveJITCheckedRaw][args...]
// The canary sits below the initial SP, so the function would have to
// write below SP to corrupt it.
totalSize := redZoneSize + stackPad + 8 + len(args) + 64
// The gap between the canary window and the entry stack pointer must
// cover every write a legal kernel makes. Two parts:
// - frame: the callee's declared local frame, which the ABI lets it
// write anywhere in [SP-frame, SP).
// - stackPad (64): the call margin. A kernel may CALL another
// function, which pushes a return address below the frame and runs
// a small prologue of its own; 64 bytes covers both. Callees'
// own frames are not accounted: a kernel calling deep into other
// frames can write below this gap without detection.
pad := stackPad + frame
// Prepare the stack: [canary][frame gap][leaveJITCheckedRaw][args...]
totalSize := redZoneSize + pad + 8 + len(args) + 64
stackMem, err := syscall.Mmap(-1, 0, totalSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
@@ -43,8 +73,8 @@ func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
stackMem[i] = redZoneFill
}
// Return address and args after the canary and padding.
retOff := redZoneSize + stackPad
// Return address and args after the canary and the frame gap.
retOff := redZoneSize + pad
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
copy(stackMem[retOff+8:], args)