Hardening Modes

Using hardening modes

libc++ provides several hardening modes, where each mode enables a set of assertions that prevent undefined behavior caused by violating preconditions of the standard library. Different hardening modes make different trade-offs between the amount of checking and runtime performance. The available hardening modes are:

  • Unchecked mode/none, which disables all hardening checks.

  • Fast mode, which contains a set of security-critical checks that can be done with relatively little overhead in constant time and are intended to be used in production. We recommend most projects adopt this.

  • Extensive mode, which contains all the checks from fast mode and some additional checks for undefined behavior that incur relatively little overhead but aren’t security-critical. Production builds requiring a broader set of checks than fast mode should consider enabling extensive mode. The additional rigour impacts performance more than fast mode: we recommend benchmarking to determine if that is acceptable for your program.

  • Debug mode, which enables all the available checks in the library, including heuristic checks that might have significant performance overhead as well as internal library assertions. This mode should be used in non-production environments (such as test suites, CI, or local development). We don’t commit to a particular level of performance in this mode and it’s not intended to be used in production.

Note

Enabling hardening has no impact on the ABI.

Notes for users

As a libc++ user, consult with your vendor to determine the level of hardening enabled by default.

Users wishing for a different hardening level to their vendor default are able to control the level by passing one of the following options to the compiler:

  • -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE

  • -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST

  • -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE

  • -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG

Warning

The exact numeric values of these macros are unspecified and users should not rely on them (e.g. expect the values to be sorted in any way).

Warning

If you would prefer to override the hardening level on a per-translation-unit basis, you must do so before including any headers to avoid ODR issues.

Note

Since the static and shared library components of libc++ are built by the vendor, setting this macro will have no impact on the hardening mode for the pre-built components. Most libc++ code is header-based, so a user-provided value for _LIBCPP_HARDENING_MODE will be mostly respected.

Notes for vendors

Vendors can set the default hardening mode by providing LIBCXX_HARDENING_MODE as a configuration option, with the possible values of none, fast, extensive and debug. The default value is none which doesn’t enable any hardening checks (this mode is sometimes called the unchecked mode).

This option controls both the hardening mode that the precompiled library is built with and the default hardening mode that users will build with. If set to none, the precompiled library will not contain any assertions, and user code will default to building without assertions.

Vendors can also override the way the program is terminated when an assertion fails by providing a custom header.

Assertion categories

Inside the library, individual assertions are grouped into different categories. Each hardening mode enables a different set of assertion categories; categories provide an additional layer of abstraction that makes it easier to reason about the high-level semantics of a hardening mode.

Note

Users are not intended to interact with these categories directly – the categories are considered internal to the library and subject to change.

  • valid-element-access – checks that any attempts to access a container element, whether through the container object or through an iterator, are valid and do not attempt to go out of bounds or otherwise access a non-existent element. This also includes operations that set up an imminent invalid access (e.g. incrementing an end iterator). For iterator checks to work, bounded iterators must be enabled in the ABI. Types like std::optional and std::function are considered containers (with at most one element) for the purposes of this check.

  • valid-input-range – checks that ranges (whether expressed as an iterator pair, an iterator and a sentinel, an iterator and a count, or a std::range) given as input to library functions are valid: - the sentinel is reachable from the begin iterator; - TODO(hardening): both iterators refer to the same container.

    (“input” here refers to “an input given to an algorithm”, not to an iterator category)

    Violating assertions in this category leads to an out-of-bounds access.

  • non-null – checks that the pointer being dereferenced is not null. On most modern platforms, the zero address does not refer to an actual location in memory, so a null pointer dereference would not compromise the memory security of a program (however, it is still undefined behavior that can result in strange errors due to compiler optimizations).

  • non-overlapping-ranges – for functions that take several ranges as arguments, checks that those ranges do not overlap.

  • valid-deallocation – checks that an attempt to deallocate memory is valid (e.g. the given object was allocated by the given allocator). Violating this category typically results in a memory leak.

  • valid-external-api-call – checks that a call to an external API doesn’t fail in an unexpected manner. This includes triggering documented cases of undefined behavior in an external library (like attempting to unlock an unlocked mutex in pthreads). Any API external to the library falls under this category (from system calls to compiler intrinsics). We generally don’t expect these failures to compromise memory safety or otherwise create an immediate security issue.

  • compatible-allocator – checks any operations that exchange nodes between containers to make sure the containers have compatible allocators.

  • argument-within-domain – checks that the given argument is within the domain of valid arguments for the function. Violating this typically produces an incorrect result (e.g. std::clamp returns the original value without clamping it due to incorrect functors) or puts an object into an invalid state (e.g. a string view where only a subset of elements is accessible). This category is for assertions violating which doesn’t cause any immediate issues in the library – whatever the consequences are, they will happen in the user code.

  • pedantic – checks preconditions that are imposed by the Standard, but violating which happens to be benign in libc++.

  • semantic-requirement – checks that the given argument satisfies the semantic requirements imposed by the Standard. Typically, there is no simple way to completely prove that a semantic requirement is satisfied; thus, this would often be a heuristic check and it might be quite expensive.

  • internal – checks that internal invariants of the library hold. These assertions don’t depend on user input.

  • uncategorized – for assertions that haven’t been properly classified yet. This category is an escape hatch used for some existing assertions in the library; all new code should have its assertions properly classified.

