4. GNATfuzz User’s Guide

4.1. Introduction

GNATfuzz provides fuzz-testing facilities for Ada projects. The main features it provides are:

  • Identifying subprograms that are suitable for automated fuzz-testing (referred to as automatically fuzzable subprograms).

  • Generating fuzz-test harnesses that receive binary encoded data, invoke the subprogram under test, and report run-time anomalies.

  • Generating fuzz-test initial test cases.

  • Building and executing fuzz-testing campaigns controlled by user-defined stopping criteria.

  • Real-time test coverage analysis through integration with GNATcoverage.

These features are available both via the command line and through integration with the GNAT Studio IDE.

The fuzzing engine in GNATfuzz is based on AFL++, a popular open-source and highly configurable fuzzer, and an instrumentation plugin shipped as part of the GNAT Pro toolchain.

The primary goal of GNATfuzz is to abstract a lot of the technical details needed to set up, run, and understand the outcome of a fuzzing session on an application.

4.1.1. Fuzz testing in a nutshell

Note

In the context of GNATfuzz, we refer to the project or program that you’re targeting with fuzz testing as the “system under test”.

Fuzzing (or fuzz testing) in the context of software is an automated testing technique that, based on some initial input test cases (the corpus) for a program, will automatically and repeatedly run tests and generate new test cases at a very high frequency to detect faulty behavior of the system under test. Such erroneous behavior is captured by monitoring the system for triggered exceptions, failing built-in assertions, and signals such as SIGSEGV (which shouldn’t occur in Ada programs unless they’re erroneous).

Fuzz testing is now widely associated with cybersecurity. It has proven to be an effective mechanism for finding corner-case code vulnerabilities that traditional human-driven verification mechanisms, such as unit and integration testing, can miss. Such code vulnerabilities can often lead to malicious exploitations.

Fuzz testing comes in different flavors. Black-box fuzz testing refers to the approach of randomly generating test cases without taking into account any information about the internal structure, functional behavior, or execution of the system under test. AFL++’s native execution mode is considered a grey box fuzzer because it obtains code coverage by using code instrumentation information. This information helps the mutation and generation phase of the fuzzer generate test cases that have a higher probability of increasing code coverage than randomly generated test cases would. This instrumentation is added automatically by GNATfuzz during the fuzz mode, explained in detail later in this guide.

4.1.2. AFL++ basics

This section contains a very high-level overview of AFL++, a highly-configurable, instrumentation-guided genetic fuzzer. A GCC plugin, enabled via the “afl-gcc-fast” compiler driver directive, generates efficient code instrumentation that enables AFL++ to process GCC-compiled code. Once the system under test is compiled with the AFL++ GCC plugin, you can use the “afl-fuzz” fuzzing driver to run a fuzzing session. The different basic elements and phases involved in an AFL++ fuzzing session are described in Fig. 4.1

Note

This document is not intended to be a substitute for the AFL++ documentation or that of its GCC plugin extensions, but rather to provide some information needed to understand AFL++ and how it works. In this document, we mostly focus on the added value of the GNATfuzz toolchain. For more information about AFL++, you can visit the AFL++ official site.

alternate text

Fig. 4.1 Overview of an AFL++ fuzz testing session

Prior to starting a fuzzing session, you must provide some initial test cases, called the corpus, each in a separate file. Starting a fuzzing session moves the corpus to what’s called the test cases queue, which isn’t really a queue, but instead a directory containing a list of files, each representing a test case. AFL++ adds tests to the queue, but never removes any from it.

The details are complex, but in general, for each test case in the queue, AFL++ uses one or more internal or external mutators to create multiple new test cases (it can create between hundreds and tens of thousands of new tests from each test in the queue), each of which it executes on the system under test. It monitors each execution for any faulty behavior, as describe above, and moves any test cases that cause such behavior to a directory called crashes. AFL++ provides a configuration parameter which sets a timeout for each test. If the test exceeds the timeout, it’s killed and stored in the session’s hangs directory. Saving these tests allows you to replay and investigate these problematic test cases.

While running each test, AFL++ collects all the instrumentation data and updates its bitmap, an array global to the session where each index represents an instrumentation point added to the system under test during its compilation by the GCC AFL++ plugin. Whenever an instrumentation point is executed, its corresponding index in the bitmap is incremented. The bitmap enables the fuzzing session to be path-coverage aware.

If a mutated test case executes a new path, it’s considered a good candidate to further increase the coverage when mutated and is added to the queue so AFL++ will generate further mutations of that test. The fuzzing session cycle counter is increased when all the test cases in the queue have been through the fuzzing process at least once. The cycle counter is part of the fuzzing statistics that GNATfuzz produces and stores during a session. An example file containing these statistics is shown below:

alternate text

Fig. 4.2 Overview of an AFL++ fuzzer_stats file (produced by afl-fuzz++3.00a)

Some of these statistics are also included in the fuzzing session information terminal (which can be optionally shown during the execution of GNATfuzz):

alternate text

Fig. 4.3 Fuzzing session information terminal example. (produced by afl-fuzz++3.00a)

Some of the most important statistics are:

  • start_time - start time of afl-fuzz (as a unix time)

  • last_update - last update of this file (as a unix time)

  • fuzzer_pid - PID of the fuzzer process

  • cycles_done - queue cycles completed so far

  • execs_done - number of execution calls to the system under test

  • execs_per_sec - current number of executions per second

  • corpus_count - total number of entries in the queue

  • corpus_found - number of entries discovered through local fuzzing

  • corpus_imported - number of entries imported from other instances

  • max_depth - number of levels in the generated data set

  • cur_item - entry number currently being processed

  • pending_favs - number of favored entries still waiting to be fuzzed

  • pending_total - number of all entries waiting to be fuzzed

  • stability - percentage of bitmap bytes that behave consistently

  • corpus_variable - number of test cases showing variable behavior

  • unique_crashes - number of unique crashes recorded

  • unique_hangs - number of unique hangs encountered

The complete list of all the statistics generated by AFL++ and the description of each can be found here. These statistics are meant to assist you in making informed decisions on when to stop a fuzzing session. Typically, based on the statistics, you can identify when a fuzzing session has plateaued, meaning that GNATfuzz has no further potential to explore new execution paths in the system under test.

Setting up, running, and stopping a fuzzing session can be challenging. You must build and execute a coverage campaign that allows you to understand the impact of fuzz-testing on your project. There are many steps to this. First, you must construct an adequate starting corpus. The quality of the starting corpus can significantly affect the fuzzing session’s results. The more meaningful the starting corpus is as an input to the system under test, the more coverage GNATfuzz can achieve. After providing the corpus, you need to wrap the system under test in a suitable test harness. Next, you have to build the wrapped system with the GCC AFL++ plugin to add instrumentation to the code. Then you need to call the command that starts the fuzzing session with all the required configuration flags and actively monitor it to decide if it has performed enough fuzzing.

AFL++ fuzzing is primarily used to fuzz programs that accept files as inputs. This makes it more challenging to use AFL++ as a form of unit-level vulnerability testing. The current built-in mutators will mutate test cases at the binary level and are unaware of any structure within the test cases. This can be problematic when, for example, it mutates static data that should not be changed, such as an array’s upper and lower bounds - their mutation can result in corruption of the test-case data resulting in a wasted mutation cycle.

4.1.3. GNATfuzz overview

GNATfuzz utilizes several AdaCore technologies to provide ease of use and enhanced fuzzing capability for Ada and SPARK programs. Fuzzing Ada and SPARK code has a significant advantage over fuzzing other traditional, less memory-safe languages, like C. Ada’s extended runtime constraint checks can capture faulty behavior, such as overflows, which often go undetected in C. This provides a significant advantage because the software assurance and security confidence that can be achieved when fuzzing Ada and SPARK programs is higher than when fuzzing C programs.

In a nutshell, GNATfuzz abstracts away the complexity of the building, executing, and stopping fuzzing campaigns and enables unit-level-based fuzz-testing for Ada and SPARK programs by:

  • Automating the detection of fuzzable subprograms.

  • Automating the creation of the test-harness for a fuzzable subprogram.

  • Automating the creation and minimization of the starting corpus.

  • Allowing for simple specification of criteria for stopping the session and monitoring how those criteria are being met during the session.

  • Leveraging the GNATcoverage tool to provide coverage information during or after a fuzzing session.

  • Supporting multicore fuzzing.

alternate text

Fig. 4.4 GNATfuzz overview

Fig. 4.4 gives an overview of the GNATfuzz toolchain. GNATfuzz offers several modes which you can use to largely automate the manual activities needed for setting up and running a fuzz-test campaign. The green boxes in Fig. 4.4 represent the four main operating modes, of GNATfuzz: analyze, generate, corpus-gen, and fuzz.

The gnatfuzz command line interface provides complete access to these features and the IDE integration in GNAT Studio provides a full UI workflow to drive the command line interface.

We start by giving a very high-level description of the GNATfuzz workflow and then provide details about the information footprints of each GNATfuzz mode and how each works in later sections.

The workflow shown in Fig. 4.4 is intended for unit-level fuzz testing. In this scenario, you first identify fuzzable subprograms within the system under test. You do this by specifying the project file of the system under test as an argument to the analyze mode of GNATfuzz. This mode scans the project for all fuzzable subprograms and lists them in a JSON file, including the source file and line information for each. Subprograms considered to be fuzzable are those for which the GNATfuzz generate mode can automatically generate a starting corpus. See Section 4.3.2 for the auto-generation capabilities of the current version of GNATfuzz.

You then select one of these fuzzable subprograms and specify it as an argument to the generate mode of GNATfuzz. This mode creates a test harness for that subprogram, including all the execution scripts needed to run the fuzzing campaign and the input-data specifications for the subprogram. Based on these input-data specifications, you can automatically generate the starting corpus using the corpus-gen mode. After you run that mode, all the prerequisites needed to execute a fuzz-testing session are in place. The two last steps you might optionally want to perform are creating stopping criteria for the fuzzing campaign and manually providing some additional test cases for the starting corpus.

You can then call the GNATfuzz fuzz mode. This builds the system under test with AFL++ code instrumentation, using the AFL-GCC-fast compiler plugin. The system under test is also built with support for GNATcoverage; see the GNATcoverage User’s Guide for more information about the GNATcoverage tool.

After the system is built, you use the fuzz mode to execute one or more AFL++ fuzzing sessions. By default, GNATfuzz handles the multicore aspects of the fuzz testing campaign, such as allocating parallel fuzzing sessions of the system under test to available cores. It also periodically synchronizes the test-case queues and coverage reporting and monitors the stopping criteria between all the parallel fuzzing sessions. For details of the internal processes of a test fuzzing session, see Fig. 4.3.

