Standard library hardening

Document #: P3471R1
Date: 2024-11-21
Project: Programming Language C++
Audience: Library Evolution, SG23
Reply-to: Konstantin Varlamov
<>
Louis Dionne
<>

1 Introduction

This paper proposes introducing standard library hardening into the C++ Standard. Hardening allows turning some instances of undefined behavior in the standard library into a contract violation. This proposal is based on our experience implementing hardening in libc++ and deploying it widely.

2 Revision history

3 Motivation

There has been significantly increased attention to safety and security in C++ over the last few years, as exemplified by the well-known White House report and numerous recent security-related proposals.

While it is important to explore ways to make new code safer, we believe that the highest priority to deliver immediate real-world value should be to make existing code safer with minimal or no effort on behalf of users. Indeed, the amount of existing security-critical C++ code is so large that rewriting it or modifying it is both economically unviable and dangerous given the risk of introducing new issues.

There have been a few proposals accepted recently that eliminate some cases of undefined behavior in the core language. The standard library also contains many instances of undefined behavior, some of which is a direct source of security vulnerabilities; addressing those is often trivial, can be done with low overhead and almost no work on behalf of users.

In fact, at the moment all three major library implementations have some notion of a hardened or debug mode. This clearly shows interest, both from users and from implementers, in having a safer mode for the standard library. However, we believe these efforts would be vastly more useful if they were standardized and provided portable, cross-platform guarantees to users; as it stands, implementations differ in levels of coverage, performance guarantees and ways to enable the safer mode.

Finally, leaving security of the library to be a pure vendor extension fails to position ISO C++ as providing a credible solution for code bases with formal security requirements. We believe that formally requiring the basic safety guarantees that most implementations already provide in one way or another could make a significant difference from the point of view of anyone writing or following safety and security coding standards and guidelines.

4 Deployment experience

All three major implementations provide vendor-specific ways of enabling library assertions as proposed in this paper, today.

Overall, standard library hardening has been a huge success, in fact we never expected so much success. The reception has been overwhelmingly positive and while the quality of implementation will never be perfect, we are working hard to expand the scope of hardening in libc++, to improve its performance and the user experience.

5 Design overview

At a high level, this proposal consists of two parts:

There are a few important aspects to the proposed design:

To reiterate the last point, an important design principle is that hardening needs to be lightweight enough for production use by a wide variety of real-world programs. In our experience in libc++, a small set of checks that is widely used delivers far more value than a more extensive set of checks that is only enabled by select few users. Thankfully, many of the most valuable checks, such as checking for out-of-bounds access in standard containers, also happen to be relatively cheap.

6 Hardened preconditions

To specify hardening in the Standard, this proposal introduces the notion of a hardened precondition. A hardened precondition is a precondition that results in a contract violation in a hardened implementation. Adding hardening to the library largely consists of turning some of the existing preconditions into hardened preconditions in the specification. For example:

(24.7.2.2.6) Element access 23.7.2.2.6 [span.elem]

constexpr reference operator[](size_type idx) const;

1 Hardened Preconditions: idx < size() is true.

In the initial proposal, we decide to focus on hardened preconditions that prevent out-of-bounds memory access, i.e., compromise the memory safety of the program. These are some of the most valuable for the user since they help prevent potential security vulnerabilities; many of them are also relatively cheap to implement. More hardened preconditions can potentially be added in the future, but the intent is for their number to be limited to keep hardening viable for production use. Specifically, the proposal is to add hardened preconditions to:

In our experience, hardening all of these operations is trivial to implement and provides significant security value.

7 Enabling hardening

Much like a freestanding implementation, the way to request a hardened implementation is left for the implementation to define. For example, similarly to -ffreestanding, we expect that most toolchains would provide a compiler flag like -fhardened, but other alternatives like a -D_LIBCPP_HARDENING_MODE=<mode> macro would also be conforming. If this proposal gets accepted, we expect implementations to converge on a portable mechanism. Other details like whether hardened implementations and non-hardened implementations can be mixed in different translation units are intentionally left unspecified, to avoid overconstraining the implementations.

8 Relationship to Contracts, Profiles, and Erroneous Behavior

8.1 Contracts

Contracts is a new (in-progress) language feature that allows expressing preconditions and much more, with a lot of flexibility on what happens when an assertion is not satisfied. In the latest revision of this paper, we decided to base hardened preconditions on contract violations for several reasons:

It is useful to note that we don’t require implementations to actually implement these checks as contract assertions. Implementations are free to implement precondition checking however they see fit (e.g. a macro), however they are required to employ the contract violation handling mechanism when a precondition is not satisfied.

