file2fuzz: add fuzzer corpus conversion tool

Adds a new tool, file2fuzz, which allows converting existing files into
the corpus format used by the Go fuzzer.

Change-Id: Ic0cd4bc3e8aa6d47489a460ea170a3f38b7b45e9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/336049
Trust: Roland Shoemaker <roland@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Roland Shoemaker 2021-07-20 11:49:22 -07:00 коммит произвёл Roland Shoemaker
Родитель ebce39e5e3
Коммит 4ad98e9670
2 изменённых файлов: 313 добавлений и 0 удалений

132
cmd/file2fuzz/main.go Normal file
Просмотреть файл

@ -0,0 +1,132 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// file2fuzz converts binary files, such as those used by go-fuzz, to the Go
// fuzzing corpus format.
//
// Usage:
//
// file2fuzz [-o output] [input...]
//
// The defualt behavior is to read input from stdin and write the converted
// output to stdout. If any position arguments are provided stdin is ignored
// and the arguments are assumed to be input files to convert.
//
// The -o flag provides an path to write output files to. If only one positional
// argument is specified it may be a file path or an existing directory, if there are
// multiple inputs specified it must be a directory. If a directory is provided
// the name of the file will be the SHA-256 hash of its contents.
//
package main
import (
"crypto/sha256"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
// encVersion1 is version 1 Go fuzzer corpus encoding.
var encVersion1 = "go test fuzz v1"
func encodeByteSlice(b []byte) []byte {
return []byte(fmt.Sprintf("%s\n[]byte(%q)", encVersion1, b))
}
func usage() {
fmt.Fprintf(os.Stderr, "usage: file2fuzz [-o output] [input...]\nconverts files to Go fuzzer corpus format\n")
fmt.Fprintf(os.Stderr, "\tinput: files to convert\n")
fmt.Fprintf(os.Stderr, "\t-o: where to write converted file(s)\n")
os.Exit(2)
}
func dirWriter(dir string) func([]byte) error {
return func(b []byte) error {
sum := fmt.Sprintf("%x", sha256.Sum256(b))
name := filepath.Join(dir, sum)
if err := os.MkdirAll(dir, 0777); err != nil {
return err
}
if err := ioutil.WriteFile(name, b, 0666); err != nil {
os.Remove(name)
return err
}
return nil
}
}
func convert(inputArgs []string, outputArg string) error {
var input []io.Reader
if args := inputArgs; len(args) == 0 {
input = []io.Reader{os.Stdin}
} else {
for _, a := range args {
f, err := os.Open(a)
if err != nil {
return fmt.Errorf("unable to open %q: %s", a, err)
}
defer f.Close()
if fi, err := f.Stat(); err != nil {
return fmt.Errorf("unable to open %q: %s", a, err)
} else if fi.IsDir() {
return fmt.Errorf("%q is a directory, not a file", a)
}
input = append(input, f)
}
}
var output func([]byte) error
if outputArg == "" {
if len(inputArgs) > 1 {
return errors.New("-o required with multiple input files")
}
output = func(b []byte) error {
_, err := os.Stdout.Write(b)
return err
}
} else {
if len(inputArgs) > 1 {
output = dirWriter(outputArg)
} else {
if fi, err := os.Stat(outputArg); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to open %q for writing: %s", outputArg, err)
} else if err == nil && fi.IsDir() {
output = dirWriter(outputArg)
} else {
output = func(b []byte) error {
return ioutil.WriteFile(outputArg, b, 0666)
}
}
}
}
for _, f := range input {
b, err := ioutil.ReadAll(f)
if err != nil {
return fmt.Errorf("unable to read input: %s", err)
}
if err := output(encodeByteSlice(b)); err != nil {
return fmt.Errorf("unable to write output: %s", err)
}
}
return nil
}
func main() {
log.SetFlags(0)
log.SetPrefix("file2fuzz: ")
output := flag.String("o", "", "where to write converted file(s)")
flag.Usage = usage
flag.Parse()
if err := convert(flag.Args(), *output); err != nil {
log.Fatal(err)
}
}

181
cmd/file2fuzz/main_test.go Normal file
Просмотреть файл

