[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)