Finally, all fuzzing activities will stop when the stopping criteria (either specified by you or by default) are met, at which point the final results of the fuzzer are ready for your inspection, including the set of crashing test cases, the set of test cases causing the system to timeout (the hangs), the coverage report, and the AFL++ fuzzing statistics.

In the following sections, we first provide detailed information on the various GNATfuzz modes, including their configuration, and then provide examples, first using the GNATfuzz command-line interface and then using the GNATstudio IDE GNATfuzz interface, to illustrate these modes.

4.2. GNATfuzz command line interface

This section describes the various possible parameters for each GNATfuzz mode. To see the detailed effect, the artifacts each switch can generate, and how these artifacts can be fed into the next mode of the GNATfuzz toolchain, see Section GNATfuzz by example.

Note

For this documentation, the ‘$’ sign denotes the system prompt.

4.2.1. GNATfuzz “analyze” mode command line interface

In this mode GNATfuzz has the following command line interface:

Listing 4.1 GNATfuzz fuzz-analyze-mode usage
  $ gnatfuzz analyze [--help|-h] [-P <PROJECT>] [-S <SPEC-FILE>] [-X <NAME=VALUE>] [-l] [-v]

where

  • - -help, -h

    Shows the usage information for the analyze mode

  • -P <PROJECT>

    Specifies the project file to be analyzed Mandatory

  • -S <SPEC-FILE>

    Targets the analysis at an Ada specification file within the given project

  • -X <NAME=VALUE>

    Sets the external variable NAME in the system-under-test’s GPR project to VALUE

4.2.2. GNATfuzz “generate” mode command line interface

In this mode GNATfuzz has the following command line interface:

Listing 4.2 GNATfuzz fuzz-generate-mode usage
  $ gnatfuzz generate [--help|-h] [-P <PROJECT>] [-S <SOURCE-FILE>] [-L <LINE-NUMBER>]
                      [--analysis <ANALYZE-JSON>] [--subprogram-id <SUBPROGRAM-ID>]
                      [-o <OUTPUT-DIRECTORY>] [-X <NAME=VALUE>]

where

  • - -help, -h

    Shows the usage information for the generate mode

  • -P <PROJECT>

    Specifies the project file to be analyzed Mandatory

  • -S <SOURCE-FILE>

    Specifies the source file of the subprogram under test Mandatory Requires: -L Mutually Exclusive: –analysis, –subprogram-id

  • -L

    Line number of the subprogram under test (indexed at 1) Mandatory Requires: -S Mutually Exclusive: –analysis, –subprogram-id

  • - -analysis

    Path to the analysis json generated by the analyze mode Mandatory Requires: –subprogram-id Mutually Exclusive: -S, -L

  • - -subprogram-id

    Id of the target fuzzable subprogram found in the analysis file Mandatory Requires: –analysis Mutually Exclusive: -S, -L

  • -o

    Path where the generated files are created Mandatory

  • -X <NAME=VALUE>

    Sets the external variable NAME in the system-under-test’s GPR project to the value VALUE

4.2.3. GNATfuzz “corpus-gen” mode command line interface

In this mode GNATfuzz has the following command line interface:

Listing 4.3 GNATfuzz corpus-gen-mode usage
$ gnatfuzz corpus-gen [--help|-h] [-P <PROJECT>] [-o <output-directory>]
                      [-X <NAME=VALUE>] [-l] [-v]

where

  • - -help, -h

    Shows the usage information for the corpus-gen mode

  • -P <PROJECT>

    Specifies the fuzz-test.gpr project file that we want to generate a corpus for Mandatory

  • -o

    Path where the generated files are created Mandatory

  • -X <NAME=VALUE>

    Sets the external variable NAME in the system-under-test’s GPR project to VALUE

4.2.4. GNATfuzz “fuzz” mode command line interface

In this mode GNATfuzz has the following command line interface:

Listing 4.4 GNATuzz fuzz-mode usage
$ gnatfuzz fuzz [--help|-h] [-P <PROJECT>] [-X <NAME=VALUE>] [--sut-flags SUT-FLAGS]
                [--cores CORES] [--corpus-path CORPUS-PATH] [--no-cmin]
                [--input-mode INPUT-MODE] [--seed SEED] [--afl-mode AFL-MODE]
                [--no-deterministic-phase] [--no-GNATcov]
                [--stop-criteria STOP-CRITERIA] [--ignore-stop-criteria]
                [-l] [-v]

where

  • - -help, -h

    Shows the usage information for the fuzz mode

  • -P <PROJECT>

    Specifies the project file to be analyzed Mandatory

  • -X <NAME=VALUE>

    Sets the external variable NAME in the system-under-test’s GPR project to VALUE

  • - -sut-flags

    Pass applications flags to the system-under-test

  • - -cores

    The number of cores the fuzzing campaign can utilize. If 0 (also the default if no value is passed), the campaign will utilize the maximum number of available cores. The provided value must be in the range of 0 to 255

  • - -corpus-path

    Path to the starting corpus to be used

  • - -no-cmin

    Do not run afl-cmin; afl-cmin reduces the starting corpus down to a set of test cases that all find unique paths through the control flow of the system-under-test

  • - -input-mode

    Specify how the test cases will be injected into the test harness. Possible alternatives: stdin, cmdline_param. Default: stdin

  • - -seed

    Use a fixed seed for AFL random number generators. This is mainly used for deterministic benchmarking. The provided value must be in the range of 0 to the maximum file size of the starting corpus test case files (in bytes)

  • - -afl-mode

    Specify the AFL_Mode to run. Possible alternatives: afl_defer_and_persist, afl_persist, afl_defer, afl_plain. Default: afl_plain

  • - -no-deterministic-phase

    Do not run the deterministic (sequential bit flip) mutation phase

  • - -no-GNATcov

    Do not run the (near) real-time dynamic coverage analysis using GNATcoverage (statement)

  • - -stop-criteria

    Override the default stop criteria rules with the provided

  • - -ignore-stop-criteria

    Ignore stop criteria rules (fuzzer must be manually stopped)

4.3. GNATfuzz by example

This section provides several examples to demonstrate the use of the GNATfuzz tool. These examples incrementally increase in complexity and represent the main capabilities of GNATfuzz. You can run these cases on your machine. All examples are located under:

<install_prefix>/share/examples/

4.3.1. Prerequisites and possible configurations

However, as discussed below, you need to be aware of some AFL++ and GNATfuzz requirements and configurations prior to using the technology.

4.3.1.1. AFL++ requirements

To allow the AFL++ tool, and therefore GNATfuzz, to run successfully, you need to execute the following commands as root (either by logging in as root or running su or sudo, as appropriate for your system) before running any fuzzing sessions:

$ echo core >/proc/sys/kernel/core_pattern
$ cd /sys/devices/system/cpu
$ echo performance | tee cpu*/cpufreq/scaling_governor

The first command avoids having crashes misinterpreted as timeouts and the second is required to improve the performance of a fuzz-testing session. For more information, see the relevant AFL++ implementation details.

4.3.1.2. GNATfuzz requirements

The current requirements for GNATfuzz are:

  1. GNATfuzz is currently only supported by GNAT Pro Ada x86 64 bit Linux distributions.

  2. GNATfuzz requires that Ada projects be built using GPR project files and the GPRbuild compilation tool.

Warning

GNATfuzz has no specific hardware requirements but is a highly resource-intensive application. Executing a fuzz-testing session could run thousands of simultaneous tests, which can occupy the majority of the resources of a computing system. Ensure you allocate the number of cores you feel comfortable with to a fuzz-testing session (see Section 4.2.4).

4.3.1.3. GNATfuzz configurations

GNATFUZZ_XTERM : by default, the display of the AFL++ terminals (see Screenshot 4.3) is disabled during a GNATfuzz testing session. To enable their display set this environment variable.

4.3.1.4. GNATfuzz and the AFL++ execution modes

AFL++ provides four fuzz-testing execution modes: plain, persistent, defer, and defer-and-persistent. You can choose one of these via the command line switch –afl-mode when running GNATfuzz in fuzz mode . If you don’t specify this switch, GNATfuzz uses the persistent mode.

Different AFL++ executions modes cause the GNATfuzz fuzz-testing session to behave in different ways. These behaviors are explained in the following sections.

4.3.1.4.1. GNATfuzz and the AFL++ ‘plain mode

When GNATfuzz runs in AFL++ plain mode, AFL++ forks a new process for the execution of each test. Doing so guarantees that all application memory is reset between tests, but AFL++ plain mode has a high overhead because forking new processes is resource-intensive.

4.3.1.4.2. GNATfuzz and the AFL++ persistent mode

When GNATfuzz runs in AFL++ persistent mode, AFL++ executes 10,000 tests sequentially within the same process before it spawns a new process to run the the next set of tests. Avoiding the cost of forking a new process for each test makes the persistent mode very efficient: the increase in the numbers of tests executed each second can easily be 10 or 20 times faster than in AFL++ plain mode.

However, while the state of the application itself is reset between each new process, including any statically allocated state or state that is created during program elaboration, any state in dynamically allocated memory is retained between test executions within the same process.

Therefore, persistent mode requires you to completely reset dynamically allocated memory. This ensures that resource leaks are avoided, the application state is not impacted by a previous execution, and each test execution is reproducible. For GNATfuzz, this applies both to any library resources that require closure to free memory and to any dynamically allocated state, for example, any memory allocated from a pool of available memory (often referred to as a heap) via the Ada keyword new.

You can find an example of how to reset dynamically allocated memory in the program under test before each test execution in Section 4.3.10

Warning

Persistent mode is only suitable for programs where you can completely reset the state so that multiple calls in the same process are performed without any resource leaks and earlier runs do not have any impact on later runs. One way to see if this is happening is to look at the stability value that you can monitor through the GNATfuzz stopping criteria and at the AFL++ terminal. A good indication that the fuzz target is retaining some state between test execution is if the stability decreases. In such a case, you either need to reset all the state of the program under test (including in dynamic memory) or, when non-feasible, switch to plain mode.

4.3.1.4.3. GNATfuzz and the AFL++ defer mode

GNATfuzz requires the application under test to terminate within a predefined time (the default timeout is one second). If the application takes longer than the timeout, GNATfuzz assumes the process is in a hung state and copies the associated test case into the hangs directory. However, a fuzz-test session is only practical if we can maintain a high rate of test execution, which in turn requires that most tests execute significantly faster than the timeout.

The AFL++ defer mode recognizes that some applications have initializations that take a long time. If such an initialization is repeated across all tests, you can use the defer mode to perform the initialization once and use it across all tests. Defer mode achieves this by delaying the initial fork of the main() application until a user-specified location is reached within the control flow.

4.3.1.4.4. GNATfuzz and the AFL++ defer-and-persistent mode

AFL++ defer-and-persistent mode combines the AFL++ defer and persistent modes. See above for a detailed explanation of these individual modes.