@ -0,0 +1,181 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
)
// The setup for this test is mostly cribbed from x/exp/txtar.
var buildBin struct {
once sync.Once
name string
err error
}
func binPath(t *testing.T) string {
t.Helper()
if _, err := exec.LookPath("go"); err != nil {
t.Skipf("cannot build file2fuzz binary: %v", err)
}
buildBin.once.Do(func() {
exe, err := ioutil.TempFile("", "file2fuzz-*.exe")
if err != nil {
buildBin.err = err
return
}
exe.Close()
buildBin.name = exe.Name()
cmd := exec.Command("go", "build", "-o", buildBin.name, ".")
out, err := cmd.CombinedOutput()
if err != nil {
buildBin.err = fmt.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, out)
}
})
if buildBin.err != nil {
if runtime.GOOS == "android" {
t.Skipf("skipping test after failing to build file2fuzz binary: go_android_exec may have failed to copy needed dependencies (see https://golang.org/issue/37088)")
}
t.Fatal(buildBin.err)
}
return buildBin.name
}
func TestMain(m *testing.M) {
os.Exit(m.Run())
if buildBin.name != "" {
os.Remove(buildBin.name)
}
}
func file2fuzz(t *testing.T, dir string, args []string, stdin string) (string, bool) {
t.Helper()
cmd := exec.Command(binPath(t), args...)
cmd.Dir = dir
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
out, err := cmd.CombinedOutput()
if err != nil {
return string(out), true
}
return string(out), false
}
func TestFile2Fuzz(t *testing.T) {
type file struct {
name string
dir bool
content string
}
tests := []struct {
name string
args []string
stdin string
inputFiles []file
expectedStdout string
expectedFiles []file
expectedError string
}{
{
name: "stdin, stdout",
stdin: "hello",
expectedStdout: "go test fuzz v1\n[]byte(\"hello\")",
},
{
name: "stdin, output file",
stdin: "hello",
args: []string{"-o", "output"},
expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}},
},
{
name: "stdin, output directory",
stdin: "hello",
args: []string{"-o", "output"},
inputFiles: []file{{name: "output", dir: true}},
expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}},
},
{
name: "input file, output file",
args: []string{"-o", "output", "input"},
inputFiles: []file{{name: "input", content: "hello"}},
expectedFiles: []file{{name: "output", content: "go test fuzz v1\n[]byte(\"hello\")"}},
},
{
name: "input file, output directory",
args: []string{"-o", "output", "input"},
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}},
expectedFiles: []file{{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"}},
},
{
name: "input files, output directory",
args: []string{"-o", "output", "input", "input-2"},
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}},
expectedFiles: []file{
{name: "output/ffc7b87a0377262d4f77926bd235551d78e6037bbe970d81ec39ac1d95542f7b", content: "go test fuzz v1\n[]byte(\"hello\")"},
{name: "output/28059db30ce420ff65b2c29b749804c69c601aeca21b3cbf0644244ff080d7a5", content: "go test fuzz v1\n[]byte(\"hello :)\")"},
},
},
{
name: "input files, no output",
args: []string{"input", "input-2"},
inputFiles: []file{{name: "output", dir: true}, {name: "input", content: "hello"}, {name: "input-2", content: "hello :)"}},
expectedError: "file2fuzz: -o required with multiple input files\n",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tmp, err := ioutil.TempDir(os.TempDir(), "file2fuzz")
if err != nil {
t.Fatalf("ioutil.TempDir failed: %s", err)
}
defer os.RemoveAll(tmp)
for _, f := range tc.inputFiles {
if f.dir {
if err := os.Mkdir(filepath.Join(tmp, f.name), 0777); err != nil {
t.Fatalf("failed to create test directory: %s", err)
}
} else {
if err := ioutil.WriteFile(filepath.Join(tmp, f.name), []byte(f.content), 0666); err != nil {
t.Fatalf("failed to create test input file: %s", err)
}
}
}
out, failed := file2fuzz(t, tmp, tc.args, tc.stdin)
if failed && tc.expectedError == "" {
t.Fatalf("file2fuzz failed unexpectedly: %s", out)
} else if failed && out != tc.expectedError {
t.Fatalf("file2fuzz returned unexpected error: got %q, want %q", out, tc.expectedError)
}
if !failed && out != tc.expectedStdout {
t.Fatalf("file2fuzz unexpected stdout: got %q, want %q", out, tc.expectedStdout)
}
for _, f := range tc.expectedFiles {
c, err := ioutil.ReadFile(filepath.Join(tmp, f.name))
if err != nil {
t.Fatalf("failed to read expected output file %q: %s", f.name, err)
}
if string(c) != f.content {
t.Fatalf("expected output file %q contains unexpected content: got %s, want %s", f.name, string(c), f.content)
}
}
})
}
}