Envoy is fuzz tested via OSS-Fuzz. We follow the best practices described in the OSS-Fuzz ideal integration page.
Tests should be unit test-like, fast and not require writable access to the filesystem (beyond temporary files), network (including loopback) or multiple processes. See the ClusterFuzz environment for further details.
Every fuzz test must comes with a corpus. A corpus is a set of files that provide example valid inputs. Fuzzing libraries will use this seed corpus to drive mutations, e.g. via evolutionary fuzzing, to explore interesting parts of the state space.
The corpus also acts as a quick regression test for evaluating the fuzz tests without the help of a fuzzing library.
The corpus is located in a directory underneath the fuzz test. E.g. suppose you
have
test/common/common/base64_fuzz_test.cc,
a corpus directory
test/common/common/base64_corpus should
exist, populated with files that will act as the corpus.
Your fuzz test will ultimately be driven by a simple interface:
DEFINE_FUZZER(const uint8_t* data, size_t size) {
// Your test code goes here
}It is up to your test DEFINE_FUZZER implementation to map this buffer of data to
meaningful semantics, e.g. a stream of network bytes or a protobuf binary input.
The fuzz test will be executed in two environments:
-
Under Envoy's fuzz test driver when run in the Envoy repository with
bazel test //test/path/to/some_fuzz_test. This provides a litmus test indicating that the test passes CI and basic sanitizers on the supplied corpus. -
Via fuzzing library test drivers in OSS-Fuzz. This is where the real fuzzing takes places on a VM cluster and the seed corpus is used by fuzzers to explore the state space.
-
Write a fuzz test module implementing the
DEFINE_FUZZERinterface. E.g.test/common/common/base64_fuzz_test.cc. -
Define an
envoy_cc_fuzz_testtarget, seebase64_fuzz_testintest/common/common/BUILD. -
Create the seed corpus directory and populate it with at least one example input. E.g.
test/common/common/base64_corpus. -
Run the
envoy_cc_fuzz_testtarget. E.g.bazel test //test/common/common:base64_fuzz_test.
We also have integration with libprotobuf-mutator, allowing tests built on a protobuf input to work directly with a typed protobuf object, rather than a raw buffer. The interface to this is as described at https://github.com/google/libprotobuf-mutator#integrating-with-libfuzzer:
DEFINE_PROTO_FUZZER(const MyMessageType& input) {
// Your test code goes here
}Within the Envoy repository, we have various *_fuzz_test targets. When run
under bazel test, these will exercise the corpus as inputs but not actually
link and run against any fuzzer (e.g.
libfuzzer). The actual fuzzing is
performed by the oss-fuzz project, with
results provided on the ClusterFuzz dashboard.
It is possible to run against fuzzers locally by using the oss-fuzz Docker
image. This is recommended when writing new fuzz tests to check if they pick up
any low hanging fruit (i.e. what you can find on your local machine vs. the fuzz
cluster).
git clone https://github.com/google/oss-fuzz.gitcd oss-fuzzpython infra/helper.py build_image envoypython infra/helper.py build_fuzzers --sanitizer=address envoy <path to envoy source tree>. The path to the Envoy source tree can be omitted if you want to consume Envoy from GitHub at HEAD/master.python infra/helper.py run_fuzzer envoy <fuzz test target>. The fuzz test target will be the test name, e.g.server_fuzz_test.
If there is a crash, run_fuzzer will emit a log line along the lines of:
artifact_prefix='./'; Test unit written to ./crash-db2ee19f50162f2079dc0c5ba24fd0e3dcb8b9bc
The test input can be found in build/out/envoy, e.g.
build/out/envoy/crash-db2ee19f50162f2079dc0c5ba24fd0e3dcb8b9bc. For protobuf
fuzz tests, this will be in text proto format.
To test and validate fixes to the crash, add it to the corpus in the Envoy
source directory for the test, e.g. for server_fuzz_test this is
test/server/corpus, and run bazel test, e.g. bazel test //test/server:server_fuzz_test. These crash cases can be added to the corpus in
followup PRs to provide fuzzers some interesting starting points for invalid
inputs.