2026-08-30 11:27:54 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
//go:build amd64 || arm64 || riscv64 || loong64
|
|
|
|
|
|
|
|
|
|
package verify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"fmt"
|
|
|
|
|
"syscall"
|
|
|
|
|
"unsafe"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CallChecked invokes the function at fnAddr with ABI sentinels and a
|
|
|
|
|
// canary below SP, returning both the argument block (with results) and an
|
|
|
|
|
// ABIReport.
|
|
|
|
|
//
|
2026-09-19 23:49:19 +02:00
|
|
|
// 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.
|
|
|
|
|
//
|
2026-08-30 11:27:54 +02:00
|
|
|
// 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) {
|
2026-09-19 23:49:19 +02:00
|
|
|
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) {
|
2026-08-30 11:27:54 +02:00
|
|
|
report := ABIReport{}
|
|
|
|
|
|
|
|
|
|
// Reset the global result.
|
|
|
|
|
abiResult = 0
|
|
|
|
|
|
2026-09-19 23:49:19 +02:00
|
|
|
// 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
|
2026-08-30 11:27:54 +02:00
|
|
|
stackMem, err := syscall.Mmap(-1, 0, totalSize,
|
|
|
|
|
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, report, fmt.Errorf("verify: stack mmap: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = syscall.Munmap(stackMem) }()
|
|
|
|
|
|
|
|
|
|
// Fill the canary window with the detection pattern.
|
|
|
|
|
for i := range redZoneSize {
|
|
|
|
|
stackMem[i] = redZoneFill
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:19 +02:00
|
|
|
// Return address and args after the canary and the frame gap.
|
|
|
|
|
retOff := redZoneSize + pad
|
2026-08-30 11:27:54 +02:00
|
|
|
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
|
|
|
|
|
copy(stackMem[retOff+8:], args)
|
|
|
|
|
|
|
|
|
|
stackBase := uintptr(unsafe.Pointer(&stackMem[retOff]))
|
|
|
|
|
enterJITChecked(fnAddr, stackBase)
|
|
|
|
|
|
|
|
|
|
// Read the register-clobber result.
|
|
|
|
|
res := abiResult
|
|
|
|
|
report.FPClobbered = res&1 != 0
|
|
|
|
|
report.GClobbered = res&2 != 0
|
|
|
|
|
|
|
|
|
|
// Check the canary window.
|
|
|
|
|
for i := range redZoneSize {
|
|
|
|
|
if stackMem[i] != redZoneFill {
|
|
|
|
|
report.RedZoneHit = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy out the argument area.
|
|
|
|
|
out := make([]byte, len(args))
|
|
|
|
|
copy(out, stackMem[retOff+8:retOff+8+len(args)])
|
|
|
|
|
return out, report, nil
|
|
|
|
|
}
|