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
|
|
|
|
|
|
|
|
|
|
package verify
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 23:49:19 +02:00
|
|
|
"os"
|
2026-07-24 09:57:21 +02:00
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func loadABIKernel(t *testing.T) *Kernel {
|
|
|
|
|
t.Helper()
|
2026-09-19 23:49:19 +02:00
|
|
|
requireHost(t, "amd64")
|
2026-07-24 09:57:21 +02:00
|
|
|
k, err := Load("../testdata/verify/abi_amd64.s")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(k.Close)
|
|
|
|
|
return k
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestABIClean(t *testing.T) {
|
|
|
|
|
k := loadABIKernel(t)
|
|
|
|
|
|
|
|
|
|
args := make([]byte, 24)
|
|
|
|
|
PutUint64(args, 0, 3)
|
|
|
|
|
PutUint64(args, 8, 4)
|
|
|
|
|
|
|
|
|
|
out, report, err := k.CallFuncChecked("cleanAdd", args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CallFuncChecked: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := int64(GetUint64(out, 16)); got != 7 {
|
|
|
|
|
t.Errorf("cleanAdd(3, 4) = %d, want 7", got)
|
|
|
|
|
}
|
|
|
|
|
if !report.OK() {
|
|
|
|
|
t.Errorf("cleanAdd: %s", report)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestABIBPClobbered(t *testing.T) {
|
|
|
|
|
k := loadABIKernel(t)
|
|
|
|
|
|
|
|
|
|
args := make([]byte, 16)
|
|
|
|
|
PutUint64(args, 0, 42)
|
|
|
|
|
|
|
|
|
|
out, report, err := k.CallFuncChecked("dirtyBP", args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CallFuncChecked: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
|
|
|
|
t.Errorf("dirtyBP(42) = %d, want 42", got)
|
|
|
|
|
}
|
2026-08-30 11:27:54 +02:00
|
|
|
if !report.FPClobbered {
|
2026-07-24 09:57:21 +02:00
|
|
|
t.Error("dirtyBP: expected BP clobbered, but report says clean")
|
|
|
|
|
}
|
2026-08-30 11:27:54 +02:00
|
|
|
if report.GClobbered {
|
2026-07-24 09:57:21 +02:00
|
|
|
t.Error("dirtyBP: R14 should not be clobbered")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestABIR14Clobbered(t *testing.T) {
|
|
|
|
|
k := loadABIKernel(t)
|
|
|
|
|
|
|
|
|
|
args := make([]byte, 16)
|
|
|
|
|
PutUint64(args, 0, 99)
|
|
|
|
|
|
|
|
|
|
out, report, err := k.CallFuncChecked("dirtyR14", args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CallFuncChecked: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := int64(GetUint64(out, 8)); got != 99 {
|
|
|
|
|
t.Errorf("dirtyR14(99) = %d, want 99", got)
|
|
|
|
|
}
|
2026-08-30 11:27:54 +02:00
|
|
|
if !report.GClobbered {
|
2026-07-24 09:57:21 +02:00
|
|
|
t.Error("dirtyR14: expected R14 clobbered, but report says clean")
|
|
|
|
|
}
|
2026-08-30 11:27:54 +02:00
|
|
|
if report.FPClobbered {
|
2026-07-24 09:57:21 +02:00
|
|
|
t.Error("dirtyR14: BP should not be clobbered")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCallFuncCheckedErrors(t *testing.T) {
|
|
|
|
|
k := loadABIKernel(t)
|
|
|
|
|
|
|
|
|
|
// Nonexistent function.
|
|
|
|
|
_, _, err := k.CallFuncChecked("nope", make([]byte, 8))
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error for nonexistent function")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Arg block too small.
|
|
|
|
|
_, _, err = k.CallFuncChecked("cleanAdd", make([]byte, 8))
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error for too-small arg block")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
|
|
|
|
|
// loadCanaryKernel writes an inline kernel pair that exercises the canary
|
|
|
|
|
// geometry against declared frames: frameLocal owns a $96 local frame and
|
|
|
|
|
// writes its lowest local (96 bytes below the entry stack pointer, well
|
|
|
|
|
// past the 64-byte call margin a frame-0 kernel gets); belowFrame writes
|
|
|
|
|
// 136 bytes below its own frame, deep into the canary window.
|
|
|
|
|
func loadCanaryKernel(t *testing.T) *Kernel {
|
|
|
|
|
t.Helper()
|
|
|
|
|
requireHost(t, "amd64")
|
|
|
|
|
src := `#include "textflag.h"
|
|
|
|
|
|
|
|
|
|
// func frameLocal(x int64) int64
|
|
|
|
|
TEXT ·frameLocal(SB), NOSPLIT, $96-16
|
|
|
|
|
MOVQ x+0(FP), AX
|
|
|
|
|
MOVQ AX, l-96(SP)
|
|
|
|
|
MOVQ l-96(SP), AX
|
|
|
|
|
MOVQ AX, ret+8(FP)
|
|
|
|
|
RET
|
|
|
|
|
|
|
|
|
|
// func belowFrame(x int64) int64
|
|
|
|
|
TEXT ·belowFrame(SB), NOSPLIT, $96-16
|
|
|
|
|
MOVQ $1, -136(SP)
|
|
|
|
|
MOVQ x+0(FP), AX
|
|
|
|
|
MOVQ AX, ret+8(FP)
|
|
|
|
|
RET
|
|
|
|
|
`
|
|
|
|
|
file := t.TempDir() + "/canary_amd64.s"
|
|
|
|
|
if err := os.WriteFile(file, []byte(src), 0o644); err != nil {
|
|
|
|
|
t.Fatalf("write kernel: %v", err)
|
|
|
|
|
}
|
|
|
|
|
k, err := Load(file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(k.Close)
|
|
|
|
|
return k
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCallCheckedFrameLegal checks that a kernel whose declared frame
|
|
|
|
|
// extends below the fixed 64-byte margin does not trip the canary: the
|
|
|
|
|
// protected gap must adapt to the frame the TEXT directive declares.
|
|
|
|
|
func TestCallCheckedFrameLegal(t *testing.T) {
|
|
|
|
|
k := loadCanaryKernel(t)
|
|
|
|
|
|
|
|
|
|
args := make([]byte, 16)
|
|
|
|
|
PutUint64(args, 0, 42)
|
|
|
|
|
|
|
|
|
|
out, report, err := k.CallFuncChecked("frameLocal", args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CallFuncChecked(frameLocal): %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
|
|
|
|
t.Errorf("frameLocal(42) = %d, want 42", got)
|
|
|
|
|
}
|
|
|
|
|
if report.RedZoneHit {
|
|
|
|
|
t.Error("frameLocal: writing its own $96 frame must not count as a red-zone hit")
|
|
|
|
|
}
|
|
|
|
|
if !report.OK() {
|
|
|
|
|
t.Errorf("frameLocal: %s", report)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCallCheckedBelowFrameCaught checks the other side of the adaptive
|
|
|
|
|
// gap: a kernel that writes below its own frame by more than the call
|
|
|
|
|
// margin must still be reported.
|
|
|
|
|
func TestCallCheckedBelowFrameCaught(t *testing.T) {
|
|
|
|
|
k := loadCanaryKernel(t)
|
|
|
|
|
|
|
|
|
|
args := make([]byte, 16)
|
|
|
|
|
PutUint64(args, 0, 42)
|
|
|
|
|
|
|
|
|
|
out, report, err := k.CallFuncChecked("belowFrame", args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CallFuncChecked(belowFrame): %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := int64(GetUint64(out, 8)); got != 42 {
|
|
|
|
|
t.Errorf("belowFrame(42) = %d, want 42", got)
|
|
|
|
|
}
|
|
|
|
|
if !report.RedZoneHit {
|
|
|
|
|
t.Error("belowFrame: expected RedZoneHit for a write below the declared frame")
|
|
|
|
|
}
|
|
|
|
|
}
|