Using Compiler Flags to Secure Your Code
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 this article
Related articles
- Engineering
It’s time to rethink golden images. Chainguard can help.
Chainguard helps teams build developer-centric golden image programs with zero-CVE, purpose-built containers—balancing speed, security, and standardization.
Sam Katzen, Staff Product Marketing Manager
- Engineering
Why building from source matters
Chainguard SVP of Engineering Dustin Kirkland discusses why Chainguard builds every package, library, and image directly from source and why the approach works.
Dustin Kirkland, SVP of Engineering
- Engineering
Accelerating Platform Adoption with Developer Trust
Chainguard helps Platform teams drive adoption with zero-CVE, customizable container images that make internal development platforms secure, fast, and trusted.
Sam Katzen, Staff Product Marketing Manager, and Matt Stead, Marketing
- Engineering
A Gift for the Open Source Community: Chainguard’s CVE-Free Raspberry Pi Images (Beta)
Chainguard has created the first-ever CVE-free, vulnerability-free Raspberry Pi image. Learn more about how it works and what makes this special.
Dustin Kirkland, SVP of Engineering
- Engineering
How CTOs Can Justify Technology Investments to the Board
Learn how CTOs can tie technology investments to increasing revenue, speeding innovation, and reducing risk and cost to drive positive business outcomes.
Matt Moore, CTO and Co-founder
- Engineering
Guest Post: Resiliency by Design and the Importance of Internal Developer Platforms
Gaurav Saxena, a Director of Engineering at an automotive company, talks through how internal developer platforms are an important part of resiliency by design.
Gaurav Saxena, Director of Engineering, Automotive Company