feat(verify): save and replay fuzz corpora
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+120
-4
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user