Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port fuzz testing to Go 1.18 native fuzzing #508

Merged
merged 3 commits into from Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/push.yml
Expand Up @@ -29,6 +29,10 @@ jobs:
git config --global core.autocrlf false
- name: "Fetch source code"
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Go test
run: |
go test ./...
Expand All @@ -40,6 +44,10 @@ jobs:
steps:
- name: "Fetch source code"
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: "Check vet"
run: |
go vet ./...
Expand Down
1 change: 1 addition & 0 deletions hclsyntax/expression_vars_gen.go
Expand Up @@ -4,6 +4,7 @@
// just wraps the package-level function "Variables" and uses an AST walk
// to do its work.

//go:build ignore
// +build ignore

package main
Expand Down
87 changes: 32 additions & 55 deletions hclsyntax/fuzz/README.md
@@ -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.
1 change: 0 additions & 1 deletion hclsyntax/fuzz/config/corpus/attr-expr.hcl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/config/corpus/attr-literal.hcl

This file was deleted.

3 changes: 0 additions & 3 deletions hclsyntax/fuzz/config/corpus/block-attrs.hcl

This file was deleted.

2 changes: 0 additions & 2 deletions hclsyntax/fuzz/config/corpus/block-empty.hcl

This file was deleted.

5 changes: 0 additions & 5 deletions hclsyntax/fuzz/config/corpus/block-nested.hcl

This file was deleted.

Empty file.
1 change: 0 additions & 1 deletion hclsyntax/fuzz/config/corpus/utf8.hcl

This file was deleted.

16 changes: 0 additions & 16 deletions hclsyntax/fuzz/config/fuzz.go

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/empty.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/escape-dollar.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/escape-newline.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/function-call.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/int.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/literal.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/splat-attr.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/splat-full.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/utf8.hcle

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/expr/corpus/var.hcle

This file was deleted.

16 changes: 0 additions & 16 deletions hclsyntax/fuzz/expr/fuzz.go

This file was deleted.

60 changes: 60 additions & 0 deletions hclsyntax/fuzz/fuzz_test.go
@@ -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())
}
}
})
}
Empty file.
1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/escape-dollar.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/escape-newline.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/function-call.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/int.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/just-interp.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/literal.tmpl

This file was deleted.

1 change: 0 additions & 1 deletion hclsyntax/fuzz/template/corpus/utf8.tmpl

This file was deleted.

16 changes: 0 additions & 16 deletions hclsyntax/fuzz/template/fuzz.go

This file was deleted.

2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/attr-expr.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo = upper(bar + baz[1])\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/attr-literal.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo = \"bar\"\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/block-attrs.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("block {\n foo = true\n}\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/block-empty.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("block {\n}\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/block-nested.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("block {\n another_block {\n foo = bar\n }\n}\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/empty.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseConfig/utf8.hcl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo = \"föo ${föo(\"föo\")}\"\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseExpression/empty.hcle
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"\"")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"hi $${var.foo}\"")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\"bar\\nbaz\"\n")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("title(var.name)")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseExpression/int.hcle
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("42")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseExpression/literal.hcle
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo.bar.*.baz\n")
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("foo.bar[*].baz\n")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseExpression/utf8.hcle
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("föo(\"föo\") + föo")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseExpression/var.hcle
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("var.bar")
2 changes: 2 additions & 0 deletions hclsyntax/fuzz/testdata/fuzz/FuzzParseTemplate/empty.tmpl
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("")