Note

The AFL++ defer and defer-and-persistent modes are currently not supported by GNATfuzz, but we expect to support them in subsequent GNATfuzz releases.

4.3.2. GNATfuzz automation capabilities and limitations

Section 4.1.3 provides an overview of the stages you need to use to get a fuzzing session up and running in an automated fashion, each corresponding to running one of the four operating modes of GNATfuzz; namely, analyze, generate, corpus-gen, and fuzz.

The degree to which GNATfuzz can automate a testing session is determined mainly by two factors: its ability to auto-generate test harnesses and its ability to auto-generate a starting corpus. We divide subprograms into three categories based on those two factors:

  1. Fully-supported fuzzable subprograms: ones for which GNATfuzz can auto-generate both a test-harness and a starting corpus.

  2. Partially-supported fuzzable subprograms: ones for which GNATfuzz can auto-generate a test-harness but not a starting corpus. In these cases, GNATfuzz provides a subprogram that allows you to provide your own seeds. GNATfuzz will encode the manually provided seeds and generate a starting corpus.

  3. Non-supported fuzzable subprograms: subprograms for which GNATfuzz can neither auto-generate a test-harness nor a starting corpus.

Which subprograms belong to each of the three categories will evolve with future releases of GNATfuzz, with the goal of increasing automation in later releases. Also, the outcome of a GNATfuzz campaign targeting a non-compatible subprogram is dependent on the nature of the incompatibility. In some cases, the test harness will fail to compile. In other cases, the test harness will build and execute but not produce anything meaningful. In future versions of GNATfuzz, incompatible subprograms will be identified during the analysis mode.

In the current version, a test harness can be auto-generated for a subprogram unless one of the following are true:

  1. Any of the subprogram’s “in” or “in out” mode parameters are of an Access type or contain a sub-component of an Access type.

  2. Any of the subprogram’s “in” or “in out” mode parameter types are private.

  3. Any of the “in” or “in out” mode subprogram’s parameters are Subprogram Access Types.

  4. Any of the “in” or “in out” mode subprogram’s parameters are Limited types.

  5. Any of the “out” mode subprogram’s parameters are unconstrained records or arrays.

GNATfuzz can automatically generate a starting corpus for a subprogram unless any of the following are true:

  1. A test harness can’t be generated for the subprogram (see above).

  2. Any of the subprogram’s “in” or “in out” mode parameters is a type other than a scalar or an array of scalars (bounded or unbounded).

Section 4.3.3 illustrates a case of a fully-supported fuzzable subprogram and Section 4.3.4 illustrates how you can fuzz a partially-supported fuzzable subprogram by providing your own seeds via a GNATfuzz-generated interface. Finally, Section 4.3.5 illustrates how to fuzz instantiations of generic subprograms.

4.3.3. Fuzzing a simple Ada subprogram

Let us consider this simple project:

Listing 4.5 simple.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package body Simple is

   function Is_Divisible (X : Integer; Y : Integer) return Boolean
   is
   begin
      if X mod Y = 0 then
         return true;
      else
         return false;
      end if;
   end Is_Divisible;
end Simple;

and:

Listing 4.6 simple.ads
1
2
3
4
5
6
package Simple is

   function Is_Divisible (X : Integer; Y : Integer) return Boolean;
   -- checks if X is divisible by Y

end Simple;

This project consists of a single function that takes two integer arguments and returns true if the first (X) is divisible by the second (Y). The code contains a deliberate bug: division by 0 is possible. In a few steps, let’s demonstrate how the GNATfuzz toolchain can automatically detect this bug.

4.3.3.1. Using analyze mode

First, we run the GNATfuzz analyze mode on the project:

$ gnatfuzz analyze -P simple.gpr

Empowered by the Libadalang technology, this mode scans the project to detect fuzzable subprograms and generate an analysis file under <simple-project-obj-dir>/gnatfuzz/analyze.json, which contains a table listing all the fuzzable subprograms detected. In the case of this project, that file looks like:

Listing 4.7 analyze.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "user_project": "<install_prefix>/share/examples/simple/simple.gpr",
  "scenario_variables": [],
  "fuzzable_subprograms": [
    {
    "source_filename": "<install_prefix>/share/examples/simple/src/simple.ads",
    "start_line": 3,
    "id": 1,
    "label": "Is_Divisible",
    "corpus-gen-supported": true
    }
  ]
}

Here we see that GNATfuzz determined that the subprogram declared in the simple.ads specification file at line 3 is fuzzable. This corresponds to the Is_Divisible function. You can now use the GNATfuzz generate mode to autogenerate a test harness suitable for fuzzing.

4.3.3.2. Using generate mode

We can do this in one of two ways.

We can specify the location of the subprogram as given in analyze.json:

$ gnatfuzz generate -P simple.gpr -S ./src/simple.ads -L 3 -o generated_test_harness

Or we can use the unique identification number (id) for the subprogram as given in the analyze.json file:

$ gnatfuzz generate -P simple.gpr --analysis ./obj/gnatfuzz/analyze.json --subprogram-id 1 -o generated_test_harness

Either of these commands generates a new fuzz test-harness project and all the artifacts needed to support a fuzz-test session for the selected subprogram. These are placed under the specified output directory, generated_test_harness in our case, with the following directory structure:

Listing 4.8 Test-harness artifacts
  ├── generate_test_harness
  │   ├── fuzz_testing            --  subprogram-specific test-harness artifacts
  │   │   ├── build               --  corpus tools
  │   │   ├── generated_src       --  test-harness source code
  │   │   └── user_configuration  --  user configurable files
  │   └── gnatfuzz_shared         --  gnatfuzz lib artifacts
  │       ├── ...
  │       └── ...

In short:

  1. fuzz_testing/fuzz_test.gpr - the new project file of the test-harness that wraps the subprogram-under-test to enable injecting of test cases, capturing crashes, and coverage analysis.

  2. fuzz_testing/generated_src - the test-harness source code that you should not modify under normal circumstances.

  3. fuzz_testing/user_configuration - the test-harness source code that we may modify when needed. For example you may provide its starting corpus (see Section 4.3.4).

  4. fuzz_testing/fuzz_config.json - contains all the information/configurations needed by the GNATfuzz fuzz mode to enable the fuzz-testing of the generated test-harness.

  5. fuzz_testing/build - includes all the corpus-related tools needed for GNATfuzz to automatically generate the starting corpus as well as the encoding and decoding of test cases.

4.3.3.3. Using corpus-gen mode

The subprogram under test is suitable for starting corpus auto-generation (see the "corpus-gen-support": true field at line 9 of the <simple-project-obj-dir>/gnatfuzz/analyze.json). Therefore, we can use the following GNATfuzz command to automatically generate the starting corpus for our fuzzing session:

$ gnatfuzz corpus-gen -P ./generated_test_harness/fuzz_testing/fuzz_test.gpr -o starting_corpus

This generates multiple test_cases under the starting_corpus directory. Each is suitable for testing the subprogram chosen using the fuzzing mode. The test cases are in a binary format.

Note

Currently, starting corpus auto-generation support includes all the integral types and arrays of integral types (including strings). There are ongoing efforts to increase the supported types to the full scope of Ada types. Despite this, GNATfuzz generate can produce test-harnesses for a more extensive set of subprograms, beyond the ones for which their starting corpus can be auto-generated. GNATfuzz also automatically generates a framework to assist you in manually providing the starting corpus for these cases. We demonstrate this in Section 4.3.4.

Note

The corpus-gen mode is currently in its preliminary stages. It includes only one default strategy of auto-generating a starting corpus (using the First, Middle, and Last values for scalar types). Thus, when it’s feasible to auto-generate the starting corpus for a subprogram, you can currently skip the corpus-gen mode. In this case, the fuzz mode calls the corpus-gen mode in the background before performing any fuzzing-relating activities.

4.3.3.4. Decoding an auto-generated test case

If you want to inspect the contents of an auto-generated test case, you can use the decoding utility, which can be found at:

<install_prefix>/share/examples/simple/generate_test_harness/fuzz_testing/build/decode_test

For example:

$ ./decode_test ./starting-corpus/GNATfuzz_1
Parameter: Param 1 [Integer] = -2147483648
Parameter: Param 2 [Integer] = -2147483648

Here, the GNATfuzz_1 auto-generated test case is decoded and its values are printed. Param 1 and Param 2 correspond to the X and Y arguments of the Is_Divisible function, respectively.

4.3.3.5. Using fuzz mode

Now everything is in place for fuzz-testing the selected subprogram under test. We can use the following command to launch a fuzzing session:

$ gnatfuzz fuzz -P ./generated_test_harness/fuzz_testing/fuzz_test.gpr --corpus-path ./starting_corpus/ --cores=2

This command does the following:

  1. Builds the test harness.

  2. Builds the system-under-test with the AFL instrumentation pass.

  3. Builds the system-under-test with GNATcoverage.

  4. Minimizes the starting corpus using the afl-cmin tool to reduce the starting corpus to the test cases that execute unique paths of the system under test.

  5. Launches the AFL++ fuzz-testing session.

  6. Collects dynamic coverage and fuzzing statistics.

  7. Monitors the stop criteria and stops the fuzzing session if they’re met.

  8. Displays status information.

You can see the status information displayed in this example during a fuzzing session for this system under test in Fig. 4.5. GNATfuzz always displays the elapsed time since the fuzzing session was started, information on code coverage based on GNATcoverage statement coverage, and the progress on meeting the predefined stopping criteria for the session. Both the coverage information and stopping criteria sections can be disabled using the fuzz-mode’s –no-GNATcov and –ignore-stop-criteria switches, respectively.

alternate text

Fig. 4.5 GNATfuzz fuzzing session status overview

Important

The stopping criteria are defined in XML format. GNATfuzz uses a set of default stopping criteria if you don’t provide them via the –stop-criteria fuzz-mode switch. The generate mode also creates a local copy of this default stopping criteria XML file within the auto-generated test harness. You can inspect and modify these default criteria and can point to them by specifying the switch --stop-criteria=<path-to-generated-test-harness>/fuzz_testing/user_configuration/stop-criteria.xml. If you want to avoid using the stopping criteria and manually stop a fuzzing session using ctrl+c on the terminal, you must use the –ignore-stop-criteria fuzz-mode switch. More information on GNATfuzz’s stopping criteria can be found in Section 4.5.

The fuzzing command generates a session directory under <path-to-generated-test-harness>/fuzz_testing/session: with the following structure:

