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

GODRIVER-2550 Add fuzzer to bson packages #1077

Merged
merged 23 commits into from
Oct 10, 2022

Conversation

prestonvasquez
Copy link
Collaborator

@prestonvasquez prestonvasquez commented Sep 15, 2022

GODRIVER-2550

This ticket seeks to

  1. Fuzz the encoder and decoder BSON methods using Go Fuzzing, specifically in the style of the encoding/json pakage.
  2. Seed the fuzz corpus with interesting use cases via the testdata/fuzz/{fuzzer} design.
  3. Seed the fuzz corpus with all the extended JSON data from the specifications/source/bson-corpus.
  4. Add a new fuzz matrix to the evergreen CI along with a test-fuzz task that will serially run any fuzzer in our repository for 10 minutes, streaming the result to Task Log.

GODRIVER-2561 is a followup ticket to add the test-fuzz task to a periodic CI build.

Background and Theory

The goal of fuzz testing is to generate random inputs for a program until that program crashes, revealing a bug. From the Go documentation:

While fuzzing is in progress, the fuzzing engine generates new inputs and runs them against the provided fuzz target. By default, it continues to run until a failing input is found, or the user cancels the process (e.g. with Ctrl^C)

Go's Fuzz Library

The initial implementation of Go's first class Fuzzing library was developed by Dmitry Vyukov as a third party library. Issue #19109 was filed on behalf of Vyukov resulting in a subsequent design draft here which was accepted under Issue #44551.

American Fuzzy Lop (AFL)

American fuzzy lop is a security-oriented fuzzer that employs a novel type of compile-time instrumentation and genetic algorithms to automatically discover clean, interesting test cases that trigger new internal states in the targeted binary. This substantially improves the functional coverage for the fuzzed code. The compact synthesized corpora produced by the tool are also useful for seeding other, more labor- or resource-intensive testing regimes down the road.

It is important to note that the Go library does not directly use AFL, rather

Vyukov's go-fuzz tool operates in a similar way to AFL, but is written specifically for Go.

Radamsa, ZZUF

Radamsa is a test case generator for robustness testing, a.k.a. a fuzzer. It is typically used to test how well a program can withstand malformed and potentially malicious inputs. It works by reading sample files of valid data and generating interestringly different outputs from them. The main selling points of radamsa are that it has already found a slew of bugs in programs that actually matter, it is easily scriptable and, easy to get up and running.

zzuf is a transparent application input fuzzer. Its purpose is to find bugs in applications by corrupting their user-contributed data (which more than often comes from untrusted sources on the Internet). It works by intercepting file and network operations and changing random bits in the program’s input. zzuf’s behaviour is deterministic, making it easier to reproduce bugs.

In the "Fuzzing support for Go" proposal it is noted that the blind mutaiton algorithm will "apply random mutations to user-provided corpus of representative inputs (ZZUF, Radamsa)."

Algorithm

Vyukov's design implements this control loop internally:

start with some (potentially empty) corpus of inputs
for {
    choose a random input from the corpus
    mutate the input
    execute the mutated input and collect code coverage
    if the input gives new coverage, add it to the corpus
}

In order to add coverage recording to a Go program, a developer first runs the go-fuzz-build command (instead of go build), which uses the built-in ast package to add instrumentation to each block in the source code, and sends the result through the regular Go compiler. Once the instrumented binary has been built, the go-fuzz command runs it over and over on multiple CPU cores with randomly mutating inputs, recording any crashes (along with their stack traces and the inputs that caused them) as it goes.

References

  1. https://cmu-program-analysis.github.io/2021/lecture-slides/17-fuzzing.pdf
  2. https://go.dev/security/fuzz/
  3. cmd/go: support continuously running fuzzing with OSS-Fuzz golang/go#50192
  4. https://lwn.net/Articles/829242/
  5. https://lcamtuf.coredump.cx/afl/
  6. https://gitlab.com/akihe/radamsa
  7. http://caca.zoy.org/wiki/zzuf
  8. https://docs.google.com/document/u/1/d/1zXR-TFL3BfnceEAWytV8bnzB2Tfp6EPFinWVJ5V4QC8/pub
  9. https://medium.com/fuzzstation/what-is-continuous-fuzzing-e791c9bba8d0

@prestonvasquez prestonvasquez marked this pull request as ready for review September 29, 2022 20:55
Copy link
Contributor

@benjirewis benjirewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits and a few questions. Looking great, though. Thanks for all that super helpful info in the PR description, too.

.evergreen/config.yml Outdated Show resolved Hide resolved
.evergreen/config.yml Outdated Show resolved Hide resolved
.evergreen/config.yml Outdated Show resolved Hide resolved
.evergreen/run-fuzz.sh Show resolved Hide resolved
done
fi

go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not care about the output of go test (see the || true)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will still pipe the output, but if the go test panics/errors due to a crash (which is good in the case) we don't want the bash script to terminate, rather continue the generated corpus processing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an indicator that will let us know the fuzz test caused a panic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewdale Yes, this should still output the results of the test. If I added a panic to the fuzz test and then added checkpoints to the script like this:

echo "before"
go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
echo "after"

The output would be

