-
Notifications
You must be signed in to change notification settings - Fork 602
Commit
Port fuzz testing to Go 1.18 native fuzzing
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,62 @@ | ||
# hclsyntax fuzzing utilities | ||
|
||
This directory contains helper functions and corpuses that can be used to | ||
fuzz-test the `hclsyntax` parsers using [go-fuzz](https://github.com/dvyukov/go-fuzz). | ||
This directory contains helper functions and corpora that can be used to | ||
fuzz-test the `hclsyntax` parsers using Go's native fuzz testing capabilities. | ||
|
||
## Work directory | ||
Please see https://go.dev/doc/fuzz/ for more information on fuzzing. | ||
|
||
`go-fuzz` needs a working directory where it can keep state as it works. This | ||
should ideally be in a ramdisk for efficiency, and should probably _not_ be on | ||
an SSD to avoid thrashing it. Here's how to create a ramdisk: | ||
## Prerequisites | ||
* Go 1.18 | ||
|
||
### macOS | ||
|
||
``` | ||
$ SIZE_IN_MB=1024 | ||
$ DEVICE=`hdiutil attach -nobrowse -nomount ram://$(($SIZE_IN_MB*2048))` | ||
$ diskutil erasevolume HFS+ RamDisk $DEVICE | ||
$ export RAMDISK=/Volumes/RamDisk | ||
``` | ||
## Running the fuzzer | ||
|
||
### Linux | ||
Each exported function in the `hclsyntax` package has a corresponding fuzz test. | ||
These can be run one at a time via `go test`: | ||
|
||
``` | ||
$ mkdir /mnt/ramdisk | ||
$ mount -t tmpfs -o size=1024M tmpfs /mnt/ramdisk | ||
$ export RAMDISK=/mnt/ramdisk | ||
$ cd fuzz | ||
$ go test -fuzz FuzzParseTemplate | ||
$ go test -fuzz FuzzParseTraversalAbs | ||
$ go test -fuzz FuzzParseExpression | ||
$ go test -fuzz FuzzParseConfig | ||
``` | ||
|
||
## Running the fuzzer | ||
This command will exit only when a crasher is found (see "Understanding the | ||
result" below.) | ||
|
||
## Seed corpus | ||
|
||
Next, install `go-fuzz` and its build tool in your `GOPATH`: | ||
The seed corpus for each fuzz test function is stored in the corresponding | ||
directory under `hclsyntax/fuzz/testdata/fuzz/`. For example: | ||
|
||
``` | ||
$ make tools FUZZ_WORK_DIR=$RAMDISK | ||
$ ls hclsyntax/fuzz/testdata/fuzz/FuzzParseTemplate | ||
empty.tmpl | ||
escape-dollar.tmpl | ||
escape-newline-tmpl | ||
... | ||
``` | ||
|
||
Now you can fuzz one or all of the parsers: | ||
Additional seed inputs can be added to this corpus. Each file must be in the Go 1.18 corpus file format. Files can be converted to this format using the `file2fuzz` tool. To install it: | ||
|
||
``` | ||
$ make fuzz-config FUZZ_WORK_DIR=$RAMDISK/hclsyntax-fuzz-config | ||
$ make fuzz-expr FUZZ_WORK_DIR=$RAMDISK/hclsyntax-fuzz-expr | ||
$ make fuzz-template FUZZ_WORK_DIR=$RAMDISK/hclsyntax-fuzz-template | ||
$ make fuzz-traversal FUZZ_WORK_DIR=$RAMDISK/hclsyntax-fuzz-traversal | ||
$ go install golang.org/x/tools/cmd/file2fuzz@latest | ||
$ file2fuzz -help | ||
``` | ||
|
||
~> Note: `go-fuzz` does not interact well with `goenv`. If you encounter build | ||
errors where the package `go.fuzz.main` could not be found, you may need to use | ||
a machine with a direct installation of Go. | ||
|
||
## Understanding the result | ||
|
||
A small number of subdirectories will be created in the work directory. | ||
|
||
If you let `go-fuzz` run for a few minutes (the more minutes the better) it | ||
may detect "crashers", which are inputs that caused the parser to panic. Details | ||
about these are written to `$FUZZ_WORK_DIR/crashers`: | ||
may detect "crashers", which are inputs that caused the parser to panic. | ||
These are written to `hclsyntax/fuzz/testdata/fuzz/<fuzz test name>/`: | ||
|
||
``` | ||
$ ls /tmp/hcl2-fuzz-config/crashers | ||
7f5e9ec80c89da14b8b0b238ec88969f658f5a2d | ||
7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.output | ||
7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.quoted | ||
$ ls hclsyntax/fuzz/testdata/fuzz/FuzzParseTemplate | ||
582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 | ||
``` | ||
|
||
The base file above (with no extension) is the input that caused a crash. The | ||
`.output` file contains the panic stack trace, which you can use as a clue to | ||
figure out what caused the crash. | ||
|
||
A good first step to fixing a detected crasher is to copy the failing input | ||
into one of the unit tests in the `hclsyntax` package and see it crash there | ||
too. After that, it's easy to re-run the test as you try to fix it. The | ||
file with the `.quoted` extension contains a form of the input that is quoted | ||
in Go syntax for easy copy-paste into a test case, even if the input contains | ||
non-printable characters or other inconvenient symbols. | ||
|
||
## Rebuilding for new Upstream Code | ||
|
||
An archive file is created for `go-fuzz` to use on the first run of each | ||
of the above, as a `.zip` file created in this directory. If upstream code | ||
is changed these will need to be deleted to cause them to be rebuilt with | ||
the latest code: | ||
|
||
``` | ||
$ make clean | ||
``` | ||
too. After that, it's easy to re-run the test as you try to fix it. |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package fuzzhclsyntax | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/hashicorp/hcl/v2/hclsyntax" | ||
) | ||
|
||
func FuzzParseTemplate(f *testing.F) { | ||
f.Fuzz(func(t *testing.T, data []byte) { | ||
_, diags := hclsyntax.ParseTemplate(data, "<fuzz-tmpl>", hcl.Pos{Line: 1, Column: 1}) | ||
|
||
if diags.HasErrors() { | ||
t.Logf("Error when parsing template %v", data) | ||
for _, diag := range diags { | ||
t.Logf("- %s", diag.Error()) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
func FuzzParseTraversalAbs(f *testing.F) { | ||
f.Fuzz(func(t *testing.T, data []byte) { | ||
_, diags := hclsyntax.ParseTraversalAbs(data, "<fuzz-trav>", hcl.Pos{Line: 1, Column: 1}) | ||
|
||
if diags.HasErrors() { | ||
t.Logf("Error when parsing traversal %v", data) | ||
for _, diag := range diags { | ||
t.Logf("- %s", diag.Error()) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
func FuzzParseExpression(f *testing.F) { | ||
f.Fuzz(func(t *testing.T, data []byte) { | ||
_, diags := hclsyntax.ParseExpression(data, "<fuzz-expr>", hcl.Pos{Line: 1, Column: 1}) | ||
|
||
if diags.HasErrors() { | ||
t.Logf("Error when parsing expression %v", data) | ||
for _, diag := range diags { | ||
t.Logf("- %s", diag.Error()) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
func FuzzParseConfig(f *testing.F) { | ||
f.Fuzz(func(t *testing.T, data []byte) { | ||
_, diags := hclsyntax.ParseConfig(data, "<fuzz-conf>", hcl.Pos{Line: 1, Column: 1}) | ||
|
||
if diags.HasErrors() { | ||
t.Logf("Error when parsing config %v", data) | ||
for _, diag := range diags { | ||
t.Logf("- %s", diag.Error()) | ||
} | ||
} | ||
}) | ||
} |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo = upper(bar + baz[1])\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo = \"bar\"\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("block {\n foo = true\n}\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("block {\n}\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("block {\n another_block {\n foo = bar\n }\n}\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo = \"föo ${föo(\"föo\")}\"\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("\"\"") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("\"hi $${var.foo}\"") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("\"bar\\nbaz\"\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("title(var.name)") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("42") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo.bar.*.baz\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo.bar[*].baz\n") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("föo(\"föo\") + föo") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("var.bar") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("hi $${var.foo}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
go test fuzz v1 | ||
[]byte("foo ${\"bar\\nbaz\"}") |