[RFC] Hardening in libc++ (original) (raw)

We would like to improve security in libc++ by providing optional hardening modes that, when enabled, turn certain cases of undefined behavior into guaranteed program termination (in other words, turn undefined behavior into implementation-defined behavior). See the C++ Buffer Hardening RFC for more context about the overarching effort.

(Note: this RFC presents our vision for hardening in libc++. While certain parts of the RFC have been implemented in the main branch, many aren’t, and we don’t yet officially support hardening in the LLVM 17 release or on main )

Different modes provide different trade-offs between security and performance. Our current design has four modes; sorted in the order of increased security, they are:

(If you’re familiar with the safe mode that was added to libc++ in the LLVM 15 release, hardening modes can be seen as an extension of that work)

Each mode on the list is a superset of the previous one (but this might not be true of any new modes potentially added in the future). Of these, hardened and debug-lite modes are intended to be usable in production; for the debug mode, being usable in production is a non-goal (it is intended for testing). The hardened mode aims to be minimalistic and heavily prioritizes performance; we intend to set the bar high for any check to be enabled in the hardened mode, only enabling those checks that prevent memory safety bugs. The debug-lite mode additionally aims to catch common programming errors that aren’t directly exploitable; here, the criteria for a check to be enabled is roughly that the performance overhead is relatively low and the error is caused by user input (purely internal assertions are not enabled in the debug-lite mode). Different projects would make different trade-offs here, which is why we aim to provide two different modes.

(A note on terminology: a hardening mode is a name for any of the modes described above; e.g. the unchecked mode is one of the hardening modes. On the other hand, the hardened mode is the specific mode that’s in-between the unchecked mode and the debug-lite mode)

Categories

(Note: we plan to audit which assertions fall into which categories with our internal security team)

Internally, all the checks in libc++ are going to be put into several broad categories . We haven’t finalized the design of those categories yet, but by and large, the categories are defined by the kind of error they prevent. Hardening modes differ between each other by which categories of checks they enable. The unchecked mode disables all checks, and conversely the debug mode enables all checks.

The current categories are:

Additionally, the debug mode randomizes the output of certain algorithms (within the range of possible valid outputs) to prevent users from accidentally relying on unspecified behavior.

To quickly illustrate how the hardening modes relate to each other, here is a table:

Essentially any bug could, under certain circumstances, indirectly lead to an exploitable security vulnerability (by playing a role in vulnerability chaining). The hardened mode aims to prevent only bugs which directly compromise the memory safety of the program. An out-of-bounds write satisfies the criteria, but a null pointer dereference usually does not, as most platforms deterministically stop programs that try to dereference a null pointer at runtime.

Termination

When an unsuccessful check is triggered, the program is terminated via a call to __builtin_trap ; the intent is to turn undefined behavior into a guaranteed program termination and make it terminate as fast as possible (faster than a call to std::abort , which has other security problems as well). In the future, we will explore ways to provide an additional error message and potentially to allow the behavior to be customized.

Note that we will not be using the existing __libcpp_verbose_abort mechanism because its semantics are essentially to call std::abort . __libcpp_verbose_abort will still be supported and used for cases where we terminate for reasons other than encountering undefined behavior (e.g. when an exception is thrown under -fno-exceptions , and in the future from libc++abi when various runtime operations fail).

ABI considerations

Some checks require storing additional information in standard library classes — for example, to be able to check whether an iterator dereference is valid, the iterator object needs to somehow store a reference to the corresponding container. This requires an ABI break.

In the proposed design, breaking the ABI is orthogonal to setting a hardening mode. The rationale for this design stems from the observation that the ABI configuration is a property of the platform and is set by the vendor whereas the hardening mode is a property of an application and is set by the user (even though vendors can set the default hardening mode). The ABI is a property of the platform because in general every component built on the platform has to be ABI-compatible. If we were to provide e.g. a “hardened-abi-breaking” mode, it would give users an easy way to unintentionally build their application with an ABI that’s incompatible with the rest of the platform, which in almost all circumstances should be avoided. Moreover, since there will be several independent ABI-breaking settings, this would either create a combinatorial explosion of ABI modes or disallow mixing-and-matching different ABI settings (for example, it might make sense to enable bounded iterators for constant-sized containers such as std::array but not for variable-sized containers such as std::vector , but that would be impossible if the only available modes were “hardened-abi-stable” and “hardened-abi-breaking”).

ABI-breaking changes, such as enabling container-aware iterators, are controlled by a separate set of macros that are grouped together with other ABI macros (which are unrelated to hardening). Enabling a hardening mode doesn’t affect the ABI; rather, the hardening mode will enable whichever checks are possible within the current ABI configuration. For example, enabling the hardened mode will always enable the “valid-element-access” checks in std::span::operator[] (because those don’t depend on the ABI configuration), but will only enable “valid-element-access” checks in std::span::iterator::operator* if container-aware iterators for std::span are enabled in the ABI configuration (in this case, the relevant macro is _LIBCPP_ABI_BOUNDED_ITERATORS ).

Enabling hardening

At the platform level, vendors can control the default hardening mode via a CMake variable. At the application level, the hardening mode can be overridden by users via either a compiler flag or a macro.

The exact numeric values of these macros are unspecified and deliberately not ordered to prevent users from relying on implementation details.

-flibc++-hardening and _LIBCPP_HARDENING_MODE are mutually exclusive: when compiling with -flibc++-hardening , attempting to define _LIBCPP_HARDENING_MODE will result in an error.

GCC compatibility

The _LIBCPP_HARDENING_MODE macro allows enabling hardening in libc++ when compiling with the GCC compiler where the proposed -flibc++-hardening Clang flag will not be available.

Additionally, GCC has recently introduced the -fhardened flag that enables hardening in libstdc++. We plan to explore making libc++ honor that flag when compiling under GCC (it will likely enable the hardened mode) as well as adding the -fhardened flag to Clang. While the exact semantics of the -fhardened flag will necessarily differ between libc++ and libstdc++, we believe that having some broad compatibility will still be beneficial.

Configuring hardening on a per-TU basis

The hardening mode can be overridden on a per-TU basis by compiling the TU with the -flibc++-hardening flag or the _LIBCPP_HARDENING_MODE macro defined to a different value from the rest of the application. This would allow, for example, disabling checks for performance-critical parts of the code.

Note that the ability to select the hardening mode on a per-TU basis has ODR implications. However, we can use ABI tags to ensure that inline functions have a different mangling based on the hardening mode, thus avoiding ODR violations. This mechanism only covers functions defined inline — the functions compiled inside the dylib will still use the hardening mode that the library was configured with by the vendor, and the value of _LIBCPP_HARDENING_MODE set by the user won’t be respected. However, the vast majority of functions in the standard library are defined inline, so that should not be seen as a significant limitation.

Rollout

We aim to first make hardening modes available in the LLVM 18 release, with no breaking changes. LLVM 19 and 20 will contain breaking changes. Proposed timeline:

Future work

FAQ