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
+5
View File
@@ -43,6 +43,11 @@ Unreleased changes on the `development` branch.
- **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or - **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or
`0x` hex) can now be supplied to a `--call` invocation alongside `--buf` `0x` hex) can now be supplied to a `--call` invocation alongside `--buf`
buffers, closing the gap where only buffers could be supplied. buffers, closing the gap where only buffers could be supplied.
- **Fuzz corpus save and replay.** `gasm verify --fuzz --save-corpus dir`
records every input that crashes or mismatches as replayable JSON (buffer
contents and scalars, not raw pointers), and `gasm verify --replay dir`
re-runs the saved entries against the kernel in isolated child processes,
reporting whether each one reproduces.
- **`gasm audit-instructions`.** Black-box diff of a gasm encoder - **`gasm audit-instructions`.** Black-box diff of a gasm encoder
against the installed `go tool asm`, for amd64, arm64, riscv64 and against the installed `go tool asm`, for amd64, arm64, riscv64 and
loong64 (`gasm audit-instructions <arch>`): superset encodings loong64 (`gasm audit-instructions <arch>`): superset encodings
+120 -4
View File
@@ -9,6 +9,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@@ -17,6 +18,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "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) 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. 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. 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") 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)") 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)") 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)") 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") 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) set.Parse(args)
if set.NArg() != 1 { if set.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>") 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) 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 // Subprocess mode: fuzz a single function and exit. The parent selects
// the function through the environment, so no internal flag leaks into // the function through the environment, so no internal flag leaks into
// the -h output. // 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) fmt.Printf("%s: not in go tool asm\n", fuzzOne)
return 0 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) fmt.Printf("%s\n", res)
if !res.OK() { if !res.OK() {
return 1 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 // Run in a subprocess: if the function crashes on random
// input (partial function), we report it and move on. // 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 != "" { if res != "" {
fmt.Printf(" %s\n", res) fmt.Printf(" %s\n", res)
if strings.Contains(res, "MISMATCH") { 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. // 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 // 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. // 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() self, err := os.Executable()
if err != nil { if err != nil {
return fmt.Sprintf("%s: cannot find self: %v", funcName, err) 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) cmd.Env = append(os.Environ(), "GASM_VERIFY_FUZZ_ONE="+funcName)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
+8
View File
@@ -79,6 +79,8 @@ Assemble FILE, map it into executable memory, and run dynamic checks.
| `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` | | `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
| `--args <spec>` | Scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) | | `--args <spec>` | Scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
| `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) | | `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) |
| `--save-corpus <dir>` | With `--fuzz`: write each failing input to DIR as replayable JSON |
| `--replay <dir>` | Re-run saved corpus entries (JSON in DIR), one child process per entry |
The `--fuzz` mode runs each function in a subprocess; a partial function The `--fuzz` mode runs each function in a subprocess; a partial function
(e.g. a decoder that faults on malformed input) is reported as (e.g. a decoder that faults on malformed input) is reported as
@@ -92,6 +94,12 @@ offsets, and prints the arg block before and after the call, showing
return values and any output written to the buffers. Scalar parameters return values and any output written to the buffers. Scalar parameters
are supplied with `--args` (decimal, or `0x` hex) at their ABI0 offsets. are supplied with `--args` (decimal, or `0x` hex) at their ABI0 offsets.
The `--save-corpus` mode records the logical arguments (buffer contents and
scalars, not raw pointers) of every failing fuzz input as JSON. `--replay`
rebuilds a live argument block from each entry and calls it in its own child
process, reporting `OK`, `CRASH (reproduced)` or `FAIL` per entry and
exiting non-zero when any entry fails.
## `gasm debug [--func <name>] [--buf spec] [--script file] <file.s>` ## `gasm debug [--func <name>] [--buf spec] [--script file] <file.s>`
Interactive debugger for JIT-assembled functions (amd64, arm64, riscv64, Interactive debugger for JIT-assembled functions (amd64, arm64, riscv64,
+158
View File
@@ -0,0 +1,158 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"encoding/json"
"math/rand"
"os"
"runtime"
"strconv"
"testing"
)
func loadBasicKernel(t *testing.T) *Kernel {
t.Helper()
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(rand.New(rand.NewSource(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)
}
// 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) {
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)
}
}
}
+111 -3
View File
@@ -4,6 +4,7 @@
package verify package verify
import ( import (
"encoding/hex"
"fmt" "fmt"
"math/rand" "math/rand"
"regexp" "regexp"
@@ -40,6 +41,24 @@ func (r FuzzResult) String() string {
return s 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. // funcSig is a parsed // func signature from the assembly source.
type funcSig struct { type funcSig struct {
name string name string
@@ -157,6 +176,13 @@ func ExtractSignatures(src string) map[string]funcSig {
// The signature comment must appear immediately above the TEXT directive // The signature comment must appear immediately above the TEXT directive
// in the source (the conventional Go assembly layout). // in the source (the conventional Go assembly layout).
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult { 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} result := FuzzResult{Func: name, Iterations: iterations}
rng := rand.New(rand.NewSource(seed)) 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 // Generate inputs and build TWO independent arg blocks (one per
// version) so that functions which write to their arguments // version) so that functions which write to their arguments
// (e.g. histogram increments) don't corrupt the other's input. // (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. // Save the current input for crash diagnostics.
result.CrashInput = gasmArgs result.CrashInput = gasmArgs
@@ -193,6 +220,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" { if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err) result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
} }
if onSave != nil {
onSave(entry)
}
runtime.KeepAlive(bufs) runtime.KeepAlive(bufs)
continue continue
} }
@@ -204,6 +234,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" { if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err) result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
} }
if onSave != nil {
onSave(entry)
}
runtime.KeepAlive(bufs) runtime.KeepAlive(bufs)
continue continue
} }
@@ -219,6 +252,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
if result.FirstFail == "" { if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: output mismatch at result offset %d", i, resultOff) result.FirstFail = fmt.Sprintf("iter %d: output mismatch at result offset %d", i, resultOff)
} }
if onSave != nil {
onSave(entry)
}
} else { } else {
result.Matches++ 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 // genDualArgs generates two independent ABI0 argument blocks (for gasm and
// go) with identical logical content but separate backing buffers, so that // go) with identical logical content but separate backing buffers, so that
// functions which write to their arguments don't corrupt the other's input. // 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) gasmArgs = make([]byte, argSize)
goArgs = make([]byte, argSize) goArgs = make([]byte, argSize)
off := 0 off := 0
@@ -267,6 +303,11 @@ func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []b
putU64(goArgs, off+16, uint64(declaredLen)) putU64(goArgs, off+16, uint64(declaredLen))
off += 24 off += 24
sliceIdx++ sliceIdx++
entry.Args = append(entry.Args, CorpusArg{
Kind: "slice",
Len: declaredLen,
Data: hex.EncodeToString(buf1[:n*elemSize]),
})
case strings.HasPrefix(p.typ, "*["): case strings.HasPrefix(p.typ, "*["):
nElem := arrayLen(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(gasmArgs, off, unsafe.Pointer(&buf1[0]))
putPtr(goArgs, off, unsafe.Pointer(&buf2[0])) putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
off += 8 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": case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
v := uint64(rng.Intn(256)) v := uint64(rng.Intn(256))
putU64(gasmArgs, off, v) putU64(gasmArgs, off, v)
putU64(goArgs, off, v) putU64(goArgs, off, v)
off += 8 off += 8
entry.Args = append(entry.Args, CorpusArg{Kind: "int", Value: strconv.FormatUint(v, 10)})
default: default:
v := rng.Uint64() v := rng.Uint64()
putU64(gasmArgs, off, v) putU64(gasmArgs, off, v)
putU64(goArgs, off, v) putU64(goArgs, off, v)
off += 8 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 { func elemSizeFor(sliceType string) int {
+1 -1
View File
@@ -145,7 +145,7 @@ func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed
violations := 0 violations := 0
for i := range iterations { for i := range iterations {
gasmArgs, _, bufs := genDualArgs(rng, sig, fl.Args) gasmArgs, _, bufs, _ := genDualArgs(rng, sig, fl.Args)
result.CrashInput = gasmArgs result.CrashInput = gasmArgs
_, report, err := k.CallFuncChecked(name, gasmArgs) _, report, err := k.CallFuncChecked(name, gasmArgs)