Listing 4.9 Fuzz-session generated artifacts
 session/
 ├── build                             --  The object-files of various compilations needed
 │   ├── obj-AFL_PLAIN
 │   └── obj-COVERAGE
 │       └── fuzz_test-gnatcov-instr
 ├── coverage_output                   --  Dynamic coverage reporting
 ├── fuzzer_output                     --  AFL++ generated artifacts
 │   ├── gnatfuzz_1_master             --  The "master's" fuzzing-process generated artifacts
 │   │   ├── crashes                   --  Test-cases found to crash the system-under-test
 │   │   ├── hangs                     --  Test-cases found to hang the system-under-test
 │   │   └── queue                     --  Test-cases found to exercise a unique execution path
 │   ├── gnatfuzz_2_slave              --  The "slave's" fuzzing-process generated artifacts
 │   │   ├── crashes                   --  Test-cases found to crash the system-under-test
 │   │   ├── hangs                     --  Test-cases found to hang the system-under-test
 │   │   └── queue                     --  Test-cases found to exercise a unique execution path
 └── used_starting_corpus              --  The starting-corpus used by the fuzzing session

You should be aware of the following when evaluating or interpreting a fuzzing session:

  1. Multiple fuzzing processes: The fuzz-mode switch –cores specifies the number of fuzzing processes to run. Each process is created on an individual core and generates its artifacts directory under the fuzzer_output directory. The first process is always the master (gnatfuzz_1_master) and the rest are slave processes. The more cores used, the more computing power is allocated to the fuzzing session and may speed up coverage and bug finding. But be careful not to exhaust your system resources since that will have a negative effect on fuzzing speed. In order to monitor the results of a fuzzing session, it is enough to inspect only the master’s process artifacts (“gnatfuzz_1_master”). This is because all the slave processes communicate their findings to the master process.

  2. Coverage: The dynamic coverage collected during a fuzzing session is provided in the coverage_output directory. This is statement-based coverage using the source-code instrumentation coverage provided by GNATcoverage (see source-traces-based coverage by GNATcoverage). An index.html is generated under the coverage directory, which is accessible via a browser. This allows you to navigate the coverage-information annotated code of the system under test. The coverage percentages are currently reported for the entire compilation module and not a subprogram selected from that module for fuzzing. So even though the fuzzing can execute all the statements in the selected subprogram, if the compilation module contains more subprograms than the one chosen for fuzzing, the coverage percentage may never reach 100%. The exception is if the subprogram under test performs calls to all other subprograms. You should keep this in mind when using coverage as a stopping criterion.

  3. Used Starting Corpus: The used_starting_corpus directory of the session directory is the starting corpus used by the fuzzing session. This might be the same as the autogenerated or user-provided starting corpus or might be a reduced version if you enable afl-cmin. Note that the afl-cmin is currently enabled by default by the fuzz mode of GNATfuzz (use the –no-cmin to disable it.)

  4. AFL++ raw fuzzing stats: You can inspect the raw AFL++ generated statistics during the fuzzing session by examining the fuzzer_stats file under the master’s process artifacts directory (gnatfuzz_1_master). An example of an AFL++ status file is shown in Fig. 4.2.

When the fuzz-testing session is completed (either because the stopping criteria are satisfied or we killed it manually), we can check the crashes directory to examine if the fuzzer was able to capture the expected divide-by-zero bug in the function-under-test:

Listing 4.10 Inspecting found crashes
 $ cd /gnatfuzz_1_master
 $ ls crashes/
 id:000000,sig:06,src:000000,time:64,execs:258,op:flip32,pos:4  README.txt

 $ ../../../build/decode_test crashes/id\:000000\,sig\:06\,src\:000000\,time\:64\,execs\:258\,op\:flip32\,pos\:4
 Parameter: Param 1 [Integer] = -1
 Parameter: Param 2 [Integer] =  0

As you can see, GNATfuzz produced one crashing test case (note that the naming of the crashing case can vary). By decoding this case, we can see that the value for the divisor (argument Y of Is_Divisible in Code Sample 4.6) is zero. You can now use this crashing test to debug and correct the subprogram under test.

You can also run the AFL-generated executable of the subprogram under test with the crashing test as follows:

Listing 4.11 Executing found crashes
 $ cd /gnatfuzz_1_master
 $ ls crashes/
 id:000000,sig:06,src:000000,time:64,execs:258,op:flip32,pos:4  README.txt

 $  ../../build/obj-AFL_PLAIN/fuzz_test_harness.afl_fuzz crashes/id\:000000\,sig\:06\,src\:000000\,time\:64\,execs\:258\,op\:flip32\,pos\:4
 [+] GNATfuzz : Calling User Setup
 [+] GNATfuzz : Calling Subprogram Under Test
 [+] GNATfuzz :
 Parameter: Param X [Integer] = -1
 Parameter: Param Y [Integer] =  0

 [+] GNATfuzz : Exception occurred! Generating core dump. [CONSTRAINT_ERROR] [simple.adb:6 divide by zero] [raised CONSTRAINT_ERROR : simple.adb:6 divide by zero]
 Aborted

4.3.4. Fuzzing an autogenerated test-harness with user-provided seeds

Let’s now try to fuzz the records example, which contains the following source:

Listing 4.12 show_date.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package Show_Date is

    type Months is
      (January, February, March, April,
       May, June, July, August, September,
       October, November, December);

    type Date is record
       Day   : Integer range 1 .. 31;
       Month : Months;
       Year  : Integer range 1 .. 3000 := 2032;
    end record;

    procedure Display_Date (D : Date);
    --  Displays given date

 end Show_Date;
Listing 4.13 show-date.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 with Ada.Text_IO; use Ada.Text_IO;

 package body Show_Date is

    procedure Display_Date (D : Date) is
    begin
       Put_Line ("Day:" & Integer'Image (D.Day)
                 & ", Month: "
                 & Months'Image (D.Month)
                 & ", Year:"
                 & Integer'Image (D.Year));
    end Display_Date;

 end Show_Date;

First, we run the GNATfuzz analyze mode on the project:

$ gnatfuzz analyze -P records_test.gpr

This mode scans the project to detect fuzzable subprograms and generates an analysis file <records_test-project-obj-dir>/gnatfuzz/analyze.json, which contains a table listing all the fuzzable subprograms detected. In our case, the analysis information file looks like this:

Listing 4.14 analyze.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 {
   "user_project": "<install_prefix>/share/examples/records/records_test.gpr",
   "scenario_variables": [],
   "fuzzable_subprograms": [
     {
       "source_filename": "<install_prefix>/share/examples/records/src/show_date.ads",
       "start_line": 14,
       "id": 1,
       "corpus_gen_supported": false
     }
   ]
 }

By inspecting the generated analysis file, we can see there’s one fuzzable subprogram that GNATfuzz can auto-generate a test harness for, namely, the Display_Date. However, it can’t auto-generate a starting corpus for it, as shown by the field corpus_gen_supported.

We then proceed to generate a test harness and try to auto-generate a starting corpus:

$ gnatfuzz generate -P records_test.gpr -S ./src/show_date.ads -L 14 -o generated_test_harness
$ gnatfuzz corpus-gen -P ./generated_test_harness/fuzz_testing/fuzz_test.gpr -o starting_corpus
INFO:    * Building corpus creator.....INFO: Done
INFO:    * Generating corpus...INFO: Done
ERROR: Failed to create a starting corpus - check if the User_Provided_Seeds subprogram needs completing within the User Configuration package
ERROR: ERROR_STARTING_CORPUS_EMPTY_AFTER_GENERATION
ERROR: For more details, see ./generated_test_harness/fuzz_testing/build/gnatfuzz/corpus_gen.log

As we saw, GNATfuzz was able to generate a test-harness for the given subprogram, but can’t auto-generate a starting corpus. This is because the current GNATfuzz version doesn’t yet support starting-corpus auto-generation for record types. Future versions of GNATfuzz will allow this, but in cases where automatic corpus generation is not yet supported, GNATfuzz generates an easy interface to enable you to provide its seeds, which GNATfuzz then uses to generate the starting corpus required for fuzzing:

Listing 4.15 User’s seed interface
 generated_test_harness/fuzz_testing/user_configuration/
 ├── show_date-gnatfuzz_user_configuration.adb
 ├── show_date-gnatfuzz_user_configuration.ads
 └── stop_criteria.xml

The actual package namespace is either a child of GNATfuzz or, when available, a child of the package holding the subprogram under test. In this case, the package will be generated as a child package of Show_Date.

Now we can see the User_Provided_Seeds procedure in show_date-gnatfuzz_user_configuration.adb:

Listing 4.16 User_Provided_Seeds procedure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 procedure User_Provided_Seeds is
 begin
    --  Complete here with calls to Add_Corpus_Seed with each set of seed values.
    --
    --  For instance, add two calls
    --      Add_Corpus_Seed (A, B);
    --      Add_Corpus_Seed (C, D);
    --  to generate two seeds, one with input values (A, B) and one with (C, D).

    null;
 end User_Provided_Seeds;

We can now edit these procedures to add seeds for the subprogram under test, as follows:

Listing 4.17 User_Provided_Seeds procedure
1
2
3
4
5
6
7
8
9
 procedure User_Provided_Seeds is
 begin

    Add_Corpus_Seed (Fuzz_Input_Param_1 => ((24, Show_Date.February, 1978)));
    Add_Corpus_Seed (Fuzz_Input_Param_1 => ((29, Show_Date.March, 1978)));
    Add_Corpus_Seed (Fuzz_Input_Param_1 => ((29, Show_Date.March, 1984)));
    Add_Corpus_Seed (Fuzz_Input_Param_1 => ((11, Show_Date.August, 1994)));

 end User_Provided_Seeds;

where Fuzz_Input_Param_1 is the parameter of the subprogram under test, Display_Date (D : Date). Let’s try generating the starting corpus again:

$ gnatfuzz corpus-gen -P ./generated_test_harness/fuzz_testing/fuzz_test.gpr -o starting_corpus
INFO:    * Building corpus creator....INFO: Done
INFO:    * Generating corpus...INFO: Done

By inspecting the generated starting corpus and decoding it, we can see it’s correctly generated in binary format and ready to be used for a fuzzing session:

$ ls starting_corpus/
User_1  User_2  User_3  User_4

$ ./generated_test_harness/fuzz_testing/build/decode_test starting_corpus/User_3
Parameter: Param 1 [Date] =
(DAY =>  29,
 MONTH => MARCH,
 YEAR =>  1984)

$ gnatfuzz fuzz -P ./generated_test_harness/fuzz_testing/fuzz_test.gpr --corpus-path ./starting_corpus/ --cores=1

4.3.5. Fuzzing instantiations of generic subprograms

For GNATfuzz to test a generic subprogram, we must provide an instantiation. The GNATfuzz analyze mode can report the instantiation in its generated analysis file if it consists of a fuzzable subprogram (see Section 4.3.2 for what is considered a fuzzable subprogram). When multiple instantiations exist, the analyze mode provides a list of all those that are fuzzable to allow you to select the one to be fuzz-tested. When a chain of instantiations exists, the analyze mode also reports that.

Let’s now try to fuzz the generics example, which has the following source code:

Listing 4.18 Sorting_Algorithms.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 generic
    type Index_Type is range <>;
    type Element_Type is private;
    with function "<" (L, R : Element_Type) return Boolean is <>;
 package Sorting_Algorithms is

    type Array_To_Sort is array (Index_Type) of Element_Type;

    procedure Selection_Sort (X : in out Array_To_Sort);

    procedure Bubble_Sort (X : in out Array_To_Sort);

 end Sorting_Algorithms;
Listing 4.19 Sorting_Multiple_Arrays.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 with Sorting_Algorithms;

 generic
    type Index_Type is range <>;
    type Element_Type is private;
    with function "<" (L, R : Element_Type) return Boolean is <>;
 package Sorting_Multiple_Arrays is

    package Sort_Array is new Sorting_Algorithms (Index_Type, Element_Type);

    type Sortable_Matrix is array (Index_Type) of Sort_Array.Array_To_Sort;

    procedure Selection_Sort (Matrix : in out Sortable_Matrix);

    procedure Bubble_Sort (Matrix : in out Sortable_Matrix);

 end Sorting_Multiple_Arrays;
Listing 4.20 Sorting_Multiple_Arrays.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package body Sorting_Multiple_Arrays is

    procedure Selection_Sort (Matrix : in out Sortable_Matrix) is
    begin
       for K in Matrix'Range loop
          Sort_Array.Selection_Sort (Matrix (K));
       end loop;
    end Selection_Sort;

    procedure Bubble_Sort (Matrix : in out Sortable_Matrix) is
    begin
       for K in Matrix'Range loop
          Sort_Array.Bubble_Sort (Matrix (K));
       end loop;
    end Bubble_Sort;

 end Sorting_Multiple_Arrays;
Listing 4.21 Instantiations.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 with Sorting_Algorithms;
 with Sorting_Multiple_Arrays;

 package Instantiations is

    type Range_Array_5 is range 1 .. 5;

    package Sort_Integer_Array_Of_Range_5 is new Sorting_Algorithms
      (Range_Array_5, Integer);

    package Sort_Integer_Arrays_Of_Range_5 is
      new Sorting_Multiple_Arrays (Range_Array_5, Integer);

 end Instantiations;

We didn’t display the Sorting_Algorithms.adb file, which implements the two sorting algorithms, Selection_Sort, and Bubble_Sort. You can find this file in the example’s source directory.

First, we run the GNATfuzz analyze mode on the project:

$ gnatfuzz analyze -P generics.gpr

This generates an analysis file generated in the <generics_test-project-obj-dir>/gnatfuzz/analyze.json file, which contains a table with all the fuzzable subprograms detected:

Listing 4.22 analyze.json
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
 {
   "user_project": "<install_prefix>/share/examples/generic/generics.gpr",
   "scenario_variables": [],
   "fuzzable_subprograms": [
     {
       "source_filename": "<install_prefix>/share/examples/generic/src/sorting_algorithms.ads",
       "start_line": 9,
       "label": "Selection_Sort",
       "instantiations": [
         {
           "id": 1,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 8,
               "label": "Sort_Integer_Array_Of_Range_5"
             }
           ]
         },
         {
           "id": 2,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 11,
               "label": "Sort_Integer_Arrays_Of_Range_5"
             },
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/sorting_multiple_arrays.ads",
               "start_line": 9,
               "label": "Sort_Array"
             }
           ]
         }
       ]
     },
     {
       "source_filename": "<install_prefix>/share/examples/generic/src/sorting_algorithms.ads",
       "start_line": 11,
       "label": "Bubble_Sort",
       "instantiations": [
         {
           "id": 3,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 8,
               "label": "Sort_Integer_Array_Of_Range_5"
             }
           ]
         },
         {
           "id": 4,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 11,
               "label": "Sort_Integer_Arrays_Of_Range_5"
             },
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/sorting_multiple_arrays.ads",
               "start_line": 9,
               "label": "Sort_Array"
             }
           ]
         }
       ]
     },
     {
       "source_filename": "<install_prefix>/share/examples/generic/src/sorting_multiple_arrays.ads",
       "start_line": 13,
       "label": "Selection_Sort",
       "instantiations": [
         {
           "id": 5,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 11,
               "label": "Sort_Integer_Arrays_Of_Range_5"
             }
           ]
         }
       ]
     },
     {
       "source_filename": "<install_prefix>/share/examples/generic/src/sorting_multiple_arrays.ads",
       "start_line": 15,
       "label": "Bubble_Sort",
       "instantiations": [
         {
           "id": 6,
           "corpus_gen_supported": true,
           "instantiation_chain": [
             {
               "source_filename": "<install_prefix>/share/examples/generic/src/instantiations.ads",
               "start_line": 11,
               "label": "Sort_Integer_Arrays_Of_Range_5"
             }
           ]
         }
       ]
     }
   ]
 }

