2026-07-24 09:57:21 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
//go:build amd64
|
|
|
|
|
|
|
|
|
|
package verify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"fmt"
|
|
|
|
|
"syscall"
|
|
|
|
|
"unsafe"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// abiResult records register-clobber violations detected by the ABI-checking
|
|
|
|
|
// trampoline. Bit 0: BP clobbered. Bit 1: R14 clobbered.
|
|
|
|
|
var abiResult uint64
|
|
|
|
|
|
|
|
|
|
// savedBP holds the caller's frame pointer across the ABI-checked JIT call.
|
2026-08-29 15:25:15 +02:00
|
|
|
// Written and read by enterJITChecked/leaveJITChecked (abi_amd64.s); no Go
|
|
|
|
|
// code references it, which GoLand cannot see inside assembly.
|
|
|
|
|
//
|
|
|
|
|
// noinspection GoUnusedGlobalVariable
|
|
|
|
|
//
|
|
|
|
|
//lint:ignore U1000 written and read by the assembly
|
2026-07-24 09:57:21 +02:00
|
|
|
var savedBP uintptr
|
|
|
|
|
|
|
|
|
|
// leaveCheckedPtr is initialised by the linker from the GLOBL/DATA in
|
|
|
|
|
// abi_amd64.s: it holds the raw address of leaveJITCheckedRaw (which has
|
|
|
|
|
// no ABIInternal wrapper, so the JIT function RETs directly into it).
|
|
|
|
|
var leaveCheckedPtr uintptr
|
|
|
|
|
|
|
|
|
|
// enterJITChecked sets sentinels in BP and R14, switches to the prepared
|
2026-08-29 15:25:15 +02:00
|
|
|
// stack and jumps to fn. The body lives in abi_amd64.s and reads the
|
|
|
|
|
// parameters from the frame by name, which GoLand cannot see.
|
|
|
|
|
//
|
|
|
|
|
// noinspection GoUnusedParameter
|
2026-07-24 09:57:21 +02:00
|
|
|
//
|
|
|
|
|
//go:nosplit
|
|
|
|
|
func enterJITChecked(fn uintptr, stack uintptr)
|
|
|
|
|
|
|
|
|
|
// leaveJITCheckedRaw is the raw return trampoline for ABI checks. Its
|
|
|
|
|
// address is obtained from the GLOBL in abi_amd64.s (leaveCheckedPtr),
|
|
|
|
|
// which points to the .abi0 code — NOT the ABIInternal wrapper that this
|
|
|
|
|
// declaration would generate. The declaration exists solely to satisfy
|
|
|
|
|
// go vet's "missing Go declaration" check.
|
|
|
|
|
//
|
2026-08-29 15:25:15 +02:00
|
|
|
// noinspection GoUnusedFunction
|
|
|
|
|
//
|
|
|
|
|
//lint:ignore U1000 the assembly obtains this address through leaveCheckedPtr
|
2026-07-24 09:57:21 +02:00
|
|
|
//go:nosplit
|
|
|
|
|
func leaveJITCheckedRaw()
|
|
|
|
|
|
|
|
|
|
// ABIReport describes the result of an ABI-checking call.
|
|
|
|
|
type ABIReport struct {
|
|
|
|
|
BPClobbered bool // BP was modified by the function
|
|
|
|
|
R14Clobbered bool // R14 (goroutine pointer) was modified
|
|
|
|
|
RedZoneHit bool // the 128-byte red zone below SP was written
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OK returns true when no violations were detected.
|
|
|
|
|
func (r ABIReport) OK() bool {
|
|
|
|
|
return !r.BPClobbered && !r.R14Clobbered && !r.RedZoneHit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String returns a human-readable summary.
|
|
|
|
|
func (r ABIReport) String() string {
|
|
|
|
|
if r.OK() {
|
|
|
|
|
return "ABI clean"
|
|
|
|
|
}
|
|
|
|
|
s := "ABI violation:"
|
|
|
|
|
if r.BPClobbered {
|
|
|
|
|
s += " BP clobbered"
|
|
|
|
|
}
|
|
|
|
|
if r.R14Clobbered {
|
|
|
|
|
s += " R14 clobbered"
|
|
|
|
|
}
|
|
|
|
|
if r.RedZoneHit {
|
|
|
|
|
s += " red-zone written"
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// redZoneSize is the System V AMD64 red zone: 128 bytes below SP that a
|
|
|
|
|
// leaf function may use without adjusting SP. Go does not use the red zone,
|
|
|
|
|
// so any write there is a bug.
|
|
|
|
|
const redZoneSize = 128
|
|
|
|
|
|
|
|
|
|
// redZoneFill is the byte pattern used to detect red-zone writes.
|
|
|
|
|
const redZoneFill = 0xA5
|
|
|
|
|
|
2026-08-29 17:12:53 +02:00
|
|
|
// CallChecked invokes the function at fnAddr with ABI sentinels and a
|
|
|
|
|
// red-zone canary, returning both the argument block (with results) and an
|
|
|
|
|
// ABIReport.
|
2026-07-24 09:57:21 +02:00
|
|
|
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
|
|
|
|
|
report := ABIReport{}
|
|
|
|
|
|
|
|
|
|
// Reset the global result.
|
|
|
|
|
abiResult = 0
|
|
|
|
|
|
|
|
|
|
// Prepare the stack: [red-zone canary][padding][leaveJITCheckedRaw][args...]
|
|
|
|
|
// The red zone sits below the initial SP, so the function would have to
|
|
|
|
|
// write below SP to corrupt it.
|
|
|
|
|
totalSize := redZoneSize + stackPad + 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 {
|
|
|
|
|
return nil, report, fmt.Errorf("verify: stack mmap: %w", err)
|
|
|
|
|
}
|
2026-08-29 15:25:15 +02:00
|
|
|
defer func() { _ = syscall.Munmap(stackMem) }()
|
2026-07-24 09:57:21 +02:00
|
|
|
|
|
|
|
|
// Fill the red zone with the canary pattern.
|
2026-08-29 15:25:15 +02:00
|
|
|
for i := range redZoneSize {
|
2026-07-24 09:57:21 +02:00
|
|
|
stackMem[i] = redZoneFill
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return address and args after the red zone and padding.
|
|
|
|
|
retOff := redZoneSize + stackPad
|
|
|
|
|
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.BPClobbered = res&1 != 0
|
|
|
|
|
report.R14Clobbered = res&2 != 0
|
|
|
|
|
|
|
|
|
|
// Check the red zone.
|
2026-08-29 15:25:15 +02:00
|
|
|
for i := range redZoneSize {
|
2026-07-24 09:57:21 +02:00
|
|
|
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
|
|
|
|
|
}
|