ocaml-afl-examples icon indicating copy to clipboard operation
ocaml-afl-examples copied to clipboard

Small examples of how to use AFL to fuzz OCaml programs

OCaml AFL fuzzing examples

Small examples of how to use AFL to fuzz OCaml programs

These examples are here to help you quickly get set up with AFL and to illustrate an upcoming article on the Tarides blog.

Setup

To be able to correctly run the examples in this repo and toy around with fuzzing you will need to install afl and have a +afl opam switch so that the binaries are properly instrumented for fuzzing.

You can setup the afl switch by running:

$ opam switch create fuzzing-switch 4.07.1+afl

You can either install AFL from your distribution, e.g. on Debian:

$ apt update && apt install afl

Or by using the convenience opam package:

$ opam install --switch=fuzzing-switch afl

Some of the examples have extra dependencies such as crowbar and bun. You can install all of them by running:

$ opam install --switch=fuzzing-switch crowbar bun

Simple parser

The simple-parser folder contains the most basic example and shows how you can use afl-fuzz to fuzz a simple parsing function written in OCaml.

The lib subfolder contains a library with a single parse_int function that parses an int from a string, with a little twist.

The fuzz subfolder contains the code to be compiled to the fuzzing binary fuzz_me.exe which must be passed to afl and an inputs/ folder with a couple starting test cases.

You can try fuzzing it by yourself:

$ dune build simple-parser/fuzz/fuzz_me.exe
$ afl-fuzz -i simple-parser/fuzz/inputs -o _build/default/simple-parser/fuzz/findings _build/default/simple-parser/fuzz/fuzz_me.exe @@

Or simply run:

$ dune build @simple-parser/fuzz --no-buffer

which will do pretty much exactly the above.

AFL should find the crash fairly quickly. It will show up in the top right corner of afl-fuzz's output, under uniq crashes, see the picture below.

afl-output-screenshot-emphasized-crashes

You can inspect the input that triggered the crash by running:

$ cat _build/default/simple-parser/fuzz/findings/crashes/id*
abc

and reproduce it by running:

$ cd _build/default/simple-parser/fuzz
$ ./fuzz_me.exe findings/crashes/id*
Fatal error: exception Failure("secret crash")

Awesome list

The awesome-list folder contains an example of how AFL can be used conjointly with the crowbar library to both find crashes and do some property based testing.

The lib subfolder contains a library with a single sort function that sorts lists of integers. Again it mostly works fine except in two specific cases where it will either crash or sort the list in reverse order.

The fuzz subfolder contains the code for the fuzzing binary. It is slightly different from the previous example as here we use crowbar to build the correct binary instead of doing it by hand. Furthermore, we don't check for crashes only anymore but also want to know if the function under test invariant, i.e. the resulting list is sorted in increasing order, stands. The resulting fuzzing binary has roughly two modes of execution: an AFL one and a QuickCheck one.

In QuickCheck mode it uses OCaml's randonmess source to try a fixed number of inputs. To try that mode you can run:

$ dune exec awesome-list/fuzz/fuzz_me.exe

Alternatively you can also run the QuickCheck mode until a test failure is discovered with the -i option of the binary like this:

$ dune exec -- awesome-list/fuzz/fuzz_me.exe -i

In AFL mode, it just use the input supplied by AFL as a source of randomness to supply values of the right form to your test functions. You can run it just like with a regular fuzzing binary:

$ dune build awesome-list/fuzz/fuzz_me.exe
$ afl-fuzz -i awesome-list/fuzz/inputs -o _build/default/awesome-list/fuzz/findings ./_build/default/awesome-list/fuzz/fuzz_me.exe @@

Or use the convenience dune alias:

$ dune build @awesome-list/fuzz --no-buffer

