Home
Unchained
Engineering Blog

Using Compiler Flags to Secure Your Code

Adrian Mouat, Staff DevRel Engineer

At Chainguard, we recently adopted the OpenSSF-recommended compiler flags for building C/C++ code to improve memory safety and security in our packages. I thought it would be fun to dig into what effect some of these flags have, in particular -D_FORTIFY_SOURCE, which is all about replacing unsafe versions of C standard library calls with “hardened” versions that do bounds checking. It’s been some time since I coded C/C++ in anger, and reading the docs on the _FORTIFY_SOURCE macro still left me a bit confused, so I had a go at writing some example code to demonstrate what happens. This post is relatively high-level and should be easy to follow along even if you don't know C.



The demo code is available in this gist. You will be able to build and run this code locally if you have a C compiler installed, but for reproducibility we'll use this Dockerfile to get a development environment. First save the gist as fortify.c in a folder, then create a Dockerfile with the following contents:


FROM cgr.dev/chainguard/gcc-glibc:latest-dev@sha256:74f534d01e9644a77d5af4b405433c76f58d7d55fc0aeb7aa8c8cea62cbafa8a
RUN apk add vim # add your editor of choice
COPY fortify.c .
ENTRYPOINT ["/bin/sh"]

You can then build the Dockerfile and run the image to get a container with a development environment:


docker build -t devenv .
docker run -it devenv

That should give you a shell prompt where you can test building the code (don't worry about the warnings, we'll come back to that later):


/work # gcc -D_FORTIFY_SOURCE=0 -fno-stack-protector fortify.c -o ./fortify

I'll omit the prompt from here on, but all the following commands should be run inside the container. The above gcc command will build the code with _FORTIFY_SOURCE and stack protections off, so we can see what happens. Let's see what happens when we run the code:


./fortify
Exercising my buffers
Memset Overflow

Memset Overflow End
Overflow Struct Test
MoreTextThanBuffer
Overflow Struct Test End
Simple Buffer Overflow
MoreTextThanBuffer
Simple Buffer Overflow Test End
My buffers hurt
Segmentation fault

You may or may not get the segmentation fault at the end, but the program has run to the final print statement. 


So what's going on here? Let's start by looking at the overflowBuffer function:


void overflowBuffer() {

    printf("Simple Buffer Overflow\n");
    char large_input[] = "MoreTextThanBuffer";
    char small_buf[8];
    strcpy(small_buf, large_input);
    printf("%s\n", small_buf);
    printf("Simple Buffer Overflow Test End\n");
}

This is about as simple a buffer overflow as you can get. The string "MoreTextThanBuffer" has more than eight characters, so it overflows the buffer when strcpy is called. The reason buffer overflows should scare you is that they can potentially allow attackers to overwrite memory and do horrible things (see Smashing the Stack for Fun and Profit, but note that paper was written in 1996 and various protections, including _FORTIFY_SOURCE are now in place). In the best case, they will cause your program to crash and should be eradicated whenever possible.


When compiling earlier, you probably noticed some interesting warnings, such as:


fortify.c: In function 'overflowBuffer':
fortify.c:48:5: warning: 'strcpy' writing 19 bytes into a region of size 8 [-Wstringop-overflow=]
   48 |     strcpy(small_buf, large_input);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Thankfully, it's now quite hard to accidentally write simple buffer overflows with modern compilers. Going back to the example, let's try recompiling with _FORTIFY_SOURCE set to 1, which is the lowest level (and also the default, at least in my configuration):


gcc -D_FORTIFY_SOURCE=1 -fno-stack-protector fortify.c -o ./fortify
./fortify

This will give you the following output:


Exercising my buffers
Memset Overflow

Memset Overflow End
Overflow Struct Test
MoreTextThanBuffer
Overflow Struct Test End
Simple Buffer Overflow
*** buffer overflow detected ***: terminated
Aborted

