glibc 2.44 enables FORTIFY_SOURCE=3 by default and your struct tricks are now fatal
Published by RodHat

glibc 2.44 shipped last week. The headline security change: FORTIFY_SOURCE=3 is now the
default for builds using GCC 12 or later. If you are running a distribution that builds glibc
from source and ships a recent GCC, your next glibc update may start producing SIGABRT in
code that never crashed before.
Some of that code has real bugs. The rest has been playing games that technically worked because nobody was watching closely enough. Either way, glibc is watching now.
What FORTIFY_SOURCE actually does
The short version: -D_FORTIFY_SOURCE=N tells glibc to replace calls to functions like
memcpy, strcpy, sprintf, and about forty others with versions that check whether the
destination buffer is large enough before writing. If the destination is smaller than the
write, the program aborts with a stack trace.
Level 1 has been around since glibc 2.3 in 2003. It uses __builtin_object_size to compute
buffer sizes at compile time. Static buffers. Things the compiler can see directly.
Level 2, added in glibc 2.14, extends this to more call sites and enables the __OPTIMIZE__
flag requirement so you actually have to be compiling with optimizations turned on for it to
work. Widely deployed in production for years. Most major distros have shipped level 2 for
a decade.
Level 3, added in glibc 2.35 with GCC 12 support, does something different: it uses
__builtin_dynamic_object_size instead of the static version. The dynamic variant queries
object size at runtime, not compile time. This catches overflows into heap-allocated buffers
where the size is only known when the program is running, which level 2 could not see.
That is the change. Level 2 checks what the compiler knows. Level 3 checks what the program knows at the moment the dangerous function runs.
What it breaks and why
A struct with a char array member at the end, and then a memcpy that writes past the
declared array size into adjacent heap memory that the allocation actually covers. This
pattern exists in a lot of old Unix code. The struct is allocated with extra bytes past the
end, the array is written as if it is larger than declared, and it works because the allocator
gave you the memory. Level 2 never saw this because the array size is a compile-time constant.
Level 3 sees the dynamic allocation size and knows the write is past the array boundary.
The Linux kernel headers have used __flexarr and similar macros to declare zero-length or
one-element arrays at the end of structs since before most people reading this were alive.
Userspace code that copies kernel structures or mimics kernel patterns for its own purposes
often carries the same idiom. Some of that code is now aborting.
Container-of patterns, where you deliberately write to a char buffer inside one struct and later read it out through a different struct type, also trip level 3 in cases where the destination type’s known size is smaller than the write. Yes, it is undefined behavior in C23. It has been undefined behavior for a while. That has not stopped people from doing it.
There is also a category of sprintf-family calls into stack buffers where level 3’s dynamic size pass finds cases level 2 missed because the buffer size was passed through a function argument and the compiler lost track of it. These are actual bugs. They should abort.
The part where Rod grudgingly admits this is correct
Thirty years of gcc -Wall not catching a bug does not mean the bug is not there. It means
-Wall was not looking in the right place.
FORTIFY_SOURCE=3 aborts are not false positives in the traditional sense. The abort happens when a write would go past the boundary of the object the runtime knows about. If your code writes past that boundary and “works,” it works because of allocator alignment padding or adjacent allocations that happen to survive the corruption. That is not correct behavior. It is a bug that has not killed you yet.
The real complaint is that this change landed as a default without enough lead time for production code to audit clean. glibc 2.35 shipped level 3 support in February 2022. Four years was apparently not enough for every downstream project to notice. That is on the projects, not on glibc.
Fedora enabled FORTIFY_SOURCE=3 in its build flags a while ago. The screaming was audible from several kernel-adjacent projects and then it was fixed. Those projects are better for it. The same thing is happening at the glibc-default layer now, just broader.
What to do
If you hit a SIGABRT after a glibc update, the abort message will name the function. A
stack trace from a core file or ASAN_OPTIONS=abort_on_error=0 run will give you the call
site. From there you either have a real bug to fix or an intentional pattern to audit.
For intentional struct-hack patterns that you own and know are safe: you can use
__attribute__((access(...))) annotations in GCC 12+ to tell the compiler the actual
intended access range. You can also rewrite to use explicit flexible array members
(char data[];) declared at the end of the struct, which is what C99 standardized this
idiom into. The old one-element or zero-element hack predates the standard; the standard
exists now.
For third-party code that aborts: file a bug upstream. Do not suppress the abort with
-D_FORTIFY_SOURCE=0 in your build flags. That defeats the whole point and leaves the
underlying bug resident.
For code you cannot fix: -D_FORTIFY_SOURCE=2 in that translation unit is a targeted
downgrade. Better than nuking it globally. Still worse than fixing the code.
Kerrisk’s TLPI covers the glibc object-size machinery in detail as part of the broader
security hardening chapter. See /go/book-tlpi/ if you want to understand
what the compiler and runtime are actually checking and why the level distinction matters
at the ABI level.
Related: Landlock in the 7.3 merge window covers the kernel-side of the “restrict what this process can do” story. And if the supply-chain angle of a security default change interests you, the XZ backdoor one-year scorecard is still relevant reading on how defaults become attack surfaces.
Sources
- glibc 2.44 release announcement (GNU C Library project (libc-announce mailing list))
- GNU C Library 2.44 Released With FORTIFY_SOURCE=3 Default (Phoronix)