Using ASAN with AFL

This file discusses some of the caveats for fuzzing under ASAN, and suggests a handful of alternatives.

Short version

ASAN on 64-bit systems requests a lot of memory in a way that can’t be easily distinguished from a misbehaving program bent on crashing your system.

Because of this, fuzzing with ASAN is recommended only in four scenarios:

  • On 32-bit systems, where we can always enforce a reasonable memory limit (-m 800 or so is a good starting point),
  • On 64-bit systems only if you can do one of the following:
    • Compile the binary in 32-bit mode (gcc -m32),
    • Precisely gauge memory needs using http://jwilk.net/software/recidivm .
    • Limit the memory available to process using cgroups on Linux (see experimental/asan_cgroups).

To compile with ASAN, set AFL_USE_ASAN=1 before calling make clean all. The afl-gcc / afl-clang wrappers will pick that up and add the appropriate flags. Note that ASAN is incompatible with -static, so be mindful of that.

(You can also use AFL_USE_MSAN=1 to enable MSAN instead.)

There is also the option of generating a corpus using a non-ASAN binary, and then feeding it to an ASAN-instrumented one to check for bugs. This is faster, and can give you somewhat comparable results. You can also try using libdislocator (see libdislocator/README.dislocator in the parent directory) as a lightweight and hassle-free (but less thorough) alternative.

Long version

ASAN allocates a huge region of virtual address space for bookkeeping purposes. Most of this is never actually accessed, so the OS never has to allocate any real pages of memory for the process, and the VM grabbed by ASAN is essentially “free” - but the mapping counts against the standard OS-enforced limit (RLIMIT_AS, aka ulimit -v).

On our end, afl-fuzz tries to protect you from processes that go off-rails and start consuming all the available memory in a vain attempt to parse a malformed input file. This happens surprisingly often, so enforcing such a limit is important for almost any fuzzer: the alternative is for the kernel OOM handler to step in and start killing random processes to free up resources. Needless to say, that’s not a very nice prospect to live with.

Unfortunately, un*x systems offer no portable way to limit the amount of pages actually given to a process in a way that distinguishes between that and the harmless “land grab” done by ASAN. In principle, there are three standard ways to limit the size of the heap:

  • The RLIMIT_AS mechanism (ulimit -v) caps the size of the virtual space - but as noted, this pays no attention to the number of pages actually in use by the process, and doesn’t help us here.
  • The RLIMIT_DATA mechanism (ulimit -d) seems like a good fit, but it applies only to the traditional sbrk() / brk() methods of requesting heap space; modern allocators, including the one in glibc, routinely rely on mmap() instead, and circumvent this limit completely.
  • Finally, the RLIMIT_RSS limit (ulimit -m) sounds like what we need, but doesn’t work on Linux - mostly because nobody felt like implementing it.

There are also cgroups, but they are Linux-specific, not universally available even on Linux systems, and they require root permissions to set up; I’m a bit hesitant to make afl-fuzz require root permissions just for that. That said, if you are on Linux and want to use cgroups, check out the contributed script that ships in experimental/asan_cgroups/.

In settings where cgroups aren’t available, we have no nice, portable way to avoid counting the ASAN allocation toward the limit. On 32-bit systems, or for binaries compiled in 32-bit mode (-m32), this is not a big deal: ASAN needs around 600-800 MB or so, depending on the compiler - so all you need to do is to specify -m that is a bit higher than that.

On 64-bit systems, the situation is more murky, because the ASAN allocation is completely outlandish - around 17.5 TB in older versions, and closer to 20 TB with newest ones. The actual amount of memory on your system is (probably!) just a tiny fraction of that - so unless you dial the limit with surgical precision, you will get no protection from OOM bugs.

On my system, the amount of memory grabbed by ASAN with a slightly older version of gcc is around 17,825,850 MB; for newest clang, it’s 20,971,600. But there is no guarantee that these numbers are stable, and if you get them wrong by “just” a couple gigs or so, you will be at risk.

To get the precise number, you can use the recidivm tool developed by Jakub Wilk (http://jwilk.net/software/recidivm). In absence of this, ASAN is not recommended when fuzzing 64-bit binaries, unless you are confident that they are robust and enforce reasonable memory limits (in which case, you can specify ‘-m none’ when calling afl-fuzz).

Using recidivm or running with no limits aside, there are two other decent alternatives: build a corpus of test cases using a non-ASAN binary, and then examine them with ASAN, Valgrind, or other heavy-duty tools in a more controlled setting; or compile the target program with -m32 (32-bit mode) if your system supports that.

Interactions with the QEMU mode

ASAN, MSAN, and other sanitizers appear to be incompatible with QEMU user emulation, so please do not try to use them with the -Q option; QEMU doesn’t seem to appreciate the shadow VM trick used by these tools, and will likely just allocate all your physical memory, then crash.

ASAN and OOM crashes

By default, ASAN treats memory allocation failures as fatal errors, immediately causing the program to crash. Since this is a departure from normal POSIX semantics (and creates the appearance of security issues in otherwise properly-behaving programs), we try to disable this by specifying allocator_may_return_null=1 in ASAN_OPTIONS.

Unfortunately, it’s been reported that this setting still causes ASAN to trigger phantom crashes in situations where the standard allocator would simply return NULL. If this is interfering with your fuzzing jobs, you may want to cc: yourself on this bug:

What about UBSAN?

Some folks expressed interest in fuzzing with UBSAN. This isn’t officially supported, because many installations of UBSAN don’t offer a consistent way to abort() on fault conditions or to terminate with a distinctive exit code.

That said, some versions of the library can be binary-patched to address this issue, while newer releases support explicit compile-time flags - see this mailing list thread for tips: