Skip to content

[libc++][hardening] Introduce assertion semantics #148268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/libcxx-build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ jobs:
'generic-abi-unstable',
'generic-hardening-mode-debug',
'generic-hardening-mode-extensive',
'generic-hardening-mode-extensive-observe-semantic',
'generic-hardening-mode-fast',
'generic-hardening-mode-fast-with-abi-breaks',
'generic-merged',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set(LIBCXX_HARDENING_MODE "extensive" CACHE STRING "")
set(LIBCXX_TEST_PARAMS "assertion_semantic=observe" CACHE STRING "")
61 changes: 61 additions & 0 deletions libcxx/docs/Hardening.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ modes are:

Enabling hardening has no impact on the ABI.

.. _notes-for-users:

Notes for users
---------------

Expand Down Expand Up @@ -72,6 +74,11 @@ to control the level by passing **one** of the following options to the compiler
pre-built components. Most libc++ code is header-based, so a user-provided
value for ``_LIBCPP_HARDENING_MODE`` will be mostly respected.

In some cases, users might want to override the assertion semantic used by the
library.
This can be done similarly to setting the hardening mode; please refer to the
:ref:`relevant section <assertion-semantics>`.

Notes for vendors
-----------------

Expand Down Expand Up @@ -260,6 +267,60 @@ 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.

This default behavior can be customized by users via :ref:`assertion semantics
<assertion-semantics>`; it can also be completely overridden by vendors by
providing a :ref:`custom assertion failure handler
<override-assertion-handler>`.

.. _assertion-semantics:

Assertion semantics
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comment: we need to mention (or at least plan for) what happens when C++26 Contracts are implemented. In particular, there will be a time when Clang and libc++ will implement Contracts and the Contracts runtime (class std::contract_violation, etc). Furthermore, we'll want libc++ to be a valid C++26 Hardened Implementation, which requires that hardening failures be treated as contract violations.

When that's the case, what should happen with _LIBCPP_ASSERTION_SEMANTIC?

Copy link
Member Author

@var-const var-const Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our design is constrained by the fact that we want to support hardening in older language modes where Contracts would not be available even when using a new compiler (after all, a major motivation for hardening is making existing code bases more secure). Support for the observe semantic is important for adopting hardening in production, so I think it would be a significant limitation if this semantic were only available for projects that can use very recent compiler versions. It makes having some form of assertion semantics unavoidable.

I also think it makes sense to have assertion semantics mimic the actual Contracts semantics -- it's simple(r) conceptually and might make migrating to Contracts easier. We could have e.g. a boolean flag ("observe"/"not observe") instead but I think it would be creating unnecessary differences without buying us much.

