Know your CFLAGS

Simple Tips to find Bugs with Compiler Features

The gcc and clang compilers have a couple of compile time and runtime features that allow developers to find potentially security relevant bugs in their code. Sometimes just running a software with these features enabled will expose bugs.

Every developer of a C / C++ software should be aware of these features and use them to have code with fewer bugs. In a project using standard autotools this can be as simple as running:

./configure CFLAGS="-fsanitize=address,undefined -Wformat -Werror=format-security -Werror=array-bounds -g" CXXFLAGS="-fsanitize=address,undefined -Wformat -Werror=format-security -Werror=array-bounds -g" LDFLAGS="-fsanitize=address,undefined" CC="clang" CXX="clang++"
make
make check

Appart from using available tests it often makes sense to just run the software and test it. That may already expose bugs.

Let's look at these flags in detail:

Address Sanitizer (-fsanitize=address)

Out of bounds memory access and other memory access errors like use after free are very common problems in C / C++ software. Some of these errors make an application crash and are therefore easily detected, but others usually don't crash.

Take this simple code:

int a[2] = {2, 1};
int b = a[2];

This is a typical stack out of bounds read error. The array a has two elements with indices 0 and 1, the element 2 does not exist. However this code usually won't crash, so bugs like this often go unnoticed.

Address Sanitizer (asan), enabled with the compiler flag -fsanitize=address, will add additional bounds checks and report such errors. Address Sanitizer is usually not meant to be used for production software (although one might decide to do so for high risk applications), because it causes a significant slow down. As Address Sanitizer uses a special library for checks we also have to add it to the linker flags (LDFLAGS).

A surprisingly large number of applications will report errors just by compiling and running it with Address Sanitizer enabled. This indicates that many developers are not aware of this powerful feature.

Undefined Behaviour Sanitizer (-fsanitize=undefined)

A couple of things that developers might do in their C code is considered undefined behaviour by the C standard. Often these things are surprising to developers if they are not aware of it. If an undefined behaviour occurs the compiler is free to do whatever it wants. The programmer can not expect any reliable functionality of the code, therefore it should be avoided.

The most common examples of undefined behaviour are signed integer overflows and various invalid uses of shift operators. Take these for example:

int a = INT_MAX;
a++;

Setting the integer a to the highest possible integer value and then trying to increment will cause an overflow. While unsigned overflows are defined and the variable will flip back to zero, signed overflows are undefined.

The Undefined Behaviour Sanitizer (ubsan), enabled with -fsanitize=undefined, can detect many undefined behaviour issues and will emit an error.

As with Address Sanitizer, Undefined Behaviour Sanitizer is not meant to be used in production software, because it causes a significant slowdown.

Format string warnings (-Wformat-security -Werror=format-security)

Format string vulnerabilities happen if an attacker controlled C string is directly used in a printf function. An example would be code like this:

char *c;
[some code to fill c]
printf(c);

If c here contains format string characters (%s, %i etc.) it will expect further parameters to the printf function, but there are none, so invalid memory gets accessed.

The parameter -Wformat-security, available in both gcc and clang, will warn about these issues. It will not check whether the variable is attacker controlled, but it is usually a good idea to just fix all instances of these warnings to code like this:

printf("%s", c);

Adding -Werror=format-security will make errors out of these warnings.

Please note that this option cannot find all instances of potential format string vulnerabilities. This code for example would not cause a warning:

printf(c, i);

Compile time out of bounds checks (-Warray-bounds -Werror=array-bounds)

Let's remember the array example from above:

int a[2] = {2, 1};
int b = a[2];

This is a pretty obvious error that can already be detected compile time. The clang compiler has the parameter -Warray-bounds that is enabled by default. These warnings can be made errors with -Werror=array-bounds. Currently gcc has no such feature.

Debugging (-g)

The -g parameter will add further debugging information to the executable. The reason for that is that the error messages by the sanitizers will contain line numbers. That makes it much easier to find the code causing the errors.

GCC versus clang

The sanitizer features and most compiler warnings are present in both gcc and clang (which is the C compiler of LLVM). However they are usually developed in clang and only later ported to gcc. Therefore you may get more advanced features if you use clang. One notable example is that the latest version of Address Sanitizer will enable checks for memory leaks (This is also in gcc 5.1, but that's not yet available on most Linux systems).

As mentioned above clang also contains rudimentary compile time bounds checking. For these two reasons it's recommended to use clang for testing.

Other security flags

There are a couple of compiler flags for production systems. Their main purpose is not to find bugs. Their purpose to prevent exploitation in software, therefore they are not covered in this tutorial. Many of them are enabled on common linux distributions these days, some unfortunately are not.

These flags include -fstack-protector (Stack canaries), -pie/-pic (improved Address Space Layout Randomization / ASLR) and -FORTIFY_SOURCE (buffer overflow detection).

Links

Undefined Behavior Sanitizer explanation in Red Hat's developer blog
Address Sanitizer runtime flags
Rudimentary ubsan runtime flag documentation from Chromium

CC0
The Fuzzing Project is run by Hanno Böck