Fuzzing "FuzzDecode" in "./bson/fuzz_test.go"
before
fuzz: elapsed: 0s, gathering baseline coverage: 0/460 completed
failure while testing seed corpus entry: FuzzDecode/seed#0
fuzz: elapsed: 0s, gathering baseline coverage: 0/460 completed
--- FAIL: FuzzDecode (0.08s)
    --- FAIL: FuzzDecode (0.00s)
        testing.go:1356: panic: test
            goroutine 60 [running]:
            runtime/debug.Stack()
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/runtime/debug/stack.go:24 +0x104
            testing.tRunner.func1()
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/testing/testing.go:1356 +0x258
            panic({0x104d9f300, 0x104e0eac0})
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/runtime/panic.go:884 +0x204
            go.mongodb.org/mongo-driver/bson.FuzzDecode.func1(0x1400013d718?, {0x10476bb28?, 0x0?, 0x0?})
                /Users/preston.vasquez/Developer/mongo-go-driver/bson/fuzz_test.go:19 +0xb8
            reflect.Value.call({0x104da5400?, 0x104e0d9c0?, 0x13?}, {0x104cc889d, 0x4}, {0x140002d46c0, 0x2, 0x2?})
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/reflect/value.go:584 +0x688
            reflect.Value.Call({0x104da5400?, 0x104e0d9c0?, 0x1400008c820?}, {0x140002d46c0?, 0x0?, 0x1400032e978?})
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/reflect/value.go:368 +0x90
            testing.(*F).Fuzz.func1.1(0x1400007eba0?)
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/testing/fuzz.go:337 +0x1dc
            testing.tRunner(0x14000221520, 0x140002d8480)
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/testing/testing.go:1446 +0x10c
            created by testing.(*F).Fuzz.func1
                /opt/homebrew/Cellar/go/1.19.1/libexec/src/testing/fuzz.go:324 +0x4c4


FAIL
exit status 1
FAIL    go.mongodb.org/mongo-driver/bson        0.298s
after

bson/bson_corpus_spec_test.go Outdated Show resolved Hide resolved
bson/bson_corpus_spec_test.go Outdated Show resolved Hide resolved
seedBSONCorpus(f)

f.Fuzz(func(t *testing.T, data []byte) {
for _, typ := range []func() interface{}{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not loop over []interface{}? Is there a reason I'm not seeing for using these constructors?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a practice I took from the Go team. I think the idea is to make the type instantiation more realistic, i.e. something like this:

x := interface{} // or in our case typ()
// unmarshal into &x

And not like

for _, x := range []interface{ ... } {
// unmarshal into &x
}

t.Fatal("failed to marshal", err)
}

if err := Unmarshal(encoded, i); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting that we unmarshal, marshal and unmarshal again. What's the reasoning there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first unmarshal is to check the validity of the extended JSON. We only want this to generate corpus data on a panic/crash. If the value is not valid BSON and we don't crash, then this subtest if over.

The first marshal is to check that we can encode valid data structures into BSON, it seems unnecessary to fuzz the encoding of invalid data structures.

The last unmarshal is where we check that we can decode valid BSON, if this fails/panics/crashes then we want to know about it.

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x10\x00\x00\x00\v\x00\x00\x00\b\x00\x00\v\x00\x00\x00\x00")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these testdata/fuzz/FuzzDecode files separate from the bson-corpus seeds? How are these added into the seed corpus?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This data is different from testdata/bson-corpus. Three of these were interesting cases generated by running the fuzzer. One is BSON that encapsulates all types for an initial maximum code coverage in the style of the encoding/json fuzz test.

From the documentation:

seed corpus: A user-provided corpus for a fuzz test which can be used to guide the fuzzing engine. It is composed of the corpus entries provided by f.Add calls within the fuzz test, and the files in the testdata/fuzz/{FuzzTestName} directory within the package. These entries are run by default with go test, whether fuzzing or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the explanation.

Copy link
Contributor

@benjirewis benjirewis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Great work 🧑‍🔧

done
fi

go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, thanks

# Move the file to the directory.
mv $CORPUS_FILE $PROJECT_DIRECTORY/fuzz/$FUNC

echo "Moved $CORPUS_FILE to $PROJECT_DIRECTORY/fuzz/$FUNC"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, sounds good.

func seedBSONCorpus(f *testing.F) {
fileNames, err := findJSONFilesInDir(dataDir)
if err != nil {
f.Fatalf("failed to find JSON files in directory %q: %v", dataDir, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f.Fatalf("failed to find JSON files in directory %q: %v", dataDir, err)
f.Fatalf("failed to find JSON files in directory %q: %v", dataDir, err)

@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x10\x00\x00\x00\v\x00\x00\x00\b\x00\x00\v\x00\x00\x00\x00")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for the explanation.

done
fi

go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an indicator that will let us know the fuzz test caused a panic?


# Clean testing cache.
go clean -testcache
go clean -fuzzcache
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is cleaning the testcache and fuzzcache necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewdale I don't think it's necessary. I will remove these lines, I typically clean them locally to see how code coverage compares between runs but it doesn't seem like something we'd care about in CI. I will remove these lines.

Copy link
Collaborator

@matthewdale matthewdale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! 👍

@prestonvasquez prestonvasquez merged commit 6f84f7e into mongodb:master Oct 10, 2022
@prestonvasquez prestonvasquez deleted the GODRIVER-2550 branch October 10, 2022 16:17
Julien-Beezeelinx pushed a commit to Julien-Beezeelinx/mongo-go-driver that referenced this pull request Oct 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants