0
6
Login
Code
Issues
1
Pull requests
Events
Packages
0b0217bce1893d56c2a541a081347e17764c736e
0b0217bce1893d56c2a541a081347e17764c736e

Table of contents

  1. Background
  2. Setting up the test
    1. Developing an NNAPI fuzz test
    2. Preparing a device
    3. Building and uploading fuzz test
  3. Running the test
    1. Running the full fuzz test
    2. Reproducing crash case
    3. Finding minimal crash case
  4. Fuzz test case format
  5. Changing Model.proto
    1. Adding new operations or operands
    2. Expanding the structure
    3. Backward-incompatible change
  6. Appendix
    1. Alternative ways to build the device image and test binary

Background

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:

Setting up the test

Developing an NNAPI fuzz test

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, 2)
  2. Create a blueprint cc_fuzz target that includes libneuralnetworks_fuzzer_defaults as a default (examples: 1, 2)

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) 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). Change the test locally to test different methods or different driver versions.

Preparing a device

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 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:

$ . build/envsetup.sh
$ lunch <target>  # e.g., <TARGET_PRODUCT>-userdebug
$ mma -j

Building and uploading fuzz test

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:

$ 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, and they are explained more in depth in the Android background reading here.

Once the test is built, it can be pushed to the device via:

$ 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

Running the full fuzz test

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.

$ 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 section below.

Reproducing crash case

When a crash occurs, the crash test case can be re-run with the following command:

$ 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

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:

$ 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

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 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:

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. 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. 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

Adding new operations or operands

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

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

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 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 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

Alternative ways to build the device image and test binary

You can build a pre-configured sanitized device image with:

$ . 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:

$ . 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:

$ m $FUZZER_NAME -j