387 lines
16 KiB
Markdown
387 lines
16 KiB
Markdown
|
# Table of contents
|
|||
|
1. [Background](#background)
|
|||
|
2. [Setting up the test ](#setting_up_the_test)
|
|||
|
1. [Developing an NNAPI fuzz test](#developing_an_nnapi_fuzz_test)
|
|||
|
2. [Preparing a device](#preparing_a_device)
|
|||
|
3. [Building and uploading fuzz test](#building_and_uploading_fuzz_test)
|
|||
|
3. [Running the test](#running_the_test)
|
|||
|
1. [Running the full fuzz test](#running_the_full_fuzz_test)
|
|||
|
2. [Reproducing crash case](#reproducing_crash_case)
|
|||
|
3. [Finding minimal crash case](#finding_minimal_crash_case)
|
|||
|
4. [Fuzz test case format](#fuzz_test_case_format)
|
|||
|
5. [Changing Model.proto](#changing_model_proto)
|
|||
|
1. [Adding new operations or operands](#adding_new_operations_or_operands)
|
|||
|
2. [Expanding the structure](#expanding_the_structure)
|
|||
|
3. [Backward-incompatible change](#backward_incompatible_change)
|
|||
|
1. [Appendix](#appendix)
|
|||
|
1. [Alternative ways to build the device image and test binary](#alternatives)
|
|||
|
|
|||
|
# Background <a id="background"></a>
|
|||
|
|
|||
|
This document seeks to be a crash-course and cheat-sheet for running the NNAPI
|
|||
|
fuzz tests.
|
|||
|
|
|||
|
The purpose of fuzz testing is to find crashes, assertions, memory violations,
|
|||
|
or general undefined behavior in the code under test due to factors such as
|
|||
|
unexpected inputs. The NNAPI fuzz tests are based on `libFuzzer`, which is
|
|||
|
efficient at fuzzing because it uses line coverage of previous test cases to
|
|||
|
generate new random inputs. For example, `libFuzzer` favors test cases that
|
|||
|
run on uncovered lines of code. This greatly reduces the amount of time tests
|
|||
|
take to find problematic code.
|
|||
|
|
|||
|
Currently, there are two NNAPI fuzz test targets: `libneuralnetworks_fuzzer`
|
|||
|
which tests at the NNAPI NDK layer (testing libneuralnetworks as a static
|
|||
|
library) and `libneuralnetworks_driver_fuzzer` which tests an in-process driver
|
|||
|
at the NNAPI HAL layer (the sample driver, unless the test is modified to do
|
|||
|
otherwise). To simplify development of future tests, this directory also
|
|||
|
defines an NNAPI fuzzing test harness and packages it in a blueprint default
|
|||
|
`libneuralnetworks_fuzzer_defaults`.
|
|||
|
|
|||
|
Useful background reading and reference documents:
|
|||
|
* libFuzzer overview: http://llvm.org/docs/LibFuzzer.html
|
|||
|
* Android-specific libFuzzer documentation:
|
|||
|
https://source.android.com/devices/tech/debug/libfuzzer
|
|||
|
* Android Security Testing (sanitizers, fuzzer, etc.):
|
|||
|
https://source.android.com/devices/tech/debug/fuzz-sanitize
|
|||
|
* Sanitizer flags:
|
|||
|
https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
|
|||
|
* Address Sanitizer flags:
|
|||
|
https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
|
|||
|
* libprotobuf-mutator:
|
|||
|
https://github.com/google/libprotobuf-mutator#libprotobuf-mutator
|
|||
|
|
|||
|
# Setting up the test <a id="setting_up_the_test"></a>
|
|||
|
|
|||
|
## Developing an NNAPI fuzz test <a id="developing_an_nnapi_fuzz_test"></a>
|
|||
|
|
|||
|
### Creating a new fuzz test using `libneuralnetworks_fuzzer_defaults`
|
|||
|
|
|||
|
To create a new fuzz test:
|
|||
|
1. Create code that implements the function
|
|||
|
`void nnapiFuzzTest(const TestModel& testModel)` (examples: [1][1], [2][2])
|
|||
|
2. Create a blueprint `cc_fuzz` target that includes
|
|||
|
`libneuralnetworks_fuzzer_defaults` as a default (examples: [1][3], [2][4])
|
|||
|
|
|||
|
### Modifying `libneuralnetworks_driver_fuzzer` to test custom driver
|
|||
|
|
|||
|
Alter the `libneuralnetworks_driver_fuzzer` code locally to test your own
|
|||
|
driver. In the section `“TODO: INSERT CUSTOM DEVICE HERE”`, replace
|
|||
|
`“std::make_shared<const sample::Device>("example-driver")”` ([link][5]) with
|
|||
|
your own driver.
|
|||
|
|
|||
|
This code employs an in-process driver (as opposed to retrieving it on the
|
|||
|
device via `IDevice::getService(...))` for three reasons. First, the test runs
|
|||
|
faster because it does not need to communicate with the driver via IPC because
|
|||
|
the driver is created in the same process. Second, it ensures that the
|
|||
|
`libFuzzer` can use the coverage from the driver to guide the test
|
|||
|
appropriately, as everything is built as one unit. Finally, whenever a crash
|
|||
|
occurs, only one stacktrace needs to be analyzed to debug the problem.
|
|||
|
|
|||
|
The current version of the test assumes a 1.3 driver and uses the methods
|
|||
|
`IDevice::prepareModel` and `IDevice::execute` ([link][6]). Change the test
|
|||
|
locally to test different methods or different driver versions.
|
|||
|
|
|||
|
## Preparing a device <a id="preparing_a_device"></a>
|
|||
|
|
|||
|
Because the test is self-contained, you should be able to just use a regular
|
|||
|
device image without any modifications. The next section
|
|||
|
[Building and uploading fuzz test](#building-and-uploading-fuzz-test) describes
|
|||
|
how to build the test binary itself. If you need to have the entire image
|
|||
|
fuzzed (for example, if you want to sanitize a shared library), you can build a
|
|||
|
sanitized image with one of the following two sequences of commands depending
|
|||
|
on your needs:
|
|||
|
|
|||
|
### You can build a normal device image with:
|
|||
|
```bash
|
|||
|
$ . build/envsetup.sh
|
|||
|
$ lunch <target> # e.g., <TARGET_PRODUCT>-userdebug
|
|||
|
$ mma -j
|
|||
|
```
|
|||
|
|
|||
|
## Building and uploading fuzz test <a id="building_and_uploading_fuzz_test"></a>
|
|||
|
|
|||
|
For simplicity and clarity, the rest of the code here will use the following
|
|||
|
environment variables:
|
|||
|
```
|
|||
|
$ FUZZER_NAME=libneuralnetworks_driver_fuzzer
|
|||
|
$ FUZZER_TARGET_ARCH=$(get_build_var TARGET_ARCH)
|
|||
|
$ FUZZER_TARGET_DIR=/data/fuzz/$FUZZER_TARGET_ARCH/$FUZZER_NAME
|
|||
|
$ FUZZER_TARGET=$FUZZER_TARGET_DIR/$FUZZER_NAME
|
|||
|
```
|
|||
|
|
|||
|
When building with a non-sanitized lunch target, build the fuzz test with the
|
|||
|
following command:
|
|||
|
```bash
|
|||
|
$ SANITIZE_TARGET=hwaddress m $FUZZER_NAME -j
|
|||
|
```
|
|||
|
|
|||
|
Note that the above commands use `hwaddress` sanitization, but other sanitizers
|
|||
|
can be used in place of or in addition to `hwaddress`. More command options for
|
|||
|
building with other sanitizers can be found [here][7], and they are explained
|
|||
|
more in depth in the Android background reading [here][8].
|
|||
|
|
|||
|
Once the test is built, it can be pushed to the device via:
|
|||
|
```bash
|
|||
|
$ adb root
|
|||
|
$ adb sync data
|
|||
|
$ adb shell mkdir -p $FUZZER_TARGET_DIR/dump
|
|||
|
```
|
|||
|
|
|||
|
The directory `$FUZZER_TARGET_DIR/` is now as follows:
|
|||
|
* `$FUZZER_NAME` -- fuzz test binary
|
|||
|
* `corpus/` -- directory for reference/example “good” test cases, used to speed
|
|||
|
up fuzz tests
|
|||
|
* `dump/` -- sandbox directory used by the fuzz test; this can be ignored
|
|||
|
* `crash-*` -- any future problematic test cases will be dumped to the directory
|
|||
|
|
|||
|
# Running the test <a id="running_the_test"></a>
|
|||
|
|
|||
|
## Running the full fuzz test <a id="running_the_full_fuzz_test"></a>
|
|||
|
|
|||
|
The fuzz test can be launched with the following command, and will continue
|
|||
|
running until the user terminates the process (e.g., ctrl+c) or until the test
|
|||
|
crashes.
|
|||
|
|
|||
|
```bash
|
|||
|
$ adb shell HWASAN_OPTIONS=handle_sigill=2:handle_sigfpe=2:handle_sigbus=2:handle_abort=2:handle_segv=2 $FUZZER_TARGET $FUZZER_TARGET_DIR/dump/ $FUZZER_TARGET_DIR/corpus/ -artifact_prefix=$FUZZER_TARGET_DIR/
|
|||
|
```
|
|||
|
|
|||
|
(When using a non-hwasan build, you need to change the `HWASAN_OPTIONS`
|
|||
|
variable to match whatever build you’re using, e.g., `ASAN_OPTIONS`.)
|
|||
|
|
|||
|
When something unexpected occurs (e.g., a crash or a very slow test case), the
|
|||
|
test case that causes it will be dumped to a file in the directory specified by
|
|||
|
“`-artifact_prefix`”. The generated file will appear as
|
|||
|
`slow-unit-<unique_identifier>`, `crash-<unique_identifier>`,
|
|||
|
`oom-<unique_identifier>`, or `timeout-<unique_identifier>`. Normally,
|
|||
|
`libFuzzer` crash files will contain unreadable binary data; however,
|
|||
|
`libneuralnetworks_driver_fuzzer`‘s output is formatted in a human readable way
|
|||
|
because it uses `libprotobuf-mutator`, so it’s fine to inspect the file to get
|
|||
|
more information on the test case that caused the problem. For more
|
|||
|
information, refer to the [Fuzz test case format](#fuzz-test-case-format)
|
|||
|
section below.
|
|||
|
|
|||
|
## Reproducing crash case <a id="reproducing_crash_case"></a>
|
|||
|
|
|||
|
When a crash occurs, the crash test case can be re-run with the following
|
|||
|
command:
|
|||
|
|
|||
|
```bash
|
|||
|
$ adb shell HWASAN_OPTIONS=handle_sigill=2:handle_sigfpe=2:handle_sigbus=2:handle_abort=2:handle_segv=2 $FUZZER_TARGET $FUZZER_TARGET_DIR/<test_case_name>
|
|||
|
```
|
|||
|
(Note that the execution parameters for `HWASAN_OPTIONS` are the same as those
|
|||
|
above.)
|
|||
|
|
|||
|
E.g., `<test_case_name>` could be:
|
|||
|
* `minimized-from-15b1dae0d2872d8dccf4f35fbf4ecbecee697a49`
|
|||
|
* `slow-unit-cad88bd58853b71b875ac048001b78f7a7501dc3`
|
|||
|
* `crash-07cb8793bbc65ab010382c0f8d40087897826129`
|
|||
|
|
|||
|
## Finding minimal crash case <a id="finding_minimal_crash_case"></a>
|
|||
|
|
|||
|
When a crash occurs, sometimes the offending test case is large and
|
|||
|
complicated. `libFuzzer` has a way to minimize the crashing case to simplify
|
|||
|
debugging with the following command:
|
|||
|
|
|||
|
```bash
|
|||
|
$ adb shell HWASAN_OPTIONS=handle_sigill=2:handle_sigfpe=2:handle_sigbus=2:handle_abort=2:handle_segv=2 $FUZZER_TARGET $FUZZER_TARGET_DIR/<test_case_name> -artifact_prefix=$FUZZER_TARGET_DIR/ -minimize_crash=1 -max_total_time=60
|
|||
|
```
|
|||
|
(Note that the execution parameters for `HWASAN_OPTIONS` are the same as those
|
|||
|
above.)
|
|||
|
|
|||
|
Note that the `<test_case_name>` must be some sort of crash for the
|
|||
|
minimization to work. For example, minimization will not work on something like
|
|||
|
`slow_unit-*` cases. Increasing the `max_total_time` value may yield a more
|
|||
|
minimal test crash, but will take longer.
|
|||
|
|
|||
|
# Fuzz test case format <a id="fuzz_test_case_format"></a>
|
|||
|
|
|||
|
By itself, `libFuzzer` will generate a random collection of bytes as input to
|
|||
|
the fuzz test. The test developer then needs to convert this random data to
|
|||
|
some structured testing format (e.g., a syntactically correct NNAPI model).
|
|||
|
Doing this conversion can be slow and difficult, and can lead to inefficient
|
|||
|
mutations and tests. Additionally, whenever the fuzz test finds a crashing test
|
|||
|
case, it will dump this test case as an unreadable binary chunk of data in a
|
|||
|
file (e.g., `crash-*` files described above).
|
|||
|
|
|||
|
To help with both of these issues, the NNAPI fuzz tests additionally use a
|
|||
|
library called [`libprotobuf-mutator`][9] to handle the conversions from the
|
|||
|
random `libFuzzer` input to a protobuf format used for NNAPI fuzz testing. The
|
|||
|
conversion from this protobuf format to a model format is much more
|
|||
|
straightforward and efficient. As another useful utility, `libprotobuf-mutator`
|
|||
|
provides the option to represent this data as human-readable text. This means
|
|||
|
that whenever the fuzz test finds a crash, the resultant test case that is
|
|||
|
dumped to a file will be in a human-readable format.
|
|||
|
|
|||
|
Here is one example of a crash case that was found:
|
|||
|
```protobuf
|
|||
|
model {
|
|||
|
main {
|
|||
|
operands {
|
|||
|
operand {
|
|||
|
type: TENSOR_QUANT8_ASYMM
|
|||
|
scale: 1
|
|||
|
}
|
|||
|
operand {
|
|||
|
type: TENSOR_INT32
|
|||
|
dimensions {
|
|||
|
dimension: 1
|
|||
|
}
|
|||
|
lifetime: CONSTANT_COPY
|
|||
|
}
|
|||
|
operand {
|
|||
|
type: TENSOR_QUANT8_ASYMM
|
|||
|
dimensions {
|
|||
|
dimension: 9
|
|||
|
}
|
|||
|
scale: 1
|
|||
|
lifetime: SUBGRAPH_OUTPUT
|
|||
|
}
|
|||
|
operand {
|
|||
|
type: TENSOR_QUANT8_ASYMM
|
|||
|
dimensions {
|
|||
|
dimension: 1
|
|||
|
dimension: 1
|
|||
|
dimension: 3
|
|||
|
dimension: 3
|
|||
|
}
|
|||
|
scale: 1
|
|||
|
lifetime: SUBGRAPH_INPUT
|
|||
|
}
|
|||
|
operand {
|
|||
|
type: TENSOR_QUANT8_ASYMM
|
|||
|
dimensions {
|
|||
|
dimension: 1
|
|||
|
}
|
|||
|
scale: 1
|
|||
|
lifetime: CONSTANT_COPY
|
|||
|
}
|
|||
|
operand {
|
|||
|
type: INT32
|
|||
|
lifetime: CONSTANT_COPY
|
|||
|
}
|
|||
|
}
|
|||
|
operations {
|
|||
|
operation {
|
|||
|
inputs {
|
|||
|
index: 3
|
|||
|
index: 4
|
|||
|
index: 5
|
|||
|
}
|
|||
|
outputs {
|
|||
|
index: 0
|
|||
|
}
|
|||
|
}
|
|||
|
operation {
|
|||
|
type: TILE
|
|||
|
inputs {
|
|||
|
index: 0
|
|||
|
index: 1
|
|||
|
}
|
|||
|
outputs {
|
|||
|
index: 2
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
input_indexes {
|
|||
|
index: 3
|
|||
|
}
|
|||
|
output_indexes {
|
|||
|
index: 2
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
This format is largely based on the format defined in [NNAPI HAL][10]. The one
|
|||
|
major exception is that the contents of an operand's data are replaced by data
|
|||
|
generated from the “Buffer” message (except for `TEMPORARY_VARIABLE`,
|
|||
|
`NO_VALUE`, and `SUBGRAPH_OUTPUT` operands, in which cases there is no data or
|
|||
|
the data is ignored, so the “Buffer” message is ignored). This is done for a
|
|||
|
practical reason: `libFuzzer` (and by extension `libprotobuf-mutator`) converge
|
|||
|
slower when the amount of randomly generated input is large. For the fuzz tests,
|
|||
|
the contents of the operand data are not as interesting as the structure of the
|
|||
|
graph itself, so the data was replaced by a “Buffer”, which is one of the
|
|||
|
following:
|
|||
|
* EmptyBuffer empty, represented no value
|
|||
|
* uint32_t scalar, repesenting a scalar value
|
|||
|
* uint32_t random_seed, used to generate random data
|
|||
|
|
|||
|
The NNAPI `libprotobuf-mutator` implementation uses the proto3 specification.
|
|||
|
Note that this means whenever a field contains the default value (e.g., uint32_t
|
|||
|
holds a value of 0), [that field is omitted from the text][11]. For example, in
|
|||
|
the test case listed above, `model.main.operations[0].operation.type` is omitted
|
|||
|
because it holds the value `ADD`.
|
|||
|
|
|||
|
# Changing Model.proto <a id="changing_model_proto"></a>
|
|||
|
|
|||
|
## Adding new operations or operands <a id="adding_new_operations_or_operands"></a>
|
|||
|
|
|||
|
When adding a new operation to the NNAPI, the fuzzer should be updated with the
|
|||
|
following steps:
|
|||
|
1. Add the new operation or operand to either `OperationType` or `OperandType`.
|
|||
|
in `Model.proto`
|
|||
|
2. Add a new `static_assert` for the new type in `StaticAssert.cpp` to make sure
|
|||
|
the value in `Model.proto` matches the value in `TestHarness` (e.g.,
|
|||
|
`TestOperationType` or `TestOperandType`)
|
|||
|
|
|||
|
## Expanding the structure <a id="expanding_the_structure"></a>
|
|||
|
|
|||
|
For any deeper changes to the `Model.proto` type (e.g., adding a new field):
|
|||
|
1. Make the change to `TestHarness`
|
|||
|
2. Make the corresponding change in `Model.proto` to mirror `TestHarness`
|
|||
|
3. Make the corresponding change in `Converter.cpp` to handle the conversion
|
|||
|
from `Model.proto` types to `TestHarness` types.
|
|||
|
4. Make the corresponding change in `GenerateCorpus.cpp` to handle the
|
|||
|
conversion from `TestHarness` types to `Model.proto` types.
|
|||
|
|
|||
|
## Backward-incompatible change <a id="backward_incompatible_change"></a>
|
|||
|
|
|||
|
Making an backward-incompatible change (e.g., removing proto fields) affects the
|
|||
|
existing corpus, so the corpus needs to be regenerated for the test to continue
|
|||
|
to run efficiently. In addition to the steps in
|
|||
|
[Expanding the structure](#expanding_struct) above, the corpus can be
|
|||
|
regenerated with the following steps:
|
|||
|
1. run libneuralnetworks_fuzzer_seed_corpus to generate the corpus entries
|
|||
|
2. Unzip the generated files (>8000 test cases)
|
|||
|
3. [Merge][12] the cases with libneuralnetworks_fuzzer to reduce the corpus size
|
|||
|
4. Take and rename the top 500 cases to seedXXX
|
|||
|
5. Update the corpus with the new seed files
|
|||
|
|
|||
|
# Appendix <a id="appendix"></a>
|
|||
|
|
|||
|
## Alternative ways to build the device image and test binary <a id="alternatives"></a>
|
|||
|
|
|||
|
You can build a pre-configured sanitized device image with:
|
|||
|
```bash
|
|||
|
$ . build/envsetup.sh
|
|||
|
$ lunch <sanitized_target> # e.g., <TARGET_PRODUCT>_hwasan-userdebug
|
|||
|
$ mma -j
|
|||
|
```
|
|||
|
|
|||
|
Alternatively, you can build other (read: non-sanitized) targets with the following command:
|
|||
|
```bash
|
|||
|
$ . build/envsetup.sh
|
|||
|
$ lunch <non-sanitized_target> # e.g., <TARGET_PRODUCT>-userdebug
|
|||
|
$ SANITIZE_TARGET=hwaddress mma -j
|
|||
|
```
|
|||
|
|
|||
|
When using a sanitized lunch target, build the fuzz test with the following
|
|||
|
command:
|
|||
|
```bash
|
|||
|
$ m $FUZZER_NAME -j
|
|||
|
```
|
|||
|
|
|||
|
[1]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/DriverFuzzTest.cpp;l=307-324;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
|
|||
|
[2]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/FuzzTest.cpp;l=130-151;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
|
|||
|
[3]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/Android.bp;l=195-216;drc=60823f07172e6b5bbc06b2fac25a15ab91c80b25
|
|||
|
[4]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/Android.bp;l=218-240;drc=60823f07172e6b5bbc06b2fac25a15ab91c80b25
|
|||
|
[5]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/DriverFuzzTest.cpp?q=example-driver&ss=android%2Fplatform%2Fsuperproject
|
|||
|
[6]: https://cs.android.com/search?q=prepareModel%20execute&ss=android%2Fplatform%2Fsuperproject:packages%2Fmodules%2FNeuralNetworks%2Fruntime%2Ftest%2Fandroid_fuzzing%2F
|
|||
|
[7]: https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/sanitize.go;l=140-187;drc=b5b2aba43b5bb6305ea69d60f9bf580f711d7c96
|
|||
|
[8]: https://source.android.com/devices/tech/debug/libfuzzer
|
|||
|
[9]: https://cs.android.com/android/platform/superproject/+/master:external/libprotobuf-mutator/
|
|||
|
[10]: https://cs.android.com/android/platform/superproject/+/master:hardware/interfaces/neuralnetworks/
|
|||
|
[11]: https://developers.google.com/protocol-buffers/docs/proto3#default
|
|||
|
[12]: https://llvm.org/docs/LibFuzzer.html#corpus
|