If this is the case, then we cannot escape the situation where libc++ assertion semantics and actual Contracts evaluation semantics "clash" and need to choose the best (in the sense of "lesser of several evils") alternative. Summarizing our offline discussion, I think when a user sets the assertion semantic in an environment where Contract semantics are available, we can:

  • A. Make this an error.
  • B. Allow the Contracts semantic to silently take precedence (effectively override) the assertion semantic.
  • C. Do either A or B based on whether the assertion semantic matches the Contract semantic (if they match, setting the assertion semantic is a no-op, if they don't match, produce an error). While it's not impossible for a project to use different semantics based on the language mode, it seems very unlikely in practice.

(C) seems like the better option -- (A) would make it painful for projects to support more than one language mode, and (B) might allow silently downgrading the semantic to a non-terminating one, for example. IIUC, the evaluation semantic can be chosen at runtime, making it hard or impossible to catch mismatch, but these cases should be relatively rare, and I think (C) should generally cover this.

This also means that we would want to support assertion semantics long term, having support for Contracts doesn't automatically deprecate them.

-------------------

What happens when an assertion fails depends on the assertion semantic being
used. Four assertion semantics are available, based on C++26 Contracts
evaluation semantics:

- ``ignore`` evaluates the assertion but has no effect if it fails (note that it
differs from the Contracts ``ignore`` semantic which would not evaluate
the assertion at all);
- ``observe`` logs an error (indicating, if possible on the platform, that the
error is fatal) but continues execution;
- ``quick-enforce`` terminates the program as fast as possible via a trap
instruction. It is the default semantic for the production modes (``fast`` and
``extensive``);
- ``enforce`` logs an error and then terminates the program. It is the default
semantic for the ``debug`` mode.

Notes:

- Continuing execution after a hardening check fails results in undefined
behavior; the ``observe`` semantic is meant to make adopting hardening easier
but should not be used outside of the adoption period;
- C++26 wording for Library Hardening precludes a conforming Hardened
implementation from using the Contracts ``ignore`` semantic when evaluating
hardened preconditions in the Library. Libc++ allows using this semantic for
hardened preconditions, but please be aware that using ``ignore`` does not
produce a conforming "Hardened" implementation, unlike the other semantics
above.

The default assertion semantics are as follows:

- ``fast``: ``quick-enforce``;
- ``extensive``: ``quick-enforce``;
- ``debug``: ``enforce``.

The default assertion semantics can be overridden by passing **one** of the
following options to the compiler:

- ``-D_LIBCPP_ASSERTION_SEMANTIC=_LIBCPP_ASSERTION_SEMANTIC_IGNORE``
- ``-D_LIBCPP_ASSERTION_SEMANTIC=_LIBCPP_ASSERTION_SEMANTIC_OBSERVE``
- ``-D_LIBCPP_ASSERTION_SEMANTIC=_LIBCPP_ASSERTION_SEMANTIC_QUICK_ENFORCE``
- ``-D_LIBCPP_ASSERTION_SEMANTIC=_LIBCPP_ASSERTION_SEMANTIC_ENFORCE``

All the :ref:`same notes <notes-for-users>` apply to setting this macro as for
setting ``_LIBCPP_HARDENING_MODE``.

.. _override-assertion-handler:

Overriding the assertion failure handler
Expand Down
5 changes: 5 additions & 0 deletions libcxx/docs/ReleaseNotes/21.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ Improvements and New Features

- ``ctype::tolower`` and ``ctype::toupper`` have been optimized, resulting in a 2x performance improvement.

- Hardening now supports assertion semantics that allow customizing how a hardening assertion failure is handled. The
four available semantics, modeled on C++26 Contracts, are ``ignore``, ``observe``, ``quick-enforce`` and ``enforce``.
The ``observe`` semantic is intended to make it easier to adopt Hardening in production but should not be used outside
of this scenario. Please refer to the :ref:`Hardening documentation <hardening>` for details.

Deprecations and Removals
-------------------------

Expand Down
34 changes: 34 additions & 0 deletions libcxx/include/__config
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,40 @@ _LIBCPP_HARDENING_MODE_EXTENSIVE, \
_LIBCPP_HARDENING_MODE_DEBUG
# endif

// Hardening assertion semantics generally mirror the evaluation semantics of C++26 Contracts:
// - `ignore` evaluates the assertion but doesn't do anything if it fails (note that it differs from the Contracts
// `ignore` semantic which wouldn't evaluate the assertion at all);
// - `observe` logs an error (indicating, if possible, that the error is fatal) and continues execution;
// - `quick-enforce` terminates the program as fast as possible (via trapping);
// - `enforce` logs an error and then terminates the program.
//
// Notes:
// - Continuing execution after a hardening check fails results in undefined behavior; the `observe` semantic is meant
// to make adopting hardening easier but should not be used outside of this scenario;
// - C++26 wording for Library Hardening precludes a conforming Hardened implementation from using the Contracts
// `ignore` semantic when evaluating hardened preconditions in the Library. Libc++ allows using this semantic for
// hardened preconditions, however, be aware that using `ignore` does not produce a conforming "Hardened"
// implementation, unlike the other semantics above.
// clang-format off
# define _LIBCPP_ASSERTION_SEMANTIC_IGNORE (1 << 1)
# define _LIBCPP_ASSERTION_SEMANTIC_OBSERVE (1 << 2)
# define _LIBCPP_ASSERTION_SEMANTIC_QUICK_ENFORCE (1 << 3)
# define _LIBCPP_ASSERTION_SEMANTIC_ENFORCE (1 << 4)
// clang-format on

// Allow users to define an arbitrary assertion semantic; otherwise, use the default mapping from modes to semantics.
// The default is for production-capable modes to use `quick-enforce` (i.e., trap) and for the `debug` mode to use
// `enforce` (i.e., log and abort).
# ifndef _LIBCPP_ASSERTION_SEMANTIC

# if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
# define _LIBCPP_ASSERTION_SEMANTIC _LIBCPP_ASSERTION_SEMANTIC_ENFORCE
# else
# define _LIBCPP_ASSERTION_SEMANTIC _LIBCPP_ASSERTION_SEMANTIC_QUICK_ENFORCE
# endif

# endif // _LIBCPP_ASSERTION_SEMANTIC

// } HARDENING

# define _LIBCPP_TOSTRING2(x) #x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ int main(int, char**) {
}
// mismatch of static extent
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<int, D, 5> e1(std::array{1000, 3}); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<int, D, 5> e1(std::array{1000, 3}); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// value out of range
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<signed char, D, 5> e1(std::array{1000, 5}); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<signed char, D, 5> e1(std::array{1000, 5}); }()),
"extents ctor: arguments must be representable as index_type and nonnegative");
}
// negative value
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<signed char, D, 5> e1(std::array{-1, 5}); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<signed char, D, 5> e1(std::array{-1, 5}); }()),
"extents ctor: arguments must be representable as index_type and nonnegative");
}
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ int main(int, char**) {
}
// mismatch of static extent
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<int, D, 5> e1(1000, 3); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<int, D, 5> e1(1000, 3); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// value out of range
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<signed char, D, 5> e1(1000, 5); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<signed char, D, 5> e1(1000, 5); }()),
"extents ctor: arguments must be representable as index_type and nonnegative");
}
// negative value
{
TEST_LIBCPP_ASSERT_FAILURE(([] { std::extents<signed char, D, 5> e1(-1, 5); }()),
TEST_LIBCPP_ASSERT_FAILURE(([] { [[maybe_unused]] std::extents<signed char, D, 5> e1(-1, 5); }()),
"extents ctor: arguments must be representable as index_type and nonnegative");
}
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ int main(int, char**) {
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_left::mapping<std::extents<signed char, D>> m(
[[maybe_unused]] std::layout_left::mapping<std::extents<signed char, D>> m(
std::layout_left::mapping<std::extents<int, D>>(std::extents<int, D>(500)));
}()),
"extents ctor: arguments must be representable as index_type and nonnegative");
Expand All @@ -55,7 +55,7 @@ int main(int, char**) {
[[maybe_unused]] std::extents<signed char, D, 5> e(arg_exts);
// but the product is not, so we can't use it for layout_left
TEST_LIBCPP_ASSERT_FAILURE(
([=] { std::layout_left::mapping<std::extents<signed char, D, 5>> m(arg); }()),
([=] { [[maybe_unused]] std::layout_left::mapping<std::extents<signed char, D, 5>> m(arg); }()),
"layout_left::mapping converting ctor: other.required_span_size() must be representable as index_type.");
}
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ int main(int, char**) {
{
// the extents are representable but the product is not, so we can't use it for layout_left
TEST_LIBCPP_ASSERT_FAILURE(
([=] { std::layout_left::mapping<std::extents<signed char, D, 5>> m(std::extents<signed char, D, 5>(100)); }()),
([=] {
[[maybe_unused]] std::layout_left::mapping<std::extents<signed char, D, 5>> m(
std::extents<signed char, D, 5>(100));
}()),
"layout_left::mapping extents ctor: product of extents must be representable as index_type.");
}
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ int main(int, char**) {
}
// mismatch of static extent
{
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_left::mapping<std::extents<int, 3>> m(arg); }()),
TEST_LIBCPP_ASSERT_FAILURE(([=] { [[maybe_unused]] std::layout_left::mapping<std::extents<int, 3>> m(arg); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// non-representability of extents itself
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_left::mapping<std::extents<signed char, D>> m(
[[maybe_unused]] std::layout_left::mapping<std::extents<signed char, D>> m(
std::layout_right::mapping<std::extents<int, D>>(std::extents<int, D>(500)));
}()),
"extents ctor: arguments must be representable as index_type and nonnegative");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ int main(int, char**) {
}
// mismatch of static extent
{
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_right::mapping<std::extents<int, D, 3>> m(arg); }()),
TEST_LIBCPP_ASSERT_FAILURE(([=] { [[maybe_unused]] std::layout_right::mapping<std::extents<int, D, 3>> m(arg); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// non-representability of extents itself
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_right::mapping<std::extents<signed char, D>> m(
[[maybe_unused]] std::layout_right::mapping<std::extents<signed char, D>> m(
std::layout_right::mapping<std::extents<int, D>>(std::extents<int, D>(500)));
}()),
"extents ctor: arguments must be representable as index_type and nonnegative");
Expand All @@ -55,7 +55,7 @@ int main(int, char**) {
[[maybe_unused]] std::extents<signed char, D, 5> e(arg_exts);
// but the product is not, so we can't use it for layout_right
TEST_LIBCPP_ASSERT_FAILURE(
([=] { std::layout_right::mapping<std::extents<signed char, D, 5>> m(arg); }()),
([=] { [[maybe_unused]] std::layout_right::mapping<std::extents<signed char, D, 5>> m(arg); }()),
"layout_right::mapping converting ctor: other.required_span_size() must be representable as index_type.");
}
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ int main(int, char**) {
// the extents are representable but the product is not, so we can't use it for layout_right
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_right::mapping<std::extents<signed char, D, 5>> m(std::extents<signed char, D, 5>(100));
[[maybe_unused]] std::layout_right::mapping<std::extents<signed char, D, 5>> m(
std::extents<signed char, D, 5>(100));
}()),
"layout_right::mapping extents ctor: product of extents must be representable as index_type.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ int main(int, char**) {
}
// mismatch of static extent
{
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_right::mapping<std::extents<int, 3>> m(arg); }()),
TEST_LIBCPP_ASSERT_FAILURE(([=] { [[maybe_unused]] std::layout_right::mapping<std::extents<int, 3>> m(arg); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// non-representability of extents itself
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_right::mapping<std::extents<signed char, D>> m(
[[maybe_unused]] std::layout_right::mapping<std::extents<signed char, D>> m(
std::layout_left::mapping<std::extents<int, D>>(std::extents<int, D>(500)));
}()),
"extents ctor: arguments must be representable as index_type and nonnegative");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,25 @@ int main(int, char**) {
{
std::extents<int, D, D> arg_exts{100, 5};
std::layout_stride::mapping<std::extents<int, D, D>> arg(arg_exts, std::array<int, 2>{1, 100});
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_stride::mapping<std::extents<int, D, 3>> m(arg); }()),
"extents construction: mismatch of provided arguments with static extents.");
TEST_LIBCPP_ASSERT_FAILURE(
([=] { [[maybe_unused]] std::layout_stride::mapping<std::extents<int, D, 3>> m(arg); }()),
"extents construction: mismatch of provided arguments with static extents.");
}
// non-representability of extents itself
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_stride::mapping<std::extents<signed char, D>> m(
[[maybe_unused]] std::layout_stride::mapping<std::extents<signed char, D>> m(
std::layout_stride::mapping<std::extents<int, D>>(std::extents<int, D>(500), std::array<int, 1>{1}));
}()),
"extents ctor: arguments must be representable as index_type and nonnegative");
}
// all strides must be larger than zero
{
always_convertible_layout::mapping<std::dextents<int, 2>> offset_map(std::dextents<int, 2>{10, 10}, 100, -1);
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_stride::mapping<std::extents<signed char, D, D>> m(offset_map); }()),
"layout_stride::mapping converting ctor: all strides must be greater than 0");
TEST_LIBCPP_ASSERT_FAILURE(
([=] { [[maybe_unused]] std::layout_stride::mapping<std::extents<signed char, D, D>> m(offset_map); }()),
"layout_stride::mapping converting ctor: all strides must be greater than 0");
}
// required_span_size not representable, while individual extents are
{
Expand All @@ -84,7 +86,7 @@ int main(int, char**) {
[[maybe_unused]] std::extents<signed char, D, 5> e(arg_exts);
// but the product is not, so we can't use it for layout_stride
TEST_LIBCPP_ASSERT_FAILURE(
([=] { std::layout_stride::mapping<std::extents<signed char, D, 5>> m(arg); }()),
([=] { [[maybe_unused]] std::layout_stride::mapping<std::extents<signed char, D, 5>> m(arg); }()),
"layout_stride::mapping converting ctor: other.required_span_size() must be representable as index_type.");
}
// required_span_size not representable, while individual extents are, edge case
Expand All @@ -98,14 +100,15 @@ int main(int, char**) {
[[maybe_unused]] std::extents<signed char, D, 10> e(arg_exts);
// but the product is not, so we can't use it for layout_stride
TEST_LIBCPP_ASSERT_FAILURE(
([=] { std::layout_stride::mapping<std::extents<signed char, D, 10>> m(arg); }()),
([=] { [[maybe_unused]] std::layout_stride::mapping<std::extents<signed char, D, 10>> m(arg); }()),
"layout_stride::mapping converting ctor: other.required_span_size() must be representable as index_type.");
}
// base offset must be 0 (i.e. mapping(0,...,0)==0) for a strided layout with positive strides
{
always_convertible_layout::mapping<std::dextents<int, 2>> offset_map(std::dextents<int, 2>{10, 10}, 3);
TEST_LIBCPP_ASSERT_FAILURE(([=] { std::layout_stride::mapping<std::extents<signed char, D, D>> m(offset_map); }()),
"layout_stride::mapping converting ctor: base offset of mapping must be zero.");
TEST_LIBCPP_ASSERT_FAILURE(
([=] { [[maybe_unused]] std::layout_stride::mapping<std::extents<signed char, D, D>> m(offset_map); }()),
"layout_stride::mapping converting ctor: base offset of mapping must be zero.");
}
return 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ int main(int, char**) {
{
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_stride::mapping<std::extents<unsigned, D, 5, 7>> m(
[[maybe_unused]] std::layout_stride::mapping<std::extents<unsigned, D, 5, 7>> m(
std::extents<unsigned, D, 5, 7>(20), std::array<unsigned, 3>{4, 1, 200});
}()),
"layout_stride::mapping ctor: the provided extents and strides lead to a non-unique mapping");
Expand All @@ -58,7 +58,7 @@ int main(int, char**) {
// will fail because neither of the equal strides is associated with an extent of 1
TEST_LIBCPP_ASSERT_FAILURE(
([=] {
std::layout_stride::mapping<std::extents<unsigned, D, 5, 2>> m3(
[[maybe_unused]] std::layout_stride::mapping<std::extents<unsigned, D, 5, 2>> m3(
std::extents<unsigned, D, 5, 2>(2), std::array<unsigned, 3>{5, 1, 5});
}()),
"layout_stride::mapping ctor: the provided extents and strides lead to a non-unique mapping");
Expand Down
Loading
Loading