This article describes a new level of fortification supported in GCC. This new level detects more buffer overflows and bugs which mitigates security issues in applications at run time.
C programs routinely suffer from memory management problems. For several years, a
_FORTIFY_SOURCE preprocessor macro inserted error detection to address these problems at compile time and run time. To add an extra level of security,
_FORTIFY_SOURCE=3 has been in the GNU C Library (glibc) since version 2.34. I described its mechanisms in my previous blog post, Broadening compiler checks for buffer overflows in _FORTIFY_SOURCE. There has been compiler support for this builtin in Clang for some time. Compiler support has also been available for GCC since the release of version 12 in May 2022. The new mitigation should be available in GNU/Linux distributions with packaged GCC 12.
The following sections discuss two principal gains from this enhanced level of security mitigation and the resulting impact on applications.
2 principal gains:
Enhanced buffer size detection
Better fortification coverage
1. A new builtin provides enhanced buffer size detection
There is a new builtin underneath the new
_FORTIFY_SOURCE=3 macro n GCC 12 named
__builtin_dynamic_object_size. This builtin is more powerful than the previous
__builtin_object_size builtin used in
_FORTIFY_SOURCE=2. When passed a pointer,
__builtin_object_sizereturns as a compile-time constant that is either the maximum or minimum object size estimate of the object that pointer may be pointing to at that point in the program. On the other hand,
__builtin_dynamic_object_size is capable of returning a size expression that is evaluated at execution time. Consequently, the
_FORTIFY_SOURCE=3 builtin detects buffer overflows in many more places than
The implementation of
__builtin_dynamic_object_size in GCC is compatible with
__builtin_object_size and thereby interchangeable, especially in the case of fortification. Whenever possible, the builtin computes a precise object size expression. When the builtin does not determine the size exactly, it returns either a maximum or minimum size estimate, depending on the size type argument.
This code snippet demonstrates the key advantage of returning precise values:
char *__attribute__ ((noinline)) do_set (bool cond)
char *buf = buf1;
buf = malloc (42);
memset (buf, 0, 22);
int main (int argc, char **argv)
b = do_set (false);
The program runs to completion when built with
gcc -O -D_FORTIFY_SOURCE=2 -o sample sample.c
But the program aborts when built with
-D_FORTIFY_SOURCE=3 and outputs the following message:
*** buffer overflow detected ***: terminated
Aborted (core dumped)
The key enhancement stems from the difference in behavior between
__builtin_object_size and returns the maximum estimate for object size at pointer
buf, which is 42. Hence, GCC assumes that the
memset operation is safe at compile time and does not add a call to check the buffer size at run time.
However, GCC with
__builtin_dynamic_object_size to emit an expression that returns the precise size of the buffer that
buf points to at that part in the program. As a result, GCC realizes that the call to
memset might not be safe. Thus, the compiler inserts a call to
__memset_chk into the running code with that size expression as the bound for
2. Better fortification coverage
Building distribution packages with
_FORTIFY_SOURCE=3 revealed several issues that
_FORTIFY_SOURCE=2 missed. Surprisingly, not all of these issues were straightforward buffer overflows. The improved fortification also encountered issues in the GNU C library (glibc) and raised interesting questions about object lifetimes.
Thus, the benefit of improved fortification coverage has implications beyond buffer overflow mitigation. I will explain the outcomes of
_FORTIFY_SOURCE=3 increased coverage in the following sections.
More trapped buffer overflows
Building applications with
_FORTIFY_SOURCE=3 detected many simple buffer overflows, such as the off-by-one access in clisp issue. We expected these revelations, which strengthened our justification for building applications with
To further support the use of
_FORTIFY_SOURCE=3 to improve fortification, we used the Fortify metrics GCC plugin to estimate the number of times _FORTIFY_SOURCE=3 resulted in a call to a checking function (
__memset_chk, etc.). We used Fedora test distribution and some of the
Server package group as the sample, which consisted of 96 packages. The key metric is fortification coverage, defined by counting the number of calls to
__builtin_object_size that resulted in a successful size determination and the ratio of this number taken to the total number of
__builtin_object_size calls. The plugin also shows the number of successful calls if using
__builtin_dynamic_object_size instead of
__builtin_object_size, allowing us to infer the fortification coverage if all
__builtin_object_size calls were replaced with
In this short study, we found that
_FORTIFY_SOURCE=3 improved fortification by nearly 4 times. For example, the Bash shell went from roughly 3.4% coverage with
_FORTIFY_SOURCE=2 to nearly 47% with
_FORTIFY_SOURCE=3. This is an improvement of nearly 14 times. Also, fortification of programs in
sudo went from a measly 1.3% to 49.57% — a jump of almost 38 times!
The discovery of bugs in glibc
The increased coverage of
_FORTIFY_SOURCE=3 revealed programming patterns in application programs that tripped over the fortification without necessarily a buffer overflow. While there were some bugs in glibc, we had to either explain why we did not support it or discover ways to discourage those programming patterns.
One example is
wcrtomb, where glibc makes stronger assumptions about the object size passed than POSIX allowed. Specifically, glibc assumes that the buffer passed to
wcrtomb is always at least
MB_CUR_MAX bytes long. In contrast, the POSIX description makes no such assumption. Due to this discrepancy, any application that passed a smaller buffer would potentially make
wcrtomb overflow the buffer during conversion. Then the fortified version
__wcrtomb_chk aborts with a buffer overflow, expecting a buffer that is
MB_CUR_MAX bytes long. We fixed this bug in glibc-2.36 by making glibc conform to POSIX .
_FORTIFY_SOURCE=3 revealed another pattern. Applications such as systemd used
malloc_usable_size to determine available space in objects and then used the residual space. The glibc manual discourages this type of usage, dictating that
malloc_usable_size is for diagnostic purposes only. But applications use the function as a hack to avoid reallocating buffers when there is space in the underlying malloc chunk. The implementation of
malloc_usable_size needs to be fixed to return the allocated object size instead of the chunk size in non-diagnostic use. Alternatively, another solution is to deprecate the function. But that is a topic for discussion by the glibc community.
Strict C standards compliance
One interesting use case exposed by
_FORTIFY_SOURCE=3 raised the question of object lifetimes and what developers can do with freed pointers. The bug in question was in AutoGen, using a pointer value after reallocation to determine whether the same chunk extended to get the new block of memory. This practice allowed the developer to skip copying over some pointers to optimize for performance. At the same time, the program continued using the same pointer, not the
realloc call result, since the old pointer did not change.
Seeing that the old pointer continued without an update, the compiler assumed that the object size remained the same. How could it know otherwise? The compiler then failed to account for the reallocation, resulting in an abort due to the perceived buffer overflow.
Strictly speaking, the C standards prohibit using a pointer to an object after its lifetime ends. It should neither be read nor dereferenced. In this context, it is a bug in the application.
However, this idiom is commonly used by developers to prevent making redundant copies. Future updates to GCC may account for this idiom wherever possible, but applications should also explicitly indicate object lifetimes to remain compliant. In the AutoGen example, a simple fix is to unconditionally refresh the pointer after reallocation, ensuring the compiler can detect the new object size.
The gains of improved security coverage outweigh the cost
_FORTIFY_SOURCE=3 may impact the size and performance of the code. Since
_FORTIFY_SOURCE=2 generated only constant sizes, its overhead was negligible. However,
_FORTIFY_SOURCE=3 may generate additional code to compute object sizes. These additions may also cause secondary effects, such as register pressure during code generation. Code size tends to increase the size of resultant binaries for the same reason.
We need a proper study of performance and code size to understand the magnitude of the impact created by
_FORTIFY_SOURCE=3 additional runtime code generation. However the performance and code size overhead may well be worth it due to the magnitude of improvement in security coverage.
The future of buffer overflow detection
_FORTIFY_SOURCE=3 has led to significant gains in security mitigation. GCC 12 support brings those gains to distribution builds. But the new level of fortification also revealed interesting issues that require additional work to support correctly. For more background information, check out my previous article, Enhance application security with FORTIFY_SOURCE.
Object size determination and fortification remain relevant areas for improvements in compiler toolchains. The toolchain team at Red Hat continues to be involved in the GNU and LLVM communities to make these improvements.Last updated: November 8, 2023