So what's happened here? The _FORTIFY_SOURCE macro has swapped the strcpy call to a safer version that has performed bounds checking and aborted the program. It might not look like it, but this is a major improvement over the previous version. While the previous version got further through the program, it also corrupted memory that would have led to unpredictable and unstable behaviour in a longer running program. The guiding principle is that it is better to crash fast than to return incorrect answers.


The second level of _FORTIFY_SOURCE adds some more checks including the ability to check buffers inside objects (even if the whole object has room to store the data). We can see this in the following function:


void overflowStruct() {

    printf("%s\n", "Overflow Struct Test");
    char large_input[] = "MoreTextThanBuffer";
    struct outerStruct {
	struct innerStruct {
	    char buf[4];
	    int n;
	} inner;
	char buf[20];
    };
    struct outerStruct test_struct;
    strcpy(test_struct.inner.buf, large_input);
    printf("%s\n", test_struct.inner.buf);
    printf("%s\n", "Overflow Struct Test End");
}

In this case, we are overflowing the buffer inside the "inner" struct. If you compile with _FORTIFY_SOURCE set to 1, this isn't caught, but by upping the level to _FORTIFY_SOURCE=2:


gcc -D_FORTIFY_SOURCE=2 -fno-stack-protector fortify.c -o ./fortify
./fortify

We get:


Exercising my buffers
Memset Overflow

Memset Overflow End
Overflow Struct Test
*** buffer overflow detected ***: terminated
Aborted

The new code has detected the overflow at runtime and aborted the program.


The highest level (at the time of writing) of _FORTIFY_SOURCE is 3. This level adds bound checking when the buffer size is dependent on a variable value. In our example code we have the following function:


void memsetOverflow(int b) {

    printf("Memset Overflow\n");
    char small_buf[8];
    char *sbp = small_buf;
    if (b) {
	sbp = malloc(23);
    }
    memset(sbp, 0, 22);
    printf("%s\n", sbp);
    printf("Memset Overflow End\n");
}

Here, the size of the buffer is dependent on the value of the argument "b". With _FORTIFY_SOURCE=2, it sees the maximum size of the buffer is 23, which is enough to hold the data. With _FORTIFY_SOURCE=3, it is clever enough to add an expression (rather than a constant) that evaluates to the correct size of the buffer. Hence if we run with _FORTIFY_SOURCE=2 the code will run, but if we compile with _FORTIFY_SOURCE=3:


gcc -D_FORTIFY_SOURCE=3 -fno-stack-protector fortify.c -o ./fortify

And run the program:


./fortify
Exercising my buffers
Memset Overflow
*** buffer overflow detected ***: terminated
Aborted

The overflow is again caught.


When wouldn't you want to use _FORTIFY_SOURCE=3? There is an argument that because _FORTIFY_SOURCE=3 will evaluate variable expressions at runtime, there is potential for a performance overhead. In reality, this seems to be rare and I would encourage everyone to turn it on by default and investigate if they see unexpected performance issues. 


Finally, what about -fno-stack-protector? Let's try running with the stack protector on but _FORTIFY_SOURCE off:


gcc -D_FORTIFY_SOURCE=0 -fstack-protector fortify.c -o ./fortify
./fortify

We get:


Memset Overflow

Memset Overflow End
*** stack smashing detected ***: terminated
Aborted

The stack protector has added guard checks around the function that have detected the overflow and again aborted the program. This test is completely separate from _FORTIFY_SOURCE and will result in a small overhead due to the extra checks.


Hopefully that explains what these checks do and why everyone should enable them. The performance overhead cost is negligible, but the potential benefit from preventing buffer overflows is large. And if you're using Chainguard Images, you are already taking advantage of these settings!


Not using Chainguard Images? You can check out our Images Directory to see if we have what you need, and contact us if you would like to learn more.

Share

Ready to Lock Down Your Supply Chain?

Talk to our customer obsessed, community-driven team.

Get Started