Let’s now focus on fuzzing the Selection_Sort generic subprogram from the Sorting_Algorithms.ads package. In analysis file 4.22 at line 9, we see a list with two possible instantiations of this generic subprogram, with IDs 1 and 2, respectively. By inspecting the source code, we can see the following two instantiations:

1. instantiations.ads:Sort_Integer_Array_Of_Range_5  -> sorting_algorithms.ads:Selection_Sort
2. instantiations.ads:Sort_Integer_Arrays_Of_Range_5 -> sorting_multiple_arrays.ads:Sort_Array
                                                     -> sorting_algorithms.ads:Selection_Sort

The first is a direct instantiation of the generic Selection_Sort subprogram and the second is an indirect instantiation of the same generic via a chain of instantiations. By using the IDs of the instantiations, we can generate a test-harness for the selected instantiation of the Selection_Sort and perform a fuzz-testing session. For example:

$ gnatfuzz generate -P generics.gpr --analysis obj/gnatfuzz/analyze.json --subprogram-id 2 -o generated_test_harness
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr

4.3.6. Fuzzing global and private state

The current version of GNATfuzz doesn’t support the automatic generation of test harnesses that can fuzz global and private states, which can also affect a subprogram’s execution. Fortunately, there are easy ways to enable the fuzzing of such a state (to understand if it’s considered fuzzable, see Section 4.3.2). In this example, we demonstrate a simple way of enabling the fuzzing of global and private variables that affect the execution of a subprogram. Let’s consider the global_and_private_state example with the following source:

Listing 4.23 Global_And_Private_State_Fuzzing.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 package Global_And_Private_State_Fuzzing is

    Operation_Increase_Enabled : Boolean := True;

    function Increase_Or_Decrease_Value
      (Value : Integer; Amount : Integer) return Integer;
    --  Based on the "Operation_Increase_Enabled" this function will
    --  either increase or decrease the given "Value" by the given "Amount",
    --  and base on the "Absolute_Value_Enabled", it will return either the
    --  absolute value or not of the selected operation's result

 private
    Absolute_Value_Enabled : Boolean := False;

 end Global_And_Private_State_Fuzzing;
Listing 4.24 Global_And_Private_State_Fuzzing.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 package body Global_And_Private_State_Fuzzing is

    function Increase_Or_Decrease_Value (Value : Integer; Amount : Integer)
                                         return Integer
    is
       Temp_Value : Integer := 0;
    begin

       if Operation_Increase_Enabled then
          Temp_Value := Value + Amount;
       else
          Temp_Value := Value - Amount;
       end if;

       if Absolute_Value_Enabled then
          return abs Temp_Value;
       end if;

       return Temp_value;
    end Increase_Or_Decrease_Value;

 end Global_And_Private_State_Fuzzing;

The operation performed by the Increase_Or_Decrease_Value subprogram is controlled by the global parameter Operation_Increase_Enabled. The result of the selected operation is conditionally returned as an absolute value depending on the Absolute_Value_Enabled private variable. Let’s try to fuzz this subprogram as is:

$ gnatfuzz analyze -P global_and_private_state.gpr
$ gnatfuzz generate -P global_and_private_state.gpr -S src/global_and_private_state_fuzzing.ads -L 5 -o generated_test_harness/
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr

When we allow the fuzzing session to run for a while, we see that the coverage reaches 78% and doesn’t increase any further:

alternate text

Fig. 4.6 Coverage can’t reach 100% due to not fuzzing the global and private state that affect the subprogram’s execution.

We can also examine the coverage report for the source code of the subprogram under test:

$ firefox generated_test_harness/fuzz_testing/session/coverage_output/global_fuzzing.adb.html

Because the GNATfuzz autogenerated test-harness doesn’t change the Operation_Increase_Enabled global or the Absolute_Value_Enabled private variable, we can see from the coverage report in Fig. 4.7 that the code at line 11 and line 15 is never reached.

alternate text

Fig. 4.7 Coverage report for the Increase_Or_Decrease_Value subprogram

We can add the following child package to the global_and_private_state project to enable the fuzzing of the Operation_Increase_Enabled global and the Absolute_Value_Enabled private variable:

Listing 4.25 Global_And_Private_State_Fuzzing-child.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 package Global_And_Private_State_Fuzzing.Child is

    function Fuzz_Global_And_Private_State
      (Value : Integer; Amount : Integer; Oper_Increase_Enabled : Boolean;
       Absolute_Val_Enabled : Boolean) return Integer;
    --  Wrapper function for the "Increase_Or_Decrease_Value" function
    --  that takes two extra arguments which will allow the fuzzing of
    --  the global and private variables that affect the execution of
    --  the wrapped function.

 end Global_And_Private_State_Fuzzing.Child;
Listing 4.26 Global_And_Private_State_Fuzzing-child.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 package body Global_And_Private_State_Fuzzing.Child is

    function Fuzz_Global_And_Private_State
      (Value : Integer; Amount : Integer; Oper_Increase_Enabled : Boolean;
       Absolute_Val_Enabled : Boolean) return Integer
    is
    begin

       Operation_Increase_Enabled := Oper_Increase_Enabled;
       Absolute_Value_Enabled     := Absolute_Val_Enabled;

       return Increase_Or_Decrease_Value (Value, Amount);

    end Fuzz_Global_And_Private_State;

 end Global_And_Private_State_Fuzzing.Child;

This child package applies a wrapper function, Fuzz_Global_And_Private_State, to the function we want to fuzz test. Creating a child of the package containing the subprogram we want to fuzz has two benefits: the child can access the global and private state of the parent and we don’t need to modify our original code. The wrapper has the same parameters as the wrapped function, plus two extra: one represents the global and the other the private variable that affect the wrapped function’s execution state. As we can see in Source Code 4.26 on lines 9 and 10, these extra parameters are used to assign values to the Operation_Increase_Enabled global variable and the Absolute_Value_Enabled private variable, respectively, and thus can drive their values during fuzz testing of Fuzz_Global_And_Private_State. The wrapped function, Increase_Or_Decrease_Value, is called at line 12. This setup now allows the fuzz testing of the Increase_Or_Decrease_Value as before, but it now also drives the global and local state that affects its execution.