Note that if Contracts were to not be pursued anymore, this feature could easily be reworded in terms of a guaranteed termination in an implementation-defined way, or using Erroneous Behavior. We are not strongly attached to the exact mechanism used to implement this, but we find that Contracts is a nearly perfect fit.

8.2 Profiles

The various Profiles proposals introduce a framework to specify sets of safety guarantees (such as a type safety profile or an invalidation profile). If profiles become a part of the Standard in the future, hardening can most likely be formulated as an additional profile; this would formalize how hardening is turned on and off.

However, we feel strongly that a hardening mode as specified in this paper standardizes existing practice and delivers value today without waiting for a larger and still experimental language feature.

8.3 Erroneous Behavior

While Erroneous Behavior is a way to clearly mark some code as incorrect in the specification, it does not clearly specify what should happen in case of EB. For example, a conforming behavior for vector::operator[] being called out-of-bounds under EB would be to return the last element of the vector instead. While that is well-defined behavior, we feel that it is not especially useful behavior and that is certainly not what our users are looking for. In contrast, the Contracts facilities provide a well-defined and flexible framework to handle this.

9 Proposed wording (partial)

Add a new paragraph to 4.1 [intro.compliance] after paragraph 7 as indicated:

7 Two kinds of implementations are defined: a hosted implementation and a freestanding implementation. A freestanding implementation is one in which execution may take place without the benefit of an operating system. A hosted implementation supports all the facilities described in this document, while a freestanding implementation supports the entire C++ language described in [lex] through [cpp] and the subset of the library facilities described in [compliance].

8 Additionally, an implementation can be a hardened implementation. A hardened implementation is one in which violating a hardened precondition is a contract violation.

Add a new element to 16.3.2.4 [structure.specifications] after element 3.3 as indicated:

(3.3) Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any preconditions results in undefined behavior.

(3.4) Hardened Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any hardened preconditions results in a contract violation in a hardened implementation, and undefined behavior otherwise.

Modify 23.7.2.2.6 [span.elem] around paragraph 1 as indicated:

constexpr reference operator[](size_type idx) const;

1 Hardened Preconditions: idx < size() is true.

2 Returns: *(data() + idx).

3 Throws: Nothing.

Modify 27.3.3.6 [string.view.access] around paragraph 1 as indicated:

constexpr const_reference operator[](size_type pos) const;

1 Hardened Preconditions: pos < size().

2 Returns: data_[pos].

3 Throws: Nothing.

Modify 23.2.4 [sequence.reqmts] around paragraph 117 as indicated:

a[n]

117 Result: reference; const_reference for constant a

118 Hardened Preconditions: n < a.size();

119 Effects: Equivalent to: return *(a.begin() + n);

120 Remarks: Required for basic_string, array, deque, inplace_vector, and vector.

Modify 22.8.6.6 [expected.object.obs] paragraph 1 as indicated:

constexpr const T* operator->() const noexcept;

constexpr T* operator->() noexcept;

1 Hardened Preconditions: has_value() is true.

2 Returns: addressof(val).

Modify 22.8.6.6 [expected.object.obs] paragraph 3 as indicated:

constexpr const T& operator*() const & noexcept;

constexpr T& operator*() & noexcept;

3 Hardened Preconditions: has_value() is true.

4 Returns: val.

Modify 22.8.6.6 [expected.object.obs] paragraph 5 as indicated:

constexpr T&& operator*() && noexcept;

constexpr const T&& operator*() const && noexcept;

5 Hardened Preconditions: has_value() is true.

6 Returns: std::move(val).

Modify 22.5.3.7 [optional.observe] paragraph 1 as indicated:

constexpr const T* operator->() const noexcept;

constexpr T* operator->() noexcept;

1 Hardened Preconditions: *this contains a value.

2 Returns: *val.

3 Remarks: These functions are constexpr functions.

Modify 22.5.3.7 [optional.observe] paragraph 4 as indicated:

constexpr const T& operator*() const & noexcept;

constexpr T& operator*() & noexcept;

4 Hardened Preconditions: *this contains a value.

5 Returns: *val.

6 Remarks: These functions are constexpr functions.

Modify 22.5.3.7 [optional.observe] paragraph 7 as indicated:

constexpr T&& operator*() && noexcept;

constexpr const T&& operator*() const && noexcept;

7 Hardened Preconditions: *this contains a value.

8 Effects: Equivalent to: return std::move(*val);

10 Suggested polls