237 lines
5.8 KiB
Go
237 lines
5.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package verify
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func loadBasicKernel(t *testing.T) *Kernel {
|
|
t.Helper()
|
|
requireHost(t, "amd64")
|
|
k, err := Load("../testdata/verify/basic_amd64.s")
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
t.Cleanup(k.Close)
|
|
return k
|
|
}
|
|
|
|
func TestReplayEntry(t *testing.T) {
|
|
k := loadBasicKernel(t)
|
|
|
|
e := CorpusEntry{Func: "add", Args: []CorpusArg{
|
|
{Kind: "int", Value: "2"},
|
|
{Kind: "int", Value: "3"},
|
|
}}
|
|
out, err := k.ReplayEntry("add", e)
|
|
if err != nil {
|
|
t.Fatalf("ReplayEntry: %v", err)
|
|
}
|
|
if got := int64(GetUint64(out, 16)); got != 5 {
|
|
t.Errorf("replay add(2, 3) = %d, want 5", got)
|
|
}
|
|
}
|
|
|
|
func TestCorpusRoundTrip(t *testing.T) {
|
|
k := loadBasicKernel(t)
|
|
|
|
e := CorpusEntry{Func: "add", Args: []CorpusArg{
|
|
{Kind: "int", Value: "20"},
|
|
{Kind: "int", Value: "22"},
|
|
}}
|
|
data, err := json.Marshal(e)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var back CorpusEntry
|
|
if err := json.Unmarshal(data, &back); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
out, err := k.ReplayEntry("add", back)
|
|
if err != nil {
|
|
t.Fatalf("ReplayEntry: %v", err)
|
|
}
|
|
if got := int64(GetUint64(out, 16)); got != 42 {
|
|
t.Errorf("round-trip replay = %d, want 42", got)
|
|
}
|
|
}
|
|
|
|
// TestGenDualArgsEntryReplayable checks that the entry recorded alongside a
|
|
// generated input replays to the same observable call.
|
|
func TestGenDualArgsEntryReplayable(t *testing.T) {
|
|
k := loadBasicKernel(t)
|
|
|
|
sig, ok := parseFuncSig("// func add(a, b int) int")
|
|
if !ok {
|
|
t.Fatal("parseFuncSig failed")
|
|
}
|
|
_, _, bufs, entry := genDualArgs(newRNG(1), sig, 24)
|
|
if len(entry.Args) != 2 || entry.Args[0].Kind != "int" {
|
|
t.Fatalf("unexpected entry: %+v", entry)
|
|
}
|
|
out, err := k.ReplayEntry("add", entry)
|
|
if err != nil {
|
|
t.Fatalf("ReplayEntry: %v", err)
|
|
}
|
|
want := int64(GetUint64(out, 16))
|
|
got := entryInt(t, entry.Args[0]) + entryInt(t, entry.Args[1])
|
|
if got != want {
|
|
t.Errorf("replayed sum = %d, want %d", want, got)
|
|
}
|
|
runtime.KeepAlive(bufs)
|
|
}
|
|
|
|
func entryInt(t *testing.T, a CorpusArg) int64 {
|
|
t.Helper()
|
|
v, err := strconv.ParseUint(a.Value, 10, 64)
|
|
if err != nil {
|
|
t.Fatalf("entry value %q: %v", a.Value, err)
|
|
}
|
|
return int64(v)
|
|
}
|
|
|
|
// strProbeSrc is a kernel that consumes an ABI0 string header: it
|
|
// dereferences the data pointer (proving it points at live memory) and
|
|
// returns len(s) + s[0] when the string is non-empty, len(s) otherwise.
|
|
const strProbeSrc = `#include "textflag.h"
|
|
|
|
// func strProbe(s string) int64
|
|
TEXT ·strProbe(SB), NOSPLIT, $0-24
|
|
MOVQ s_base+0(FP), SI
|
|
MOVQ s_len+8(FP), CX
|
|
XORQ AX, AX
|
|
TESTQ CX, CX
|
|
JZ probe_done
|
|
MOVB (SI), AL
|
|
ADDQ AX, CX
|
|
|
|
probe_done:
|
|
MOVQ CX, ret+16(FP)
|
|
RET
|
|
`
|
|
|
|
func loadStrProbeKernel(t *testing.T) *Kernel {
|
|
t.Helper()
|
|
requireHost(t, "amd64")
|
|
file := t.TempDir() + "/strprobe_amd64.s"
|
|
if err := os.WriteFile(file, []byte(strProbeSrc), 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
|
|
}
|
|
|
|
// TestStringParamRoundTrip pins the ABI0 string marshalling end to end:
|
|
// genDualArgs lays a string parameter out as a two-word header pointing at
|
|
// a live buffer, the corpus entry records it, and ReplayEntry rebuilds an
|
|
// equivalent header.
|
|
func TestStringParamRoundTrip(t *testing.T) {
|
|
k := loadStrProbeKernel(t)
|
|
|
|
sig, ok := parseFuncSig("// func strProbe(s string) int64")
|
|
if !ok {
|
|
t.Fatal("parseFuncSig failed")
|
|
}
|
|
args, _, bufs, entry := genDualArgs(newRNG(7), sig, 24)
|
|
if len(entry.Args) != 1 || entry.Args[0].Kind != "string" {
|
|
t.Fatalf("unexpected entry: %+v", entry)
|
|
}
|
|
|
|
out, err := k.CallFunc("strProbe", args)
|
|
if err != nil {
|
|
t.Fatalf("CallFunc: %v", err)
|
|
}
|
|
live := int64(GetUint64(out, 16))
|
|
want := int64(entry.Args[0].Len)
|
|
if d, err := hex.DecodeString(entry.Args[0].Data); err != nil {
|
|
t.Fatalf("entry data: %v", err)
|
|
} else if len(d) > 0 {
|
|
want += int64(d[0])
|
|
}
|
|
if live != want {
|
|
t.Errorf("live call = %d, want %d (len + first byte)", live, want)
|
|
}
|
|
|
|
replayed, err := k.ReplayEntry("strProbe", entry)
|
|
if err != nil {
|
|
t.Fatalf("ReplayEntry: %v", err)
|
|
}
|
|
if got := int64(GetUint64(replayed, 16)); got != want {
|
|
t.Errorf("replayed call = %d, want %d", got, want)
|
|
}
|
|
runtime.KeepAlive(bufs)
|
|
}
|
|
|
|
// TestFuzzHookSavesFailures fuzzes add against the go-tool-asm build of a
|
|
// sub kernel with the same signature, so every iteration mismatches (safely:
|
|
// both kernels read only their own arguments) and the hook must record
|
|
// replayable entries.
|
|
func TestFuzzHookSavesFailures(t *testing.T) {
|
|
requireHost(t, "amd64")
|
|
src := `#include "textflag.h"
|
|
|
|
// func add(a, b int) int
|
|
TEXT ·add(SB), NOSPLIT, $0-24
|
|
MOVQ a+0(FP), AX
|
|
ADDQ b+8(FP), AX
|
|
MOVQ AX, ret+16(FP)
|
|
RET
|
|
|
|
// func sub(a, b int) int
|
|
TEXT ·sub(SB), NOSPLIT, $0-24
|
|
MOVQ a+0(FP), AX
|
|
SUBQ b+8(FP), AX
|
|
MOVQ AX, ret+16(FP)
|
|
RET
|
|
`
|
|
dir := t.TempDir()
|
|
file := dir + "/addsub_test_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)
|
|
|
|
sig, ok := parseFuncSig("// func add(a, b int) int")
|
|
if !ok {
|
|
t.Fatal("parseFuncSig failed")
|
|
}
|
|
gt, err := GroundTruth(file)
|
|
if err != nil {
|
|
t.Skipf("go tool asm unavailable: %v", err)
|
|
}
|
|
|
|
var saved []CorpusEntry
|
|
res := k.FuzzFuncHook("add", sig, gt["sub"], 5, 42, func(e CorpusEntry) {
|
|
saved = append(saved, e)
|
|
})
|
|
if res.Mismatches == 0 {
|
|
t.Fatal("expected mismatches against the sub reference")
|
|
}
|
|
if len(saved) == 0 {
|
|
t.Fatal("hook saved no entries despite mismatches")
|
|
}
|
|
for _, e := range saved {
|
|
if e.Func != "add" || len(e.Args) != 2 {
|
|
t.Errorf("bad entry: %+v", e)
|
|
}
|
|
if _, err := k.ReplayEntry(e.Func, e); err != nil {
|
|
t.Errorf("saved entry does not replay: %v", err)
|
|
}
|
|
}
|
|
}
|