Let’s now try to fuzz the newly created wrapper function:

$ gnatfuzz analyze -P global_and_private_state.gpr
$ gnatfuzz generate -P global_and_private_state.gpr -S src/global_and_private_state_fuzzing-child.ads -L 3 -o generated_test_harness_2/
$ gnatfuzz fuzz -P generated_test_harness_2/fuzz_testing/fuzz_test.gpr
alternate text

Fig. 4.8 Coverage reaches 100% when global and private variables that affect the execution of the subprogram under test are being fuzzed

We see that the coverage indeed reached 100% in a short time. By inspecting the coverage report generated by GNATcoverage on the source code of the Increase_Or_Decrease_Value subprogram, we can see that GNATfuzz now executes all source-code lines of the Increase_Or_Decrease_Value subprogram:

$ firefox generated_test_harness_2/fuzz_testing/session/coverage_output/global_fuzzing.adb.html
alternate text

Fig. 4.9 Coverage report for the Increase_Or_Decrease_Value subprogram when fuzzing the private and global variables that affect the subprogram’s execution

4.3.7. Fuzzing Access Types

Currently, GNATfuzz doesn’t support the auto-generation of a fuzz-test harness when a subprogram takes as an argument an Access Type of any kind (see Section 4.3.2). Fortunately, we can use the approach used to fuzz-test global and private states (see Section 4.3.6) to fuzz test some common forms of Access Types. More specifically, we can do this when the type of the object that the Access Type is granting access to is fuzzable (see Section 4.3.2). We’ll use a modified version of the example in Section 4.3.6, namely, the access_types_parameters example, where we update the parameters of the Increase_Or_Decrease_Value function to be Integer` access types:

Listing 4.27 Access_Types_Fuzzing.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package Access_Types_Fuzzing is

    Operation_Increase_Enabled : Boolean := True;
    type Integer_Access is access Integer;

    function Increase_Or_Decrease_Value
      (Value_Access : Integer_Access; Amount_Access : Integer_Access)
       return Integer;
    --  Based on the "Operation_Increase_Enabled" this function will
    --  either increase or decrease the given "Value" by the given "Amount",
    --  and base on the "Absolute_Value_Enabled", it will return either the
    --  absolute value or not of the selected operation's result

 private
    Absolute_Value_Enabled : Boolean := False;

 end Access_Types_Fuzzing;
Listing 4.28 Access_Types_Fuzzing.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 package body Access_Types_Fuzzing is

    function Increase_Or_Decrease_Value (Value_Access : Integer_Access;
                                         Amount_Access : Integer_Access)
                                         return Integer
    is
       Temp_Value : Integer := 0;
    begin
       if Operation_Increase_Enabled then
          Temp_Value := Value_Access.all + Amount_Access.all;
       else
          Temp_Value := Value_Access.all - Amount_Access.all;
       end if;

       if Absolute_Value_Enabled then
          return abs Temp_Value;
       end if;

       return Temp_value;
    end Increase_Or_Decrease_Value;

 end Access_Types_Fuzzing;

By providing a wrapper function to the Increase_Or_Decrease_Value function, we enable the auto-generation of a test-harness that allows fuzz-testing of the wrapped function. This is done similarly to the approach used for fuzz-testing global and private variables in Section 4.3.6:

Listing 4.29 Access_Types_Fuzzing-child.ads
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 package Access_Types_Fuzzing.Child is

    function Fuzz_Access_Type_Parameters
      (Value : Integer; Amount : Integer; Oper_Increase_Enabled : Boolean;
       Absolute_Val_Enabled : Boolean) return Integer;
    --  Wrapper function for the "Increase_Or_Decrease_Value" function
    --  that takes two extra arguments which will allow the fuzzing of
    --  the global and private variables that affect the execution of
    --  the wrapped function.

 end Access_Types_Fuzzing.Child;
Listing 4.30 Access_Types_Fuzzing-child.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package body Access_Types_Fuzzing.Child is

    function Fuzz_Access_Type_Parameters
      (Value : Integer; Amount : Integer; Oper_Increase_Enabled : Boolean;
       Absolute_Val_Enabled : Boolean) return Integer
    is
       Value_Access  : constant Integer_Access := new Integer'(Value);
       Amount_Access : constant Integer_Access := new Integer'(Amount);
    begin

       Operation_Increase_Enabled := Oper_Increase_Enabled;
       Absolute_Value_Enabled     := Absolute_Val_Enabled;

       return Increase_Or_Decrease_Value (Value_Access, Amount_Access);
    end Fuzz_Access_Type_Parameters;

 end Access_Types_Fuzzing.Child;

Now, we can proceed with the standard GNATfuzz modes for fuzz-testing the Fuzz_Access_Type_Parameters wrapper function:

$ gnatfuzz analyze -P access_type_parameter.gpr
$ gnatfuzz generate -P access_type_parameter.gpr -S src/access_types_fuzzing-child.ads -L 3 -o generated_test_harness/
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr

Looking at the coverage report generated, we can confirm that the function Increase_Or_Decrease_Value was successfully fuzzed:

alternate text

Fig. 4.10 Coverage report for the Increase_Or_Decrease_Value subprogram when fuzzing the private, global variables, and Access Type parameters that affect the subprogram’s execution

4.3.8. Filtering exceptions

A generated GNATfuzz fuzz-test harness captures all exceptions unhandled by the system under test and adds the test cases that caused these exceptions to the crashes directory. In some cases, you might want to filter some known and expected exceptions (such as an exception raised by design to notify the calling routine of an error when a library’s API is misused) to stop them from being captured as software bugs. The GNATfuzz-generated test-harness provides an easy mechanism to do this.

Warning

You should be careful if you decide to filter out Ada run-time exceptions. Such exceptions, if unhandled, probably indicate software bugs or potential security vulnerabilities.

Let’s look at the filtering_exceptions example that demonstrates how to tell GNATfuzz not to add a crash to the crashes directory when a specific exception is raised. The source code of the example is:

Listing 4.31 Procedure_Under_Test.ads
1
2
3
4
5
6
7
 package Procedure_Under_Test is

    Some_Expected_Exception : exception;

    procedure Test (Some_Text : String);

 end Procedure_Under_Test;
Listing 4.32 Procedure_Under_Test.adb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 with Ada.Text_IO; use Ada.Text_IO;

 package body Procedure_Under_Test is

    procedure Test (Some_Text : String) is
    begin
       Put_Line (Some_Text);
       raise Some_Expected_Exception;
    end Test;

 end Procedure_Under_Test;

As shown in Source Code 4.32, the procedure Test raises an exception at line 8. Let’s generate a test harness for this procedure and use it to filter out this exception:

$ gnatfuzz analyze -P filtering_exceptions.gpr
$ gnatfuzz generate -P filtering_exceptions.gpr -S src/procedure_under_test.ads -L 5 -o generated_test_harness

In the generated_test_harness/fuzz_testing/user_configuration/ directory of the GNATfuzz autogenerated test-harness, there’s a file called procedure_under_test-gnatfuzz_user_configuration.adb, with the following procedure:

Listing 4.33 Execute_Subprogram_Under_Test procedure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 procedure Execute_Subprogram_Under_Test
   (Some_Text :  Standard.String)
 is
 begin

    --  Call the subprogram under test. The user is not expected to change
    --  this code.
    Procedure_Under_Test.Test
      (Some_Text => Some_Text);

    --  Filter out any expected exception types here.
    --
    --  Ada run-time exceptions should not be filtered because they probably
    --  indicate software bugs and potential security vulnerabilities if
    --  captured. In such a case, GNATfuzz will place the test case
    --  responsible for raising the exception under the "crashes" directory.
    --  If an exception is expected, then an exception handler can be added
    --  here to capture it and allow GNATfuzz to ignore it.

    --  For example,
    --
    --  exception
    --     when Occurrence : Some_Expected_Exception =>
    --        null;

 end Execute_Subprogram_Under_Test;

We can modify this in the following manner:

Listing 4.34 Execute_Subprogram_Under_Test procedure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 procedure Execute_Subprogram_Under_Test
   (Some_Text :  Standard.String)
 is
 begin

    --  Call the subprogram under test. The user is not expected to change
    --  this code.
    Procedure_Under_Test.Test
      (Some_Text => Some_Text);

 exception
    when Occurrence : Procedure_Under_Test.Some_Expected_Exception =>
       null;

 end Execute_Subprogram_Under_Test;

This modification allows catching the exception before GNATfuzz will record it as a crash. So the test which led to the exception being raised won’t be added to the crashes directory. By providing the following custom stop criteria, we can confirm the assumption with a fuzz-test session:

Listing 4.35 stop_criteria.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 <GNATfuzz>
     <afl_stop_criteria>
         <rule_grouper type="and">
             <rule>
                 <state>coverage</state>
                 <operator>eq</operator>
                 <value>100</value>
                 <all_sessions>true</all_sessions>
             </rule>
             <rule>
                 <state>saved_crashes</state>
                 <operator>eq</operator>
                 <value>0</value>
                 <all_sessions>true</all_sessions>
             </rule>
             <rule>
                 <state>run_time</state>
                 <operator>gt</operator>
                 <value>60</value>
                 <all_sessions>true</all_sessions>
             </rule>
         </rule_grouper>
     </afl_stop_criteria>
 </GNATfuzz>
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr --cores=1 --stop-criteria=stop_criteria.xml

This fuzz-testing session will stop if 100% coverage of the subprogram under test is achieved and no crashes are found. This means that line 8 in Source Code 4.32 will be executed, but the raised exception won’t result in the associated test case being added to the crashes directory. We also allow the fuzz-testing session to run for one minute to ensure sufficient time was given to place any potential crashing test cases in the crashes directory. Our fuzz-testing session exits with this stopping criteria, meaning the raised exception was successfully filtered out.

4.3.9. Fuzzing C functions through Ada bindings

Currently, GNATfuzz fuzz-testing automation is not supported for C programs. However, it’s possible to fuzz-test C functions through Ada bindings.

Let’s look at the testing_C_code_through_Ada_bindings example to demonstrate how to allow GNATfuzz to fuzz-test a C function using an Ada binding and detect a bug in the C code. The C source code of the example is:

Listing 4.36 C functions to be fuzz-tested through Ada bindings (calculations.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 #include "calculations.h"

 //  simple function performing some basic math operations
 //  which allows a division by 0
 int calculations_problematic (int a, int b)
 {
   int temp = 0;
   temp = a * b;
   temp = temp + a / b;
   return temp;
 }

 // same as previous function but prohibits the division
 // by zero
 int calculations (int a, int b)
 {
   int temp = 0;
   temp = a * b;
   if (b != 0)
      temp = temp + a / b;
   return temp;
 }

The two functions perform the same simple calculation but the calculations_problematic function allows a division by zero. We want to verify that GNATfuzz is able to detect this bug by fuzz-testing the Ada binding for this function.

The Ada bindings for the two C functions are:

Listing 4.37 Ada bindings for the C functions to be fuzz-tested (procedure_under_test.ads)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 package Procedure_Under_Test is

    function Calculations (A : Integer; B : Integer) return Integer
      with Import => True,
      Convention => C,
      External_Name => "calculations";

    function Calculations_Problematic (A : Integer; B : Integer) return Integer
      with Import => True,
      Convention => C,
      External_Name => "calculations_problematic";

    procedure Test
      (Control : Integer;
       In_A    : Integer;
       In_B    : Integer);

 end Procedure_Under_Test;

A possible driver procedure to test the two C functions could be:

Listing 4.38 Ada driver subprogram to test the C bindings (procedure_under_test.adb)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 with Ada.Text_IO; use Ada.Text_IO;

 package body Procedure_Under_Test is

    procedure Test
      (Control : Integer;
       In_A    : Integer;
       In_B    : Integer)
    is
       Result : Integer := 0;
    begin

       if Control < 0 then
          Result := Calculations_Problematic (In_A, In_B);
       else
          Result := Calculations (In_A, In_B);
       end if;

       Put_Line (Result'Image);

    end Test;

 end Procedure_Under_Test;

Note

By default, when using gprbuild we only compile Ada source files. To compile C code files as well, we use the Languages attribute in the GPR file and specify c as an option: for Languages use ("ada", "c");.

By running the GNATfuzz analyze mode:

$ gnatfuzz analyze -P testing_C_code.gpr

GNATfuzz generates the following analysis file:

Listing 4.39 analyze.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 {
   "user_project": "<install_prefix>/share/examples/testing_C_code_through_Ada_bindings/testing_C_code.gpr",
   "scenario_variables": [],
   "fuzzable_subprograms": [
     {
       "source_filename": "<install_prefix>/share/examples/testing_C_code_through_Ada_bindings/src/procedure_under_test.ads",
       "start_line": 3,
       "id": 1,
       "corpus_gen_supported": true
     },
     {
       "source_filename": "<install_prefix>/share/examples/testing_C_code_through_Ada_bindings/src/procedure_under_test.ads",
       "start_line": 8,
       "id": 2,
       "corpus_gen_supported": true
     },
     {
       "source_filename": "<install_prefix>/share/examples/testing_C_code_through_Ada_bindings/src/procedure_under_test.ads",
       "start_line": 13,
       "id": 3,
       "corpus_gen_supported": true
     }
   ]
 }

Note

As we can see, the Ada bindings for the C functions can also be directly targeted by GNATfuzz for fuzz-testing. For this example, we are using the Test procedure as a driver.

We can now use the generate and fuzz mode to fuzz-test our driver function:

$ gnatfuzz generate -P testing_C_code.gpr --analysis obj/gnatfuzz/analyze.json --subprogram-id 3 -o generated_test_harness
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr --cores=1 --stop-criteria=stop_criteria.xml

The following stop-criteria only allows the fuzz-testing session to exit when full coverage of the driver function is achieved (implying that both Ada C bindings are called) and a crash occurs (which should be the divide-by-zero allowed by the calculations_problematic C function):

Listing 4.40 stop_criteria.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  <GNATfuzz>
     <afl_stop_criteria>
         <rule_grouper type="and">
             <rule>
                 <state>coverage</state>
                 <operator>eq</operator>
                 <value>100</value>
                 <all_sessions>true</all_sessions>
             </rule>
             <rule>
                 <state>saved_crashes</state>
                 <operator>gt</operator>
                 <value>0</value>
                 <all_sessions>true</all_sessions>
             </rule>
         </rule_grouper>
     </afl_stop_criteria>
 </GNATfuzz>

By examining the crashes directory, we can see that GNATfuzz was able to detect the expected divide-by-zero bug:

Listing 4.41 Inspecting found crashes
 $ cd generated_test_harness/fuzz_testing/session/fuzzer_output/gnatfuzz_1_master/
 $ ls crashes/
 id:000000,sig:06,src:000000,time:1212,execs:4167,op:havoc,rep:16  README.txt

 $ ../../../build/decode_test crashes/id\:000000\,sig\:06\,src\:000000\,time\:1212\,execs\:4167\,op\:havoc\,rep\:16
 Parameter: Param Control [Integer] = -1
 Parameter: Param In_A [Integer] =  721151
 Parameter: Param In_B [Integer] =  0

As we can see, GNATfuzz detected one crashing test case (note that the naming of the case can vary). By decoding this case, we can see that the value for the divisor (argument B passed to Calculations_Problematic) is zero while the value of the Control argument ensures the call to the problematic function. We can now use this test case to debug and correct the C function. Note that arithmetic overflows are also allowed by both C functions, so more crashes can be detected before we get to the division-by-zero crash.

4.3.10. Resetting dynamically allocated memory when fuzz-testing with the AFL++ persistent mode

In Section 4.3.1.4.2, we explained that persistent mode is only suitable for programs whose state can be completely reset between multiple test executions.

Let’s now look at the afl_persist_reset_mem example that demonstrates how to reset the dynamically allocated memory of the program under test when using the persistent execution mode:

Listing 4.42 Subprogram that will retain state between test executions when fuzz-testing in AFL++ persistent mode (test_afl_persist.ads)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 package Test_AFL_Persist is

    Procedure Initialize (Width : Integer; Length : Integer);

 private

    type Floor_Plan_Type is record
       Width   : Integer;
       Length  : Integer;
       Ratio   : Integer;
    end record;

    type Floor_Plan_Access is access all Floor_Plan_Type;

    Floor_Plan : Floor_Plan_Access;

 end Test_AFL_Persist;
Listing 4.43 Subprogram that will retain state between test executions when fuzz-testing in AFL++ persistent mode (test_afl_persist.adb)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 package body Test_AFL_Persist is

    Procedure Initialize (Width : Integer; Length : Integer)
    is
       Initialization_Error : exception;
    begin
       if Floor_Plan = null then
          Floor_Plan        := new Floor_Plan_Type;
          Floor_Plan.Width  := Width;
          Floor_Plan.Length := Length;
          Floor_Plan.Ratio  := Width / Length;
       else
          raise Initialization_Error with
            "Error! Object is already initialized";
       end if;
    end Initialize;

 end Test_AFL_Persist;

The system under test initializes a record containing both the dimensions of a floor and their ratio. The initialization is only supposed to be done once and an exception is raised otherwise. Line 11 of the Initialize subprogram is vulnerable to both division-by-zero and overflow. Setting the following stopping criteria stops the fuzz-testing session when two crashes are detected:

Listing 4.44 Stopping criteria for the Initialize subprogram
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 <GNATfuzz>
     <afl_stop_criteria>
         <rule_grouper type="single">
             <rule>
                 <state>saved_crashes</state>
                 <operator>gt</operator>
                 <value>1</value>
                 <all_sessions>true</all_sessions>
             </rule>
         </rule_grouper>
     </afl_stop_criteria>
 </GNATfuzz>

Let’s run a fuzz-testing session using the AFL++ persist mode by executing the following GNATfuzz commands:

$ gnatfuzz analyze -P afl_persist_reset_mem.gpr
$ gnatfuzz generate -P afl_persist_reset_mem.gpr -S ./src/test_afl_persist.ads -L 3 -o ./generated_test_harness
$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr --afl-mode=afl_persist --cores=1 --stop-criteria=stop_criteria.xml --no-cmin

This results in a failed attempt to launch a fuzz-testing session:

The fuzz-testing session's dry-run crashes when using AFL++ ``persistent`` mode

Fig. 4.11 The fuzz-testing session’s dry-run crashes when using AFL++ persistent mode without resetting the state of the Initialize subprogram

As we can see in Fig. 4.11, AFL++’s dry-run states that all initial test cases provided to GNATfuzz resulted in crashes. A dry run is a phase where AFL++ executes the initial set of test cases multiple times before starting a fuzz-testing session. During the dry run session, stability is calculated by checking if each run of the same test results in the same execution path. If the path differs, the stability drops and a warning is issued in the AFL++ terminal window. For a dry run to be successful, at least one of the initial test cases must always run to completion without crashing or timing out. If the dry run fails, the fuzzing session won’t start.

The Initialize subprogram under test dynamically allocates memory for the Floor_Plan record only when not already allocated by a previous call to Initialize. In this example, the first execution of the dry run evaluates the condition on line 7 as True. Line 8 is then executed and the Floor_Plan object is created. The second execution evaluates the condition on line 7 as False (i.e. the Floor_Plan object is no longer null). This causes the system under test to raise the exception at line 13, which results in a failed dry run.

As previously discussed, the persistent execution mode retains any program state while executing tests within the same process. To solve this problem, the generated fuzz test-harness requires manual intervention to ensure the memory associated with the Floor_Plan record is freed between each test execution. GNATfuzz provides a mechanism you can use to reset dynamically allocated memory before executing each test. To do so, you modify the TearDown function within the User_Configuration package (see Section 4.4).

In the generated_test_harness/fuzz_testing/user_configuration/ directory of the GNATfuzz autogenerated test-harness, there’s a file called test_afl_persist-gnatfuzz_user_configuration.adb, with the following procedure:

Listing 4.45 TearDown procedure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 procedure Teardown
   (Width : Standard.Integer;
    Length : Standard.Integer) is
 begin
    --  Complete here with any specialist test deconstruction.
    --
    --  For instance, freeing memory.
    --
    --  In addition, this subprogram can be used to validate global state
    --  data or read stored Stub values and check they are as expected.
    --
    --  This subprogram can optionally raise exceptions to indicate test
    --  failure.

    null;
 end Teardown;

We can modify this as follows:

Listing 4.46 TearDown procedure used to reset dynamically allocated memory by the system under test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 procedure Teardown
   (Width : Standard.Integer;
    Length : Standard.Integer) is

    procedure Free is new Ada.Unchecked_Deallocation
      (Floor_Plan_Type, Floor_Plan_Access);

 begin

    Free (Floor_Plan);

 end Teardown;

This modification allows you to free memory that was dynamically allocated for the Floor_Plan record before GNATfuzz executes the next test. By using the custom stop criteria given in Fig. 4.44, we can launch a new fuzz-testing campaign using the AFL++ persistent mode:

$ gnatfuzz fuzz -P generated_test_harness/fuzz_testing/fuzz_test.gpr --cores=1 --afl-mode=afl_persist --stop-criteria=stop_criteria.xml --no-cmin

This time, the fuzz-testing session launches and completes successfully:

The fuzz-testing session's dry-run crashes when using AFL++ ``persistent`` mode

Fig. 4.12 The fuzz-testing session runs and completes successfully when using AFL++ persistent mode and resetting the state of the Initialize subprogram

4.4. The “User_Configuration” package

As we’ve seen in a previous examples, the GNATfuzz auto-generated test-harness provides a User_Configuration package (for example, see Source Code 4.15). The actual package namespace is either a child of GNATfuzz or, when available, a child of the package holding the subprogram under test. In the latter case, the User_Configuration package has access to the global and local state of the package that contains the subprogram under test.

The User_Configuration package offers several useful subprograms that you can modify to provide custom functionality for a fuzz-testing session:

  1. Setup function: This function executes before each test is executed. You can use it to set up any specialized test initialization, such as setting Stub return values or assigning values to the global state. Also, you can use the return value of this function to indicate whether the setup of a test was successful or that the test should be skipped. In addition, the “in” parameters of the subprogram under test are passed into this function as “in out” parameters, so you can alter their values before running the test. You should be careful to only use this this feature when necessary, but auto-generated test inputs sometimes require modifications in order to make the test valid. For example, if you have a record containing a checksum, any mutation of the data will invalidate the checksum, so you should add code to this function to recalculate it.

  2. Teardown procedure: This procedure is called after each test instance is executed. You can use it to add any specialized finalizational functionality, such as freeing memory and also use it to validate global state data or read stored Stub values and check that they are as expected. The Teardown subprogram can also optionally raise exceptions to indicate test failure.

  3. User_Provided_Seeds procedure: You can use this procedure to provide custom seed values, which GNATfuzz uses to create a starting corpus (see Section 4.3.4).

  4. Execute_Subprogram_Under_Test procedure: This is the procedure that calls the subprogram under test. You can use it to filter out any expected exceptions (see Section 4.3.8).

4.5. GNATfuzz stopping criteria

Through the provided fuzz-test examples, we demonstrated the use of the stopping criteria to provide a predefined set of rules on when a fuzz-testing campaign should be stopped.

The stopping criteria are defined in XML format. If you don’t provide one via the –stop-criteria fuzz-mode switch, GNATfuzz uses a set of default stopping criteria . The generate mode also creates a local copy of this default stopping criteria as an XML file within the autogenerated test harness. You can inspect and modify these default criteria and point to them by specifying the switch:

--stop-criteria=<path-to-generated-test-harness>/fuzz_testing/user_configuration/stop-criteria.xml

Listing 4.47 GNATfuzz default stopping criteria
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 <GNATfuzz>
     <afl_stop_criteria>
         <rule_grouper type="and">
             <rule>
                 <state>cycles_done</state>
                 <operator>gt</operator>
                 <value>2</value>
                 <all_sessions>true</all_sessions>
             </rule>
             <rule>
                 <state>pending_favs</state>
                 <operator>eq</operator>
                 <value>0</value>
                 <all_sessions>true</all_sessions>
             </rule>
             <rule>
                 <arithmetic_state>
                     <state1>current_time</state1>
                     <state2>last_find</state2>
                     <arithmetic_operator>minus</arithmetic_operator>
                 </arithmetic_state>
                 <operator>gt</operator>
                 <value>360</value>
                 <all_sessions>true</all_sessions>
             </rule>
         </rule_grouper>
     </afl_stop_criteria>
 </GNATfuzz>

Fig. 4.47 shows the default stopping criteria generated by GNATfuzz. These default stopping criteria specify that a fuzzing session will only stop if:

  1. All entries within the test queue are processed at least three times, and

  2. There are no “Pending favorites” test cases left (promising test cases for finding new execution paths), and

  3. AFL++ doesn’t find a new path for 6 minutes.

The goal of this set of default stop-criteria rules is to allow sufficient time for obtaining a good snapshot of what GNATfuzz can achieve when testing a subprogram. Small subprograms tend to get the maximum possible code coverage in very little time. Note that the provided stopping criteria offers no guarantees that GNATfuzz will reach its full potential when testing a program. They are only intended as a good starting point for you to modify as needed, e.g., by increasing the time when the last new path is found to allow the fuzzer to exercise more potential corner cases. To see all the possible stop-criteria options, you can view the XML relevant schema file that can be found under <install_prefix>/share/gnatfuzz/auxiliary/config.xsd.

If you want to avoid using any stopping criteria and manually stop a fuzzing session using ctrl+c from the terminal, you must pass the –ignore-stop-criteria fuzz-mode switch.

4.6. GNATfuzz integration in GNAT Studio

Recent versions of GNAT Studio provide an integration with GNATfuzz.

The integration is activated automatically when GNAT Studio finds the command line executable gnatfuzz on the PATH.

4.6.1. The GNATfuzz menu

You can access some GNATfuzz operations through the toplevel GNATfuzz menu. On a regular project, the first two entries can be used to launch an analysis on the code:

  • Analyze Project: This launches GNATfuzz in analyze mode on the entire project.

  • Analyze File: This launches GNATfuzz in analyze mode on the current file only.

When the project loaded in GNAT Studio is a GNATfuzz test-harness project (determined by the presence of the fuzz_config.json file next to the project file), it provides additional menu entries:

  • Start/Stop Fuzzing session: Use this to launch a fuzzing session or stop the current fuzzing session if one is already running.

  • Switch to User Project: Use this to unload the test-harness project from GNAT Studio and load the original project.

4.6.2. Viewing analysis results and generating a fuzz-testing harness

The results of GNATfuzz in analyze mode, i.e., the subprograms that are identified as automatically fuzzable, are listed in the Locations view. Each entry is decorated with an icon you can click to launch GNATfuzz in generate mode on the corresponding subprogram and generate a fuzz-test harness project.

The Locations view showing the results of gnatfuzz analyze

Fig. 4.13 The Locations view showing the results of gnatfuzz analyze

You can fine-tune the switches passed to the command line in the intermediate dialog. By default, GNATfuzz will generate the harness project in a fuzz_testing directory inside the object directory of the root project.

If generation is successful, GNAT Studio presents a dialog allowing you to load the generated test-harness project.

4.6.3. Running a fuzz session

The first thing you might want to do before running a fuzz session is to add your seeds to pass to the corpus generator phase. This is done by editing the subprogram User_Provided_Seeds in user_configuration/<>-gnatfuzz_user_configuration.adb. You might want to edit other elements in this package to adjust the test-harness to your code.

The Project view showing a fuzz-test harness project

Fig. 4.14 The Project view showing a fuzz-test harness project

When the fuzzing session is running, you’ll see the progress in the Messages view - the same output you’d see when running gnatfuzz interactively in the command line.

The Messages view showing the gnatfuzz command line report

Fig. 4.15 The Messages view showing the gnatfuzz command line report

GNAT Studio reads the coverage information obtained by gnatfuzz during the session, displays a summary in the Coverage report, and annotates source files in the system under test with coverage information, showing which lines have been successfully explored by the fuzz-testing session. This information is cumulative and shows the coverage collectively gained by multiple individual runs of the executable under GNATfuzz.

The source editor annotated with coverage information

Fig. 4.16 The source editor annotated with coverage information

4.6.4. Viewing and debugging fuzz crashes

The Fuzz Crashes view shows the crashes and hangs found during the fuzzing session.

The Fuzz Crashes view showing the crashes found by gnatfuzz

Fig. 4.17 The Fuzz Crashes view showing the crashes found by gnatfuzz

Each row in this view corresponds to one individual test case that causes the subprogram under test to crash or hang. You can expand a row to view the parameters to the subprogram being tested that correspond to this crash or hang.

You can also launch a debugging session to place yourself at the crash point: you do this by double-clicking on a row. GNAT Studio launches the fuzzed executable in the debugger and runs it with the seed that caused the hang or crash. The debugger stops on exceptions, so you might have to manually execute past any expected exception that might occur prior to the one causing the crash.

4.7. FAQ for GNATfuzz

Question 1: If my application under test uses Ada tasks, how can I ensure any exceptions raised within task bodies are propagated up to the main test harness?

Answer: Exception propagation in Ada tasking doesn’t happen automatically. However, the fuzzing campaign may fail to identify software bugs within Ada tasks unless you ensure that the package GNATfuzz.Fallback_Handler_Start_Up is included within the main fuzz-test-harness. To help facilitate this behavior, GNATfuzz will include the required code as an Ada comment within the generated file fuzz_testing/generated_src/gnatfuzz-fuzz_test_harness.adb:

Listing 4.48 Enabling the GNATfuzz.Fallback_Handler_Start_Up
1
2
    --  Comment out the code below if the test application is multi-threaded
    --  with GNATfuzz.Fallback_Handler_Start_Up;

To enable the task-exception-propagation behavior, comment out the second line above before running the fuzzing campaign.

Question 2: If my Ada application under tests invokes C/C++ via Ada bindings, will the C/C++ libraries also be included in the fuzzing campaign?

Answer: Yes. However, for the C/C++ libraries to be included in a GNATfuzz campaign, you must dynamically build them via the standard gprbuild mechanism. GNATfuzz can only add the necessary fuzz-test instrumentation to the C/C++ libraries if it has visibility of the source code.

Question 3: What happens if I want to fuzz test aspects of my application that are statically built?

Answer: For your statically built libraries to be included in the control-flow path awareness feature of GNATfuzz, you have to build them using the additional pass implemented by the afl-gcc-fast` compiler plugin. To learn more about how to enable this compiler pass on a statically built library, please contact AdaCore support. Alternatively, you can change the build mechanism for the campaign so that the libraries are built dynamically.

