README.md
1# Background
2
3This document seeks to be a crash-course and cheat-sheet for running the NNAPI
4fuzz tests.
5
6The purpose of fuzz testing is to find crashes, assertions, memory violations,
7or general undefined behavior in the code under test due to factors such as
8unexpected inputs. For NNAPI fuzz testing, Android uses tests based on
9`libFuzzer`, which are efficient at fuzzing because they use line coverage of
10previous test cases to generate new random inputs. For example, `libFuzzer`
11favors test cases that run on uncovered lines of code. This greatly reduces the
12amount of time tests take to find problematic code.
13
14Currently, there are two NNAPI fuzz test targets: `libneuralnetworks_fuzzer`
15which tests at the NNAPI NDK layer (testing libneuralnetworks as a static
16library) and `libneuralnetworks_driver_fuzzer` which tests an in-process driver
17at the NNAPI HAL layer (the sample driver, unless the test is modified to do
18otherwise). To simplify development of future tests, this directory also
19defines an NNAPI fuzzing test harness and packages it in a blueprint default
20`libneuralnetworks_fuzzer_defaults`.
21
22Useful background reading and reference documents:
23* libFuzzer overview: http://llvm.org/docs/LibFuzzer.html
24* Android-specific libFuzzer documentation:
25 https://source.android.com/devices/tech/debug/libfuzzer
26* Android Security Testing (sanitizers, fuzzer, etc.):
27 https://source.android.com/devices/tech/debug/fuzz-sanitize
28* Sanitizer flags:
29 https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
30* Address Sanitizer flags:
31 https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
32* libprotobuf-mutator:
33 https://github.com/google/libprotobuf-mutator#libprotobuf-mutator
34
35# Setting up the test
36
37## Developing an NNAPI fuzz test
38
39### Creating a new fuzz test using `libneuralnetworks_fuzzer_defaults`
40
41To create a new fuzz test:
421. Create code that implements the function
43 `void nnapiFuzzTest(const TestModel& testModel)` (examples: [1][1], [2][2])
442. Create a blueprint `cc_fuzz` target that includes
45 `libneuralnetworks_fuzzer_defaults` as a default (examples: [1][3], [2][4])
46
47### Modifying `libneuralnetworks_driver_fuzzer` to test custom driver
48
49Alter the `libneuralnetworks_driver_fuzzer` code locally to test your own
50driver. In the section `“TODO: INSERT CUSTOM DEVICE HERE”`, replace
51`“new nn::sample_driver::SampleDriverFull(…);”` ([link][5]) with your own
52driver.
53
54This code employs an in-process driver (as opposed to retrieving it on the
55device via `IDevice::getService(...))` for three reasons. First, the test runs
56faster because it does not need to communicate with the driver via IPC because
57the driver is created in the same process. Second, it ensures that the
58`libFuzzer` can use the coverage from the driver to guide the test
59appropriately, as everything is built as one unit. Finally, whenever a crash
60occurs, only one stacktrace needs to be analyzed to debug the problem.
61
62The current version of the test assumes a 1.3 driver and uses the methods
63`IDevice::prepareModel_1_3` and `IDevice::executeSynchronously_1_3`
64([link][6]). Change the test locally to test different methods or different
65driver versions.
66
67## Preparing a device
68
69Because the test is self-contained, you should be able to just use a regular
70device image without any modifications. The next section
71[Building and uploading fuzz test](#building-and-uploading-fuzz-test) describes
72how to build the test binary itself. If you need to have the entire image
73fuzzed (for example, if you want to sanitize a shared library), you can build a
74sanitized image with one of the following two sequences of commands depending
75on your needs:
76
77### You can build a pre-configured sanitized device image with:
78```bash
79$ . build/envsetup.sh
80$ lunch <sanitized_target> # e.g., <TARGET_PRODUCT>_hwasan-userdebug
81$ mma -j
82```
83
84### Alternatively, you can build other (read: non-sanitized) targets with the following command:
85```bash
86$ . build/envsetup.sh
87$ lunch <non-sanitized_target> # e.g., <TARGET_PRODUCT>-userdebug
88$ SANITIZE_TARGET=hwaddress mma -j
89```
90
91## Building and uploading fuzz test
92
93For simplicity and clarity, the rest of the code here will use the following
94environment variables:
95```
96$ FUZZER_NAME=libneuralnetworks_driver_fuzzer
97$ FUZZER_TARGET_ARCH=$(get_build_var TARGET_ARCH)
98$ FUZZER_TARGET_DIR=/data/fuzz/$FUZZER_TARGET_ARCH/$FUZZER_NAME
99$ FUZZER_TARGET=$FUZZER_TARGET_DIR/$FUZZER_NAME
100```
101
102When using a sanitized lunch target, build the fuzz test with the following
103command:
104```bash
105$ m $FUZZER_NAME -j
106```
107
108When building with a non-sanitized lunch target, build the fuzz test with the
109following command:
110```bash
111$ SANITIZE_TARGET=hwaddress m $FUZZER_NAME -j
112```
113
114Note that the above commands use `hwaddress` sanitization, but other sanitizers
115can be used in place of or in addition to `hwaddress`. More command options for
116building with other sanitizers can be found [here][7], and they are explained
117more in depth in the Android background reading [here][8].
118
119Once the test is built, it can be pushed to the device via:
120```bash
121$ adb root
122$ adb sync data
123$ adb shell mkdir -p $FUZZER_TARGET_DIR/dump
124```
125
126The directory `$FUZZER_TARGET_DIR/` is now as follows:
127* `$FUZZER_NAME` -- fuzz test binary
128* `corpus/` -- directory for reference/example “good” test cases, used to speed
129 up fuzz tests
130* `dump/` -- sandbox directory used by the fuzz test; this can be ignored
131* `crash-*` -- any future problematic test cases will be dumped to the directory
132
133# Running the test
134
135## Running the full fuzz test
136
137The fuzz test can be launched with the following command, and will continue
138running until the user terminates the process (e.g., ctrl+c) or until the test
139crashes.
140
141```bash
142$ 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/
143```
144
145(When using a non-hwasan build, you need to change the `HWASAN_OPTIONS`
146variable to match whatever build you’re using, e.g., `ASAN_OPTIONS`.)
147
148When something unexpected occurs (e.g., a crash or a very slow test case), the
149test case that causes it will be dumped to a file in the directory specified by
150“`-artifact_prefix`”. The generated file will appear as
151`slow-unit-<unique_identifier>`, `crash-<unique_identifier>`,
152`oom-<unique_identifier>`, or `timeout-<unique_identifier>`. Normally,
153`libFuzzer` crash files will contain unreadable binary data; however,
154`libneuralnetworks_driver_fuzzer`‘s output is formatted in a human readable way
155because it uses `libprotobuf-mutator`, so it’s fine to inspect the file to get
156more information on the test case that caused the problem. For more
157information, refer to the [Fuzz test case format](#fuzz-test-case-format)
158section below.
159
160## Reproducing crash case
161
162When a crash occurs, the crash test case can be re-run with the following
163command:
164
165```bash
166$ 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>
167```
168(Note that the execution parameters for `HWASAN_OPTIONS` are the same as those
169above.)
170
171E.g., `<test_case_name>` could be:
172* `minimized-from-15b1dae0d2872d8dccf4f35fbf4ecbecee697a49`
173* `slow-unit-cad88bd58853b71b875ac048001b78f7a7501dc3`
174* `crash-07cb8793bbc65ab010382c0f8d40087897826129`
175
176# Finding minimal crash case
177
178When a crash occurs, sometimes the offending test case is large and
179complicated. `libFuzzer` has a way to minimize the crashing case to simplify
180debugging with the following command:
181
182```bash
183$ 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
184```
185(Note that the execution parameters for `HWASAN_OPTIONS` are the same as those
186above.)
187
188Note that the `<test_case_name>` must be some sort of crash for the
189minimization to work. For example, minimization will not work on something like
190`slow_unit-*` cases. Increasing the `max_total_time` value may yield a more
191minimal test crash, but will take longer.
192
193## Fuzz test case format
194
195By itself, `libFuzzer` will generate a random collection of bytes as input to
196the fuzz test. The test developer then needs to convert this random data to
197some structured testing format (e.g., a syntactically correct NNAPI model).
198Doing this conversion can be slow and difficult, and can lead to inefficient
199mutations and tests. Additionally, whenever the fuzz test finds a crashing test
200case, it will dump this test case as an unreadable binary chunk of data in a
201file (e.g., `crash-*` files described above).
202
203To help with both of these issues, the NNAPI fuzz tests additionally use a
204library called [`libprotobuf-mutator`][9] to handle the conversions from the
205random `libFuzzer` input to a protobuf format used for NNAPI fuzz testing. The
206conversion from this protobuf format to a model format is much more
207straightforward and efficient. As another useful utility, `libprotobuf-mutator`
208provides the option to represent this data as human-readable text. This means
209that whenever the fuzz test finds a crash, the resultant test case that is
210dumped to a file will be in a human-readable format.
211
212Here is one example of a crash case that was found:
213```
214model {
215 operands {
216 operand {
217 type: TENSOR_INT32
218 dimensions {
219 dimension: 1
220 }
221 scale: 0
222 zero_point: 0
223 lifetime: TEMPORARY_VARIABLE
224 channel_quant {
225 scales {
226 }
227 channel_dim: 0
228 }
229 data {
230 random_seed: 4
231 }
232 }
233 operand {
234 type: TENSOR_FLOAT32
235 dimensions {
236 dimension: 2
237 dimension: 4
238 }
239 scale: 0
240 zero_point: 0
241 lifetime: TEMPORARY_VARIABLE
242 channel_quant {
243 scales {
244 }
245 channel_dim: 0
246 }
247 data {
248 random_seed: 0
249 }
250 }
251 operand {
252 type: TENSOR_FLOAT32
253 dimensions {
254 }
255 scale: 0
256 zero_point: 0
257 lifetime: SUBGRAPH_OUTPUT
258 channel_quant {
259 scales {
260 }
261 channel_dim: 27
262 }
263 data {
264 random_seed: 0
265 }
266 }
267 }
268 operations {
269 operation {
270 type: EMBEDDING_LOOKUP
271 inputs {
272 index: 0
273 index: 1
274 }
275 outputs {
276 index: 2
277 }
278 }
279 }
280 input_indexes {
281 index: 0
282 index: 1
283 }
284 output_indexes {
285 index: 2
286 }
287 is_relaxed: true
288}
289```
290
291This format is largely based on the format defined in [NNAPI HAL][10]. The one
292major exception is that the contents of an operand's data are replaced by data
293generated from “random_seed” (except for `TEMPORARY_VARIABLE` and `NO_VALUE`
294operands, in which cases there is no data, so "random_seed" is ignored). This
295is done for a practical reason: `libFuzzer` (and by extension
296`libprotobuf-mutator`) converge slower when the amount of randomly generated
297input is large. For the fuzz tests, the contents of the operand data are not as
298interesting as the structure of the graph itself, so the data was replaced by
299a seed to a random number generator instead.
300
301[1]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/DriverFuzzTest.cpp;l=307-324;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
302[2]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/FuzzTest.cpp;l=130-151;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
303[3]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/Android.bp;l=195-216;drc=60823f07172e6b5bbc06b2fac25a15ab91c80b25
304[4]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/Android.bp;l=218-240;drc=60823f07172e6b5bbc06b2fac25a15ab91c80b25
305[5]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/DriverFuzzTest.cpp;l=48-52;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
306[6]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NeuralNetworks/runtime/test/android_fuzzing/DriverFuzzTest.cpp;l=291-292,302;drc=34aee872d5dc317ad8a32377e9114c0c606d8afe
307[7]: https://cs.android.com/android/platform/superproject/+/master:build/soong/cc/sanitize.go;l=140-187;drc=b5b2aba43b5bb6305ea69d60f9bf580f711d7c96
308[8]: https://source.android.com/devices/tech/debug/libfuzzer
309[9]: https://cs.android.com/android/platform/superproject/+/master:external/libprotobuf-mutator/
310[10]: https://cs.android.com/android/platform/superproject/+/master:hardware/interfaces/neuralnetworks/
311