Mapping between the hardening modes and the assertion categories

Category name

fast

extensive

debug

valid-element-access

valid-input-range

non-null

non-overlapping-ranges

valid-deallocation

valid-external-api-call

compatible-allocator

argument-within-domain

pedantic

semantic-requirement

internal

uncategorized

Note

At the moment, each subsequent hardening mode is a strict superset of the previous one (in other words, each subsequent mode only enables additional assertion categories without disabling any), but this won’t necessarily be true for any hardening modes that might be added in the future.

Note

The categories enabled by each mode are subject to change and users should not rely on the precise assertions enabled by a mode at a given point in time. However, the library does guarantee to keep the hardening modes stable and to fulfill the semantics documented here.

Hardening assertion failure

In production modes (fast and extensive), a hardening assertion failure immediately _traps <https://llvm.org/docs/LangRef.html#llvm-trap-intrinsic> the program. This is the safest approach that also minimizes the code size penalty as the failure handler maps to a single instruction. The downside is that the failure provides no additional details other than the stack trace (which might also be affected by optimizations).

TODO(hardening): describe __builtin_verbose_trap once we can use it.

In the debug mode, an assertion failure terminates the program in an unspecified manner and also outputs the associated error message to the error output. This is less secure and increases the size of the binary (among other things, it has to store the error message strings) but makes the failure easier to debug. It also allows testing the error messages in our test suite.

Overriding the assertion failure handler

Vendors can override the default assertion handler mechanism by following these steps:

  • create a header file that provides a definition of a macro called _LIBCPP_ASSERTION_HANDLER. The macro will be invoked when a hardening assertion fails, with a single parameter containing a null-terminated string with the error message.

  • when configuring the library, provide the path to custom header (relative to the root of the repository) via the CMake variable LIBCXX_ASSERTION_HANDLER_FILE.

Note that almost all libc++ headers include the assertion handler header which means it should not include anything non-trivial from the standard library to avoid creating circular dependencies.

There is no existing mechanism for users to override the assertion handler because the ability to do the override other than at configure-time carries an unavoidable code size penalty that would otherwise be imposed on all users, whether they require such customization or not. Instead, we let vendors decide what’s right on their platform for their users – a vendor who wishes to provide this capability is free to do so, e.g. by declaring the assertion handler as an overridable function.

ABI

Setting a hardening mode does not affect the ABI. Each mode uses the subset of checks available in the current ABI configuration which is determined by the platform.

It is important to stress that whether a particular check is enabled depends on the combination of the selected hardening mode and the hardening-related ABI options. Some checks require changing the ABI from the “default” to store additional information in the library classes – e.g. checking whether an iterator is valid upon dereference generally requires storing data about bounds inside the iterator object. Using std::span as an example, setting the hardening mode to fast will always enable the valid-element-access checks when accessing elements via a std::span object, but whether dereferencing a std::span iterator does the equivalent check depends on the ABI configuration.

ABI options

Vendors can use the following ABI options to enable additional hardening checks:

  • _LIBCPP_ABI_BOUNDED_ITERATORS – changes the iterator type of select containers (see below) to a bounded iterator that keeps track of whether it’s within the bounds of the original container and asserts valid bounds on every dereference.

    ABI impact: changes the iterator type of the relevant containers.

    Supported containers:

    • span;

    • string_view.

  • _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING – changes the iterator type of basic_string to a bounded iterator that keeps track of whether it’s within the bounds of the original container and asserts it on every dereference and when performing iterator arithmetics.

    ABI impact: changes the iterator type of basic_string and its specializations, such as string and wstring.

  • _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR – changes the iterator type of vector to a bounded iterator that keeps track of whether it’s within the bounds of the original container and asserts it on every dereference and when performing iterator arithmetics. Note: this doesn’t yet affect vector<bool>.

    ABI impact: changes the iterator type of vector (except vector<bool>).

ABI tags

We use ABI tags to allow translation units built with different hardening modes to interact with each other without causing ODR violations. Knowing how hardening modes are encoded into the ABI tags might be useful to examine a binary and determine whether it was built with hardening enabled.

Warning

We don’t commit to the encoding scheme used by the ABI tags being stable between different releases of libc++. The tags themselves are never stable, by design – new releases increase the version number. The following describes the state of the latest release and is for informational purposes only.

The first character of an ABI tag encodes the hardening mode:

  • f – [f]ast mode;

  • s – extensive (“[s]afe”) mode;

  • d – [d]ebug mode;

  • n – [n]one mode.

Hardened containers status

Name

Member functions

Iterators (ABI-dependent)

span

string_view

array

vector

✅ (see note)

string

✅ (see note)

list

forward_list

deque

map

set

multimap

multiset

unordered_map

Partial

Partial

unordered_set

Partial

Partial

unordered_multimap

Partial

Partial

unordered_multiset

Partial

Partial

mdspan

optional

N/A

function

N/A

variant

N/A

N/A

any

N/A

N/A

expected

N/A

valarray

Partial

N/A

bitset

N/A

Note: for vector and string, the iterator does not check for invalidation (accesses made via an invalidated iterator still lead to undefined behavior)

Note: vector<bool> iterator is not currently hardened.

Testing

Please see Testing documentation.

Further reading