4.8. GNATfuzz diagnostics and issue reporting

GNATfuzz reports any problems or warnings captured during the use of the GNATfuzz modes, whether specified on the command line or by the GNAT Studio IDE. You can find more information on the actions a mode is performing and any potential issues in the auto-generated log files for each mode. The location of these files are:

  1. For the analyze mode: <obj-dir>/gnatfuzz/analyze.log, where <obj-dir> is the location of the system-under-test project’s object directory.

  2. For the generate mode: <obj-dir>/gnatfuzz/generate.log, where <obj-dir> is the location of the system-under-test project’s object directory.

  3. For the corpus-gen mode: <generated_test_harness>/fuzz_testing/build/gnatfuzz/corpus_gen.log, where <generated-test-harness> is the location specified to the generate mode to create the test harness.

  4. For the fuzz mode: <generated-test-harness>/fuzz_testing/session/fuzz.log, where <generated-test-harness> is the location specified to the generate mode to create the test harness. A separate log file afl-fuzz.<N>.log will also be generated in this directory for each instance of afl-fuzz being launched.

Note that if the GNATfuzz modes are run multiple times targeting the same subprogram, multiple log files will be generated, with increasing numbering as part of their naming convention indicating their order of generation, for example, analyze.log, analyze.log_1, and analyze.log_2