feat(verify): save and replay fuzz corpora

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-08-30 21:42:12 +02:00
parent 57c0ca8b09
commit 4171e412b5
6 changed files with 403 additions and 8 deletions
+120 -4
View File
@@ -9,6 +9,7 @@ package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
@@ -17,6 +18,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"sort"
"strconv"
"strings"
@@ -1062,6 +1064,12 @@ With -profile, the static basic-block structure is listed for each function.
With -call, a single function is invoked with user-supplied buffers (-buf)
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
decoders) that crash on random input but should succeed on valid data.
With -save-corpus (and -fuzz), every input that crashes or mismatches is
written to the directory as replayable JSON. -replay re-runs saved
entries against the kernel, one child process per entry, so an input that
crashed the original run crashes only the child: the report says whether
each entry reproduces.
`)
smoke := set.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
abi := set.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
@@ -1074,6 +1082,8 @@ decoders) that crash on random input but should succeed on valid data.
bufSpec := set.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)")
scalarSpec := set.String("args", "", "scalar args for -call: name=value[,name=value] (decimal or 0x hex)")
repeat := set.Int("repeat", 1, "number of times to repeat a -call invocation")
saveCorpus := set.String("save-corpus", "", "with -fuzz: write each failing input to this directory as replayable JSON")
replay := set.String("replay", "", "replay saved corpus entries (JSON files in this directory) against the kernel")
set.Parse(args)
if set.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>")
@@ -1119,6 +1129,15 @@ decoders) that crash on random input but should succeed on valid data.
return cmdVerifyCall(k, path, *call, *bufSpec, *scalarSpec, *repeat)
}
// Corpus replay: re-run every saved entry in its own child process, so
// an input that crashed the original run crashes only the child.
if rp := os.Getenv("GASM_VERIFY_REPLAY_ONE"); rp != "" {
return cmdReplayOne(k, rp)
}
if *replay != "" {
return cmdVerifyReplay(path, *replay)
}
// Subprocess mode: fuzz a single function and exit. The parent selects
// the function through the environment, so no internal flag leaks into
// the -h output.
@@ -1145,7 +1164,24 @@ decoders) that crash on random input but should succeed on valid data.
fmt.Printf("%s: not in go tool asm\n", fuzzOne)
return 0
}
res := k.FuzzFunc(fuzzOne, sig, goCode, *fuzzN, 42)
var onSave func(verify.CorpusEntry)
if *saveCorpus != "" {
if err := os.MkdirAll(*saveCorpus, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
saved := 0
onSave = func(e verify.CorpusEntry) {
file := filepath.Join(*saveCorpus, fmt.Sprintf("%s@%d.json", sanitize(e.Func), saved))
saved++
data, err := json.MarshalIndent(e, "", " ")
if err != nil {
return
}
_ = os.WriteFile(file, data, 0o644)
}
}
res := k.FuzzFuncHook(fuzzOne, sig, goCode, *fuzzN, 42, onSave)
fmt.Printf("%s\n", res)
if !res.OK() {
return 1
@@ -1258,7 +1294,11 @@ decoders) that crash on random input but should succeed on valid data.
}
// Run in a subprocess: if the function crashes on random
// input (partial function), we report it and move on.
res := fuzzInSubprocess(path, name, *fuzzN)
var extra []string
if *saveCorpus != "" {
extra = append(extra, "-save-corpus", *saveCorpus)
}
res := fuzzInSubprocess(path, name, *fuzzN, extra...)
if res != "" {
fmt.Printf(" %s\n", res)
if strings.Contains(res, "MISMATCH") {
@@ -1341,14 +1381,90 @@ decoders) that crash on random input but should succeed on valid data.
}
// fuzzInSubprocess runs the fuzz for a single function in a child process.
// cmdVerifyReplay replays every saved corpus entry against the kernel, one
// child process per entry so an input that crashed the original run crashes
// only the child. Exits non-zero when any entry crashes or fails.
func cmdVerifyReplay(path, dir string) int {
self, err := os.Executable()
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: cannot find self: %v\n", err)
return 1
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if len(files) == 0 {
fmt.Fprintf(os.Stderr, "gasm verify: no corpus entries in %s\n", dir)
return 1
}
slices.Sort(files)
rc := 0
for _, f := range files {
cmd := exec.Command(self, "verify", path)
cmd.Env = append(os.Environ(), "GASM_VERIFY_REPLAY_ONE="+f)
out, err := cmd.CombinedOutput()
name := filepath.Base(f)
switch {
case err == nil:
fmt.Printf(" %s: OK\n", name)
case replayCrashed(err):
rc = 1
fmt.Printf(" %s: CRASH (reproduced)\n", name)
default:
rc = 1
detail := strings.TrimSpace(string(out))
if detail == "" {
detail = err.Error()
}
fmt.Printf(" %s: FAIL (%s)\n", name, detail)
}
}
return rc
}
// replayCrashed reports whether a replay child died from a signal, which
// means the saved input reproduced its original crash.
func replayCrashed(err error) bool {
exitErr, ok := err.(*exec.ExitError)
if !ok {
return false
}
ws, ok := exitErr.Sys().(syscall.WaitStatus)
return ok && ws.Signaled()
}
// cmdReplayOne is the child half of corpus replay: rebuild one entry and
// call it, reporting the outcome on stdout.
func cmdReplayOne(k *verify.Kernel, file string) int {
data, err := os.ReadFile(file)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
var e verify.CorpusEntry
if err := json.Unmarshal(data, &e); err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %s: %v\n", file, err)
return 1
}
if _, err := k.ReplayEntry(e.Func, e); err != nil {
fmt.Printf("%s: %v\n", e.Func, err)
return 1
}
return 0
}
// If the child is killed by a signal (e.g. SIGSEGV from a partial function
// faulting on random input), it returns a CRASH report instead of dying.
func fuzzInSubprocess(path, funcName string, n int) string {
func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
self, err := os.Executable()
if err != nil {
return fmt.Sprintf("%s: cannot find self: %v", funcName, err)
}
cmd := exec.Command(self, "verify", "-n", strconv.Itoa(n), path)
fuzzChildArgs := append([]string{"verify", "-n", strconv.Itoa(n)}, extra...)
fuzzChildArgs = append(fuzzChildArgs, path)
cmd := exec.Command(self, fuzzChildArgs...)
cmd.Env = append(os.Environ(), "GASM_VERIFY_FUZZ_ONE="+funcName)
out, err := cmd.CombinedOutput()
if err != nil {