A Tale of Two Compilers

What’s wrong with the following C code?

char buf[32];
scanf("%32s", buf);

It’s a classic and easy to make off-by-one error, caused by the willy-nilly inconsistency of common C functions regarding whose responsibility the null terminator is and whether it’s included in a passed count of bytes. In this case, scanf() will read up to 32 bytes from standard input and then append a null terminator, which overflows the buffer of 32 characters and writes a null byte to whatever happens to be next on the stack.

If you compile this program, run it, and give it a long input, you might see the stack corrupt and the program output 0xabad1d00. Or… you might not.

#include <stdio.h>

int main(int argc, char *argv[]) {

    int x = 0xabad1dea; // the best number.
    char buf[32];

    scanf("%32s", buf);
    printf("%s 0x%xn", buf, x);

    return 0;
}

When I run this with the input of the alphabet written out twice in a row (52 characters), the output stops at the second “F” (that’s character 32) and correctly displays 0xabad1dea. So, everything’s fine, right? The compiler must have fixed it for us. Thanks, compiler!

The compiler is actually unintentionally stabbing us in the back. (Though every programmer suspects it might sometimes be intentional.) For whatever reason suits it, it has chosen to leave empty padding bytes on the stack between the buffer and the integer, which means that slightly overflowing the buffer does not cause any corruption of other variables at this time. What happens when at some later point in the program, you either use an unbounded string copy on the assumption it’s 32 bytes or less, or copy up to 32 bytes, assuming the null terminator is included in that bound? You start corrupting other things, which may or may not be immediately apparent for the same happenstance reasons it may or may not have been immediately apparent the first go around. Your code may crash or output corrupted data three thousand lines and seventeen copy operations away.

On my Mac I have two different compilers (llvm-gcc and clang) and they output two different things for the above program when compiled for 64-bit with no extra flags: 0xabad1d00 and 0xabad1dea respectively. Neither is wrong: it’s my program that has a bug, and whether or not that bug affects the output depends on choices the compiler is entitled to make about stack layout. Both compilers are using stack guard protection, but – in the case of this protection scheme – it only works if we overflow the end of a stack frame. Since the buffer is not the highest variable on the stack, overflowing it by one byte does not overwrite the guard value at the end of the stack frame and trigger an abort trap. You can experiment with this by changing the scan format string to use a much larger number (try 64) and feeding it that much data. You can also empirically determine exactly how many bytes of padding are between the buffer and the integer but again: this is entirely implementation-dependent and it can change between every run if that suits the compiler’s mood. (Four. On my machine with this version of clang, there are four bytes of dead space, and zero with llvm-gcc.)

A key point to realize is that a bug in your code that may have been entirely latent for some time will suddenly manifest when you change compilers, including updating your current one. Don’t blame the poor compiler! The bug was yours all along. As my mentor in security paranoia taught me: Constant Vigilance! Never rely on a compiler “fixing” a bug for you. Be proactive in killing them even if they don’t seem to be hurting anything.

It just so happens that one of the features of Veracode’s binary analysis service is detecting off-by-one (or off-by-a-lot) errors in C and C++ such as this one – at a real-world scale.

Anon | December 8, 2013 5:25 pm

If you were using modern Clang/GCC why wouldn’t you compile your code with extra checks performed by the likes of AddressSanitizer (http://en.wikipedia.org/wiki/Buffer_overflow_protection#Clang.2FLLVM )? Things like fstack-protector are a tradeoff between accuracy (tries to mitigate the worst damage that can be done) and speed…

Please Post Your Comments & Reviews

Your email address will not be published. Required fields are marked *

RSS feed for comments on this post