[HN Gopher] Compiler Options Hardening Guide for C and C++
       ___________________________________________________________________
        
       Compiler Options Hardening Guide for C and C++
        
       Author : pjmlp
       Score  : 164 points
       Date   : 2025-03-31 11:01 UTC (11 hours ago)
        
 (HTM) web link (best.openssf.org)
 (TXT) w3m dump (best.openssf.org)
        
       | dailykoder wrote:
       | Nice, thank you! Saved this. Mastering GCC compiler options feels
       | harder than mastering C++ UB.
        
         | dapperdrake wrote:
         | Succinct.
        
       | dapperdrake wrote:
       | See also:
       | 
       | https://c9x.me/compile/bib/ubc.pdf
       | 
       | via https://c9x.me/compile/bib/
       | 
       | Related HN discussion:
       | https://news.ycombinator.com/item?id=26925314
        
       | dapperdrake wrote:
       | Related: Rob Pike on programming style, especially his note in
       | include files: http://doc.cat-v.org/bell_labs/pikestyle
       | 
       | See also: SQLites amalgamation. Others (iirc Philippe Gaultier)
       | have called this a Unity build:
       | https://sqlite.org/amalgamation.html
       | 
       | Rob Pike on systems software research:
       | http://doc.cat-v.org/bell_labs/utah2000/utah2000.html
       | 
       | EDIT: typo
        
         | z_open wrote:
         | His opinions on include files have fallen out of favor because
         | compiling is faster and it adds needless work. Are there
         | organizations that still do this? All the style guides I've
         | seen do not.
        
           | dapperdrake wrote:
           | If your filesystem and disks are fast enough, then maybe
           | Rob's assumptions don't apply.
        
           | ryandrake wrote:
           | I still adhere to this for personal hobby projects, more out
           | of a sense of craftsmanship than anything practical at this
           | point.
        
           | csb6 wrote:
           | I believe clang and gcc avoid reading in and re-processing
           | include files that are already included, so his advice is
           | unnecessary and creates a lot of maintenance burden,
           | especially for C++ where a lot more code is in header files.
           | It may still be useful for old compilers, though.
        
             | kevin_thibedeau wrote:
             | They recognize include guards and skip any further
             | inclusions for those cases. There are scenarios where you
             | may want multiple inclusion and you can still have that.
        
       | dapperdrake wrote:
       | Further reading:
       | 
       | Fabien Sanglard in driving compilers:
       | https://fabiensanglard.net/dc/
       | 
       | GNU binutils with their own take on how to process static
       | archives (libfoo.a)
       | https://sourceware.org/bugzilla/show_bug.cgi?id=32006
       | 
       | Linkers: Mold: https://news.ycombinator.com/item?id=26233244
       | 
       | Wild: https://news.ycombinator.com/item?id=42814683
       | 
       | List of FOSS C linkers:
       | 
       | GNU ld
       | 
       | GNU gold
       | 
       | LLVM lld mold (by LLVM lld author)
       | 
       | wild
       | 
       | EDIT: typesetting
        
         | plainOldText wrote:
         | The 5-part series by Fabien Sanglard is really good. Thanks for
         | sharing!
        
       | MITSardine wrote:
       | If my C++ project is a simple utility supposed to take some
       | files, crunch numbers, and spit out results, is there still the
       | possibility it can be used for nefarious purposes?
        
         | thfuran wrote:
         | How does it get its input files? Where does it run? What's the
         | output used for?
        
         | kibwen wrote:
         | It doesn't matter what the tool does, what matters is 1)
         | whether it is ever exposed to untrusted input, 2) what
         | permissions it has.
         | 
         | If you don't ever expose something to untrusted input, then
         | you're probably fine. But be VERY careful, because you should
         | defensively consider anything downloaded off the internet to be
         | untrusted input.
         | 
         | As for permissions, if you run a tool inside of a sandbox
         | inside of a virtual machine on an airgapped computer inside a
         | Faraday cage six stories underground, then you're probably
         | fine.
        
         | duped wrote:
         | Read/write access to a filesystem is a pretty large surface
         | area for attack, so yes.
        
       | tuananh wrote:
       | Wolfi OS (by Chainguard) is one of a few decided to adopt openssf
       | compiler options
       | 
       | https://github.com/wolfi-dev/os/blob/main/openssf-compiler-o...
        
       | derriz wrote:
       | Sane defaults should be table stakes for toolchains but C++ has
       | "history".
       | 
       | All significant C++ code-bases and projects I've worked on have
       | had 10s of lines (if not screens) of compiler and linker options
       | - a maintenance nightmare particularly with stuff related to
       | optimization. This stuff is so brittle, who knows when (with
       | which release of the compiler or linker) a particular combination
       | of optimization flags were actually beneficial? How do you
       | regression test this stuff? So everyone is afraid to touch this
       | stuff.
       | 
       | Other compiled languages have similar issues but none to the
       | extent of C++ that I've experienced.
        
         | nly wrote:
         | I've rarely seen more than a handful of compiler options even
         | on very large codebase
         | 
         | If anything there's tonnes people should be using more of.
         | 
         | The problem with all these hardening options though is they
         | noticeably reduce performance
        
           | grandempire wrote:
           | > The problem with all these hardening options though is they
           | noticeably reduce performance
           | 
           | Yep. What I would really like is 2 lists, one for
           | debug/checked mode and one for release.
        
         | duped wrote:
         | I mean if you emit compiler commands from any build system
         | they're going to be completely illegible due to the number of
         | -L,-l,-I,-i,-D flags which are mostly generated by things like
         | pkg-config and your build configuration.
         | 
         | There's not many optimization flags that people get fine
         | grained with, the exception being floating point because
         | -ffast-math alone is extremely inadvisable
        
           | dapperdrake wrote:
           | -ffast-math and -Ofast are _inadvisable_ on principle:
           | 
           | Tl;dr: python gevent messes up your x87 float registers
           | (yes.)
           | 
           | https://moyix.blogspot.com/2022/09/someones-been-messing-
           | wit...
        
             | duped wrote:
             | I disagree with "on principle." There are flaws in the
             | design of IEEE 754 and omitting strict adherence for the
             | purposes of performance is fine, if not required for some
             | applications.
             | 
             | For example, recursive filters (even the humble averaging
             | filter) will suffer untold pain without enabling DAZ/FTZ
             | mode.
             | 
             | fwiw the linked issue has been remedied in recent compilers
             | and isn't a python problem, it's a gcc problem. Even that
             | said, if your algorithm _requires_ subnormal numbers, for
             | the love of numeric stability, guard your scopes and set
             | the mxcsr register accordingly!
        
               | dapperdrake wrote:
               | In practice, "some applications" seems to include almost
               | all of NumPy and Python. Good call.
               | 
               | Like with the Java sin() fixes: if you don't care about
               | the results being correct why not constant-fold an
               | arbitrary number? Way faster at run-time.
        
               | duped wrote:
               | All numerical methods define "correct" to be within a
               | range or to some precision. There are very few algorithms
               | that require FTZ mode to be "correct" - the linked
               | article and the article _it_ links don 't even have an
               | example (there are good examples of where say, -ffinite-
               | math is super dangerous, because inf/NaNs are way more
               | common than arithmetic on subnormal numbers).
               | 
               | And yea, the fact that crt1.o being linked into shared
               | libraries fucking up the precision of some computations
               | depending on library dependencies (and the order they're
               | loaded!) was bad.. but it lingered in the entire Linux
               | ecosystem for over a decade. So how bad was it, if it
               | took that long to notice?
               | 
               | If you have a numerical algorithm that requires subnormal
               | arithmetic to converge, a) don't that's super shaky, b)
               | set/unset mxcsr at the top/bottom of your function and
               | ensure you never unwind the stack without resetting it.
               | It's preserved across context switches so you're not
               | going to get blown away by the OS scheduler.
               | 
               | This isn't practical numerical methods in C 101 but it's
               | at least 201. In practice you don't trust floats for bit
               | exact math. Use different types for that.
        
               | dapperdrake wrote:
               | IEEE 754 defaults are for people who _don 't_ get deeply
               | into numerical analysis and Cauchy sequences. Like,
               | ostensibly, most FOSS maintainers. Or most people who
               | write software in general.
               | 
               | There are people that do. HPC and the demoscene have
               | numerous examples. Most of the people I met here are
               | capable of reading gcc's manual and picking the
               | optimizations they actually need. And they know how to
               | debug this stuff.
               | 
               | If it's not obvious who gcc's defaults should cater to,
               | then redefine human-friendly until it becomes obvious.
        
               | usefulcat wrote:
               | A big problem with -ffast-math is that it causes isnan
               | and isinf to be completely, silently broken (gcc and
               | clang).
               | 
               | Like, "oh you want faster FP operations? well then surely
               | you have no need to ever be able to detect infinite or
               | NaN values.."
        
               | a_e_k wrote:
               | I find that building and testing my code with -Ofast and
               | -ffast-math from the beginning helps to avoid a lot of
               | the issues with them. Any new code that breaks with them
               | on probably wasn't particularly stable anyway and should
               | be rethought.
        
         | rollcat wrote:
         | It's because the UB must be continuously exploited by compilers
         | for that extra 1% perf gain.
         | 
         | I've been eyeing Zig recently. It makes a lot of choices
         | straightforward yet explicit, e.g. you choose between four
         | optimisation strategies: debug, safety, size, perf. Individual
         | programs/libraries can have a default or force one (for the
         | whole program or a compilation unit), but it's customary to
         | delegate that choice to the person actually building from
         | source.
         | 
         | Even simpler story with Go. It's been designed by people who
         | favour correctness over performance, and most compiler flags
         | (like -race, -asan, -clobberdead) exist to help debug problems.
         | 
         | I've been observing a lot of people complain about declining
         | software quality; yearly update treadmills delivering unwanted
         | features and creating two bugs for each one fixed. Simplicity
         | and correctness still seem to be a niche thing; I salute
         | everyone who actually cares.
        
           | nayuki wrote:
           | > It's because the UB must be continuously exploited by
           | compilers for that extra 1% perf gain.
           | 
           | Your framing of a compiler exploiting UB in programs to gain
           | performance, has an undeserved negative connotation. The fact
           | is, programs are mathematical structures/arguments, and if
           | any single step in the program code or execution is wrong, no
           | matter how small, it can render the whole program invalid.
           | Drawing from math analogies where one wrong step leads to an
           | absurd conclusion:
           | 
           | * https://en.wikipedia.org/wiki/All_horses_are_the_same_color
           | 
           | * https://en.wikipedia.org/wiki/Principle_of_explosion
           | 
           | * https://proofwiki.org/wiki/False_Statement_implies_Every_St
           | a...
           | 
           | * https://en.wikipedia.org/wiki/Mathematical_fallacy#Division
           | _...
           | 
           | Back to programming, hopefully this example will not be
           | controversial: If a program contains at least one write to an
           | arbitrary address (e.g. `*(char*)0x123 = 0x456;`), the
           | overall behavior will be unpredictable and effectively
           | meaningless. In this case, I would fully agree with a
           | compiler deleting, reordering, and manipulating code as a
           | result of that particular UB.
           | 
           | You could argue that C shouldn't have been designed so that
           | reading out of bounds is UB. Instead, it should read some
           | arbitrary value without crashing or cleanly segfault at that
           | instruction, with absolutely no effects on any surrounding
           | code.
           | 
           | You could argue that C/C++ shouldn't have made it UB to
           | dereference a null pointer for reading, but I fully agree
           | that dereferencing a null pointer for a method call or
           | writing a field must be UB.
           | 
           | Another analogy in programming is, let's forget about UB.
           | Let's say you're writing a hash table in Java (in the normal
           | safe subset without using JNI or Unsafe). If you get even one
           | statement wrong in the data structure implementation, there
           | still might be arbitrarily large consequences like dropping
           | values when you shouldn't, miscounting how many values exist,
           | duplicating values when you shouldn't, having an incorrect
           | state that causes subtle failures far in the future, etc. The
           | consequences are not as severe and pervasive as UB at the
           | language level, but it will still result in corrupt data
           | and/or unpredictable behavior for the user of that library
           | code, which can in turn have arbitrarily large consequences.
           | I guess the only difference compared to C/C++ UB is that for
           | C/C++, there is more "spooky action at a distance", where
           | some piece of UB can have very non-local consequences. But
           | even incorrect code in safe Java can produce large
           | consequences, maybe just not as large on average.
           | 
           | I am not against compilers "exploiting" UB for performance
           | gain. But these are the ways forward that I believe in, for
           | any programming language in general:
           | 
           | * In the language specification, reduce the number of
           | cases/places that are undefined. Not only does it reduce the
           | chances of bad things happening, but it also makes the rules
           | easier to remember for humans, thus making it easier to avoid
           | triggering these cases.
           | 
           | * Adding to that point, favor compile-time errors over run-
           | time UB. For example, reading from an uninitialized local
           | variable is a compile error in Java but UB in C. Rust's whole
           | shtick about lifetimes and borrowing is one huge
           | transformation of run-time problems into compile-time
           | problems.
           | 
           | * Overwhelmingly favor safety by default. For example, array
           | accesses should be bounds-checked using the convenient
           | operator like `array[index]`, whereas the unsafe unchecked
           | version should be something obnoxious and ugly like `unsafe {
           | array.get_unchecked(index) }`. Make the safe way easy and
           | make the unsafe way hard - the exact opposite of C/C++.
           | 
           | * Provide good (and preferably complete) sanitizer tools to
           | check that UB isn't triggered at run time. C/C++ did not have
           | these for the first few decades of their lives, and you were
           | flying blind when triggering UB.
        
       | fuhsnn wrote:
       | If you are taking notes, add `-fzero-init-padding-bits=all` to
       | the list, without this flag, GCC 15 onwards will not zero-
       | initialize a full union if you wrote pre-C23 style ={0} and the
       | largest member is not the first one. `-ftrivial-auto-var-init`
       | cannot help this case. https://godbolt.org/z/7zKccfnea
        
         | naitgacem wrote:
         | I have been always stuck with C99, what is the "post" C23 way
         | that will zero initialize a full union?
         | 
         | Or am I misunderstanding this?
        
           | dzaima wrote:
           | `={}` in place of `={0}` is the new option in C23.
        
       | stabbles wrote:
       | > The keyword $ORIGIN in rpath is expanded by the dynamic loader
       | to the path of the directory where the object is found, which may
       | be set by an attacker (e.g., via hard links) to a directory with
       | a malicious dependency. On Linux, the fs.protected_hardlinks
       | sysctl can help prevent this attack.
       | 
       | This has nothing to do with hardlinks, the same applies to
       | symlinks. On linux the status quo is that the dynamic loader
       | finds the library by symlink, the convention is `libfoo.so.x ->
       | libfoo.so.a.b.c` where `x` is the ABI version and `a.b.c` the
       | full version.
       | 
       | But if `libfoo.so.x -> /absolute/path/libfoo.so.a.b.c` and it has
       | `$ORIGIN/libbar.so.y` in DT_NEEDED, those are resolved relative
       | to the dir of the symlink, not to realpath of the symlink.
       | 
       | That makes sense, cause it would be a lot of startup overhead to
       | lstat every path component of every library that uses $ORIGIN.
       | 
       | I don't see the point of including this gotcha in a security
       | overview to be honest.
        
       | grandempire wrote:
       | > Our threat model is that all software developers make mistakes,
       | and sometimes those mistakes lead to vulnerabilities
       | 
       | That's not a threat model. What are the attackers going to do if
       | there are vulnerabilities in your executable? Is it connected to
       | a web server?
       | 
       | Does it have access to privileged resources?
        
         | steveklabnik wrote:
         | They're using it in the sense of "the scope of this document
         | covers this scenario," so the answer to all of your questions
         | are out of scope.
        
       | javier_e06 wrote:
       | Last week a build broke because there was space after the Wl,
       | some-linker-option The Warning messages can't be very challenging
       | to decipher.
       | 
       | Most importantly: Are the warnings show-stoppers? Not in part of
       | my pay grade.
       | 
       | There is a pragma to ignore specific warnings. This is "#pragma
       | GCC diagnostic ignore "some-compiler-warning" which is useful
       | when dealing with several versions of the GCC compiler.
       | 
       | Yes, it happens.
        
         | ryandrake wrote:
         | > Most importantly: Are the warnings show-stoppers? Not in part
         | of my pay grade.
         | 
         | The best places (code quality wise) I've ever worked were the
         | strictest on compiler warnings. Turn on all warnings, turn on
         | extra warnings, treat warnings as errors, and forbid disabling
         | warnings via #pragma. The absolute worst was the one where
         | compiling the software _using the compiler 's default warning
         | level_ produced a deluge of 40,000 warnings, and the culture
         | was to disable warnings when they became annoying (vs. you
         | know, fixing them).
         | 
         | My philosophy: Compilers don't issue warnings for fun. Every
         | one of them is a potential problem and they are almost always
         | worth fixing.
         | 
         | I also adhere to this in my personal hobby projects, too. It
         | can be challenging when integrating with third party libraries,
         | where the library maintainer doesn't care as much. I once
         | submitted a patch to an open source project I won't name here,
         | which fixed a bunch of warnings that seem to be only present in
         | macOS builds (XCode's defaults tend to be quite strict). The
         | response was not to merge it because "I don't regularly do
         | macOS builds, and besides, they're just warnings." Alright,
         | bro, sorry I tried to help.
        
       | mid-kid wrote:
       | While all of these are very useful, you'll find that a lot of
       | these are already enabled by default in many distributions of the
       | gcc compiler. Sometimes they're embedded in the compiler itself
       | through a patch or configure flag, and sometimes they're added
       | through CFLAGS variables during the compilation of distribution
       | packages. I can only really speak of gentoo, but here's a non-
       | exhaustive list:
       | 
       | * -fPIE is enabled with --enable-default-pie in GCC's ./configure
       | script
       | 
       | * -fstack-protector-strong is enabled with --enable-default-ssp
       | in GCC's ./configure script
       | 
       | * -Wl,-z,relro is enabled with --enable-relro in Binutils'
       | ./configure script
       | 
       | * -Wp,-D_FORTIFY_SOURCE=2, -fstack-clash-protection, -Wl,-z,now
       | and -fcf-protection=full are enabled by default through patches
       | to GCC in Gentoo.
       | 
       | * -Wl,--as-needed is enabled through the default LDFLAGS
       | 
       | For reference, here's the default compiler flags for a few other
       | distributions. Note that these don't include GCC patches:
       | 
       | * Arch Linux:
       | https://gitlab.archlinux.org/archlinux/packaging/packages/pa...
       | 
       | * Alpine Linux:
       | https://gitlab.alpinelinux.org/alpine/abuild/-/blob/master/d...
       | 
       | * Debian: It's a tiny bit more obscure, but running `dpkg-
       | buildflags` on a fresh container returns the following: CFLAGS=-g
       | -O2 -Werror=implicit-function-declaration -ffile-prefix-
       | map=/home/<myuser>=. -fstack-protector-strong -fstack-clash-
       | protection -Wformat -Werror=format-security -fcf-protection
        
         | jpfr wrote:
         | Most of these are implicit with -fhardened.
         | 
         | https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.h...
        
           | dapperdrake wrote:
           | Finally.
        
       | klysm wrote:
       | It would be really nice if we had a versioning scheme that
       | enabled developers to get secure by default and opt into
       | performance tradeoffs
        
       ___________________________________________________________________
       (page generated 2025-03-31 23:01 UTC)