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