Both modes should find the bugs in a split second. In QuickCheck mode it'll pretty print the input value that triggered the failure. In AFL mode you can proceed as in the above example, i.e. kill the afl-fuzz process once it found the two unique crashes. From there, inspecting the input files won't tell you much as it's just used to seed crowbar PRNG but you can run the fuzz binary on those and the input values will be pretty printed the same way they are in QuickCheck mode:

$ cd _build/default/awesome-list/fuzz
$ ./fuzz_me.exe findings/crashes/<input_file>
Awesome_list.sort: ....
Awesome_list.sort: FAIL

When given the input:

    [4; 5; 6]

the test failed:

    check false

Fatal error: exception Crowbar.TestFailure

Bun and fuzz testing in CI

This repo also contain Drone CI scripts that run fuzz testing for both our examples. afl-fuzz is not very CI friendly by essence so we use bun. bun is a CLI wrapper for afl-fuzz, written in OCaml. It takes care of a few things for you, such as running several fuzzing processes in parallel while correctly setting afl-fuzz to do so but most of all it wraps each of those processes so that the execution fail whenever one of them finds a crash. Finally, it will also display the input that lead to the test failure so that you can try to reproduce and debug it locally.

The CI script itself is in the .drone.yml file and it looks like that:

kind: pipeline
name: amd

platform:
  os: linux
  arch: amd64

steps:
- name: build
  image: ocaml/opam2:4.07
  commands:
  - sudo apt-get update && sudo apt-get -y install afl
  - sudo chown -R opam .
  - git -C /home/opam/opam-repository pull origin && opam update
  - opam switch 4.07+afl
  - opam depext crowbar bun
  - opam install -y crowbar bun
  - opam exec -- dune build @bun-fuzz --no-buffer

As you can see, there's nothing special here. We install the dependencies and build the bun-fuzz alias.

There's one for simple-parser and one for awesome-list. The aliases are identical and defined as:

(alias
 (name bun-fuzz)
 (locks %{project_root}/bun)
 (deps
  (:exe fuzz_me.exe)
  (source_tree input))
 (action
  (run bun --input inputs --output findings -- ./%{exe})))

The default bun invocation is very similar to a regular afl-fuzz invocation. You'll note the use of (locks %{project_root}/bun) to prevent concurrent execution of fuzz tests by dune. This is required because by default bun will use all available cores and afl-fuzz won't use a core that's already running an afl-fuzz process.

You may try it locally:

$ dune build @awesome-list/bun-fuzz
09:05.39:Fuzzers launched: [1 (pid=7357); 2 (pid=7358); 3 (pid=7359);
                            4 (pid=7360); 5 (pid=7361); 6 (pid=7362);
                            7 (pid=7363); 8 (pid=7364)].
09:05.39:Fuzzer 1 (pid=7357) finished
Crashes found! Take a look; copy/paste to save for reproduction:
echo J3JhaWl0IA== | base64 -d > crash_0.$(date -u +%s)
09:05.39:[ERROR]All fuzzers finished, but some crashes were found!

or you can take a look at the latest build.

bun comes with a bunch of configuration CLI options and I invite you to take a look at its documentation by running bun --help to find out how to make the most out of it in your particular use case.

One useful bun feature is its no-kill mode. There's a bun-fuzz-no-kill alias available for awesome-list fuzz tests, it's defined as:

(alias
 (name bun-fuzz-no-kill)
 (locks %{project_root}/bun)
 (deps
  (:exe fuzz_me.exe)
  (source_tree input))
 (action
  (run timeout --preserve-status 1m bun --no-kill --input inputs --output
    findings -- ./%{exe})))

The --no-kill option tells bun to let the fuzzing processes run even after they found their first crash. That can be convenient if you're not very confident about an implementation you're fuzzing and you are expecting to find several bugs in it. What makes this mode so nice is that it integrates with timeout fairly well. When receiving SIGTERM, bun will shutdown all the afl-fuzz processes and will pretty print the crash triggering inputs to the standard output in the same way it does in regular mode which makes this a viable option for running fuzz tests in CI as well.