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
+111 -3
View File
@@ -4,6 +4,7 @@
package verify
import (
"encoding/hex"
"fmt"
"math/rand"
"regexp"
@@ -40,6 +41,24 @@ func (r FuzzResult) String() string {
return s
}
// CorpusArg is one replayable argument of a corpus entry.
type CorpusArg struct {
Kind string `json:"kind"` // "slice", "ptr", "int", "scalar"
Len int `json:"len,omitempty"` // slice: declared length in elements
Data string `json:"data,omitempty"` // slice/ptr: hex-encoded buffer content
Value string `json:"value,omitempty"` // int/scalar: decimal value
}
// CorpusEntry is a replayable fuzz input: the logical arguments of one
// generated call, stored as JSON. A raw argument block replays nowhere
// (its pointers point into mappings that died with the process), so the
// corpus records buffer contents and scalars instead and ReplayEntry
// rebuilds a live block from them.
type CorpusEntry struct {
Func string `json:"func"`
Args []CorpusArg `json:"args"`
}
// funcSig is a parsed // func signature from the assembly source.
type funcSig struct {
name string
@@ -157,6 +176,13 @@ func ExtractSignatures(src string) map[string]funcSig {
// The signature comment must appear immediately above the TEXT directive
// in the source (the conventional Go assembly layout).
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult {
return k.FuzzFuncHook(name, sig, goCode, iterations, seed, nil)
}
// FuzzFuncHook is FuzzFunc with a hook invoked for every failing input (a
// crash or a mismatch), receiving a replayable corpus entry. A nil hook
// behaves exactly like FuzzFunc.
func (k *Kernel) FuzzFuncHook(name string, sig funcSig, goCode []byte, iterations int, seed int64, onSave func(CorpusEntry)) FuzzResult {
result := FuzzResult{Func: name, Iterations: iterations}
rng := rand.New(rand.NewSource(seed))
@@ -181,7 +207,8 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
// Generate inputs and build TWO independent arg blocks (one per
// version) so that functions which write to their arguments
// (e.g. histogram increments) don't corrupt the other's input.
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
gasmArgs, goArgs, bufs, entry := genDualArgs(rng, sig, fl.Args)
entry.Func = name
// Save the current input for crash diagnostics.
result.CrashInput = gasmArgs
@@ -193,6 +220,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
}
if onSave != nil {
onSave(entry)
}
runtime.KeepAlive(bufs)
continue
}
@@ -204,6 +234,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
}
if onSave != nil {
onSave(entry)
}
runtime.KeepAlive(bufs)
continue
}
@@ -219,6 +252,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: output mismatch at result offset %d", i, resultOff)
}
if onSave != nil {
onSave(entry)
}
} else {
result.Matches++
}
@@ -230,7 +266,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
// genDualArgs generates two independent ABI0 argument blocks (for gasm and
// go) with identical logical content but separate backing buffers, so that
// functions which write to their arguments don't corrupt the other's input.
func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []byte, bufs [][]byte) {
func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []byte, bufs [][]byte, entry CorpusEntry) {
gasmArgs = make([]byte, argSize)
goArgs = make([]byte, argSize)
off := 0
@@ -267,6 +303,11 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
putU64(goArgs, off+16, uint64(declaredLen))
off += 24
sliceIdx++
entry.Args = append(entry.Args, CorpusArg{
Kind: "slice",
Len: declaredLen,
Data: hex.EncodeToString(buf1[:n*elemSize]),
})
case strings.HasPrefix(p.typ, "*["):
nElem := arrayLen(p.typ)
@@ -280,21 +321,88 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
off += 8
entry.Args = append(entry.Args, CorpusArg{
Kind: "ptr",
Data: hex.EncodeToString(buf1),
})
case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
v := uint64(rng.Intn(256))
putU64(gasmArgs, off, v)
putU64(goArgs, off, v)
off += 8
entry.Args = append(entry.Args, CorpusArg{Kind: "int", Value: strconv.FormatUint(v, 10)})
default:
v := rng.Uint64()
putU64(gasmArgs, off, v)
putU64(goArgs, off, v)
off += 8
entry.Args = append(entry.Args, CorpusArg{Kind: "scalar", Value: strconv.FormatUint(v, 10)})
}
}
return gasmArgs, goArgs, bufs
return gasmArgs, goArgs, bufs, entry
}
// ReplayEntry rebuilds the argument block of a corpus entry and invokes the
// named function once, returning the argument block after the call. Slice
// buffers get the same safety padding the fuzzer uses, so over-reads that
// were harmless during the original run stay harmless on replay.
func (k *Kernel) ReplayEntry(name string, e CorpusEntry) ([]byte, error) {
fl, err := k.Func(name)
if err != nil {
return nil, err
}
args := make([]byte, fl.Args)
var bufs [][]byte
off := 0
for _, a := range e.Args {
switch a.Kind {
case "slice":
data, err := hex.DecodeString(a.Data)
if err != nil {
return nil, fmt.Errorf("corpus: slice data: %w", err)
}
buf := make([]byte, len(data)+8192)
copy(buf, data)
bufs = append(bufs, buf)
if off+24 > len(args) {
return nil, fmt.Errorf("corpus: entry does not fit the argument block of %s", name)
}
putPtr(args, off, unsafe.Pointer(&buf[0]))
putU64(args, off+8, uint64(a.Len))
putU64(args, off+16, uint64(a.Len))
off += 24
case "ptr":
data, err := hex.DecodeString(a.Data)
if err != nil {
return nil, fmt.Errorf("corpus: ptr data: %w", err)
}
buf := make([]byte, max(len(data), 8))
copy(buf, data)
bufs = append(bufs, buf)
if off+8 > len(args) {
return nil, fmt.Errorf("corpus: entry does not fit the argument block of %s", name)
}
putPtr(args, off, unsafe.Pointer(&buf[0]))
off += 8
default: // "int", "scalar"
v, err := strconv.ParseUint(a.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("corpus: %s value: %w", a.Kind, err)
}
if off+8 > len(args) {
return nil, fmt.Errorf("corpus: entry does not fit the argument block of %s", name)
}
putU64(args, off, v)
off += 8
}
}
out, err := k.CallFunc(name, args)
runtime.KeepAlive(bufs)
return out, err
}
func elemSizeFor(sliceType string) int {