From 5cb7e3e231421bdfec7f83fdac3ab423aa35bd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Tue, 4 Aug 2026 22:26:04 +0200 Subject: [PATCH] feat(verify): combine ABI checks with fuzzing for deep-path testing --- cmd/gasm/main.go | 31 +++++++++++++++++------- verify/verify.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 35271ea..858c5ab 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -633,6 +633,7 @@ With -profile, the static basic-block structure is listed for each function. `) smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args") abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)") + abiN := fs.Int("abi-n", 100, "number of ABI check iterations with varied inputs") profile := fs.Bool("profile", false, "list basic-block structure per function") groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm") fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs") @@ -823,16 +824,28 @@ With -profile, the static basic-block structure is listed for each function. } if *abi && fl.NoSplit { - args := make([]byte, fl.Args) - _, report, err := k.CallFuncChecked(name, args) - if err != nil { - fmt.Printf(" abi: FAIL — %v\n", err) - rc = 1 - } else if !report.OK() { - fmt.Printf(" abi: %s\n", report) - rc = 1 + // Try varied-input ABI fuzzing first. + if src, err := readSource(path); err == nil { + result := k.FuzzFuncCheckedByName(name, src, *abiN, int64(*abiN)) + if result.Mismatches > 0 { + fmt.Printf(" abi: %s\n", result) + rc = 1 + } else { + fmt.Printf(" abi: clean (%d varied inputs)\n", result.Matches) + } } else { - fmt.Printf(" abi: clean\n") + // Fallback: single zeroed-arg call. + args := make([]byte, fl.Args) + _, report, err := k.CallFuncChecked(name, args) + if err != nil { + fmt.Printf(" abi: FAIL — %v\n", err) + rc = 1 + } else if !report.OK() { + fmt.Printf(" abi: %s\n", report) + rc = 1 + } else { + fmt.Printf(" abi: clean\n") + } } } } diff --git a/verify/verify.go b/verify/verify.go index 131c92d..f5b09f7 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -5,6 +5,7 @@ package verify import ( "fmt" + "math/rand" "os" "sourcedock.dev/petrbalvin/gasm-devkit/asm" @@ -114,6 +115,66 @@ func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, e return CallChecked(fnAddr, args) } +// FuzzFuncCheckedByName is like FuzzFuncChecked but extracts the signature +// from the source code internally. +func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed int64) FuzzResult { + result := FuzzResult{Func: name, Iterations: iterations} + sig, ok := ExtractSignatures(src)[name] + if !ok { + result.Mismatches = iterations + result.FirstFail = "no // func signature found" + return result + } + return k.FuzzFuncChecked(name, sig, iterations, seed) +} + +// FuzzFuncChecked combines fuzzing with ABI checks: it generates varied +// inputs and verifies that callee-saved registers and the red zone are +// preserved even on deep execution paths (not just early exits). +func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed int64) FuzzResult { + result := FuzzResult{Func: name, Iterations: iterations} + rng := rand.New(rand.NewSource(seed)) + + fl, err := k.Func(name) + if err != nil { + result.Mismatches = iterations + result.FirstFail = err.Error() + return result + } + + violations := 0 + for i := 0; i < iterations; i++ { + gasmArgs, _, bufs := genDualArgs(rng, sig, fl.Args) + result.CrashInput = gasmArgs + + _, report, err := k.CallFuncChecked(name, gasmArgs) + if err != nil { + result.Mismatches++ + if result.FirstFail == "" { + result.FirstFail = fmt.Sprintf("iter %d: call: %v", i, err) + } + releaseBufs(bufs) + continue + } + + if !report.OK() { + violations++ + result.Mismatches++ + if result.FirstFail == "" { + result.FirstFail = fmt.Sprintf("iter %d: %s", i, report.String()) + } + } else { + result.Matches++ + } + releaseBufs(bufs) + } + + if violations > 0 && result.FirstFail == "" { + result.FirstFail = fmt.Sprintf("%d ABI violations across %d iterations", violations, iterations) + } + return result +} + // Close releases the executable mapping. func (k *Kernel) Close() { if k.exec != nil {