RFC: C++ Buffer Hardening
We aim to improve security of critical C++ codebases. In order to do that we plan to work on two ideas.
- Hardened C++ Standard Library
- C++ Safe Buffers Programming Model and Adoption Tooling
Hardened libc++ aims to make C++ Standard Library interfaces generally more secure.
C++ Safe Buffers Programming Model together with Hardened libc++ provide runtime mitigation of out-of-bounds memory access. The adoption tooling will automatize code migration to this new programming model.
Hardened C++ Standard Library
by Louis Dionne (@ldionne)
We plan to implement a hardened mode of libc++ in which several cases of undefined behavior are caught and turned into assertion failures instead. For example, accessing a std::span
or a std::vector
outside of its bounds would abort the program, and so would accessing an empty std::optional
. This can be done while staying Standards-conforming because undefined behavior implies that the library can do whatever it wants, which includes aborting.
These additional runtime checks will be grouped into various categories that can be controlled separately. The intent is that a vendor shipping libc++ on their platform can decide to pick which checks to enable in the library that they ship (if at all), depending on their desired level of safety. To reiterate, the end goal is for the shipped library to enable these checks in production — this is not a “debug only” feature, although it will eventually replace the long-broken “Debug mode”.
Indeed, the current libc++ “Debug mode” has several issues that explain why it has never been shipped by some vendors: it is slow, it requires a global database with a lock, and it is ABI-affecting despite the fact that its original design was done entirely to avoid that. Finally, we also realized recently that the current “Debug mode” was fundamentally incompatible with constexpr, which makes it non viable going forward.
Instead, we will explicitly allow some of the new checks to affect the ABI, which will make it possible to implement them more efficiently. For example, we may need to slightly tweak the representation of some data structures to include additional information to inform the checks. Categories of checks that are ABI-affecting will only be controllable by vendors at the time of configuring the library, while other checks will be controllable by users in their own code. ABI-affecting checks will also be encoded in the mangled names of library entities to reduce the risk for accidental mismatches.
Finally, libc++ already implements some of this proposal. Indeed, we recently introduced a safe mode where one can enable the most basic category of checks, i.e. those that do not have an effect on the ABI or on the complexity of operations. The assertion handler can also be customized to meet the needs of various users, similarly to how operator new
can be customized via weak linking.
C++ Safe Buffers
by
Artem Dergachev (@NoQ)
Jan Korous (@jankorous)
Malavika Samak (@malavikasamak)
Rashmi Mudduluru (@t-rasmud)
Ziqing Luo (@ziqingluo-90)
Programming Model
We plan to introduce Safe Buffers Programming Model under which any pointer arithmetic is considered unsafe and clang warns about it. While this can be potentially useful for security-critical C projects its main application are C++ codebases in which pointer arithmetic can be transformed to use Hardened libc++ facilities such as std::array
, std::vector
and std::span
that will be bounds-checked at runtime and potential exploits turned into traps.
Our plan is to emit a warning every time an unsafe operation is performed on a raw pointer (largely similar to clang-tidy checks for bounds safety such as clang-tidy - cppcoreguidelines-pro-bounds-pointer-arithmetic — Extra Clang Tools 16.0.0git documentation).
We plan the warnings to be directly in the compiler, being off-by-default (most likely outside of -Wall as well). This allows us to reach all maintainers of the targeted projects without forcing them to use other clang tools or to regress their build times. In particular, we expect our users to keep the warnings enabled even after they’ve updated all their code, to avoid regressing back to the old ways in the new code. This way these warnings basically form a hardened language mode. That said, we plan to have most of our machinery live in libAnalysis, from which it can be accessed by arbitrary tools or generalized to other use cases.
We are not addressing temporal memory safety violations such as use-after-free / lifetime bugs.
Adoption Tooling
In C++ mode we will also emit fixits that replace one or more variables of raw pointer type with standard containers. Such fixits would also update other uses of these variables if a standard container isn’t an exact drop-in replacement of a raw pointer; for example, if(array_pointer)
may be converted to if(span.data())
.
The fixits are targeting not only local variables but also parameter variables of pointer type. When we suggest replacing a parameter variable with, say, a std::span
, the fixit would keep the old prototype as an overload but mark it deprecated with a custom attribute. Calls to functions wearing that attribute will be treated as unsafe operations in other functions which would in turn cause us to emit a warning at the call site. This way our fixes can spread across function boundaries or even across translation unit boundaries.
We plan to produce a reasonable fixit in the majority of cases to ease adoption. This means that we’re considering fairly sophisticated and novel machinery that analyzes all uses of one or more related variables to discover which container is appropriate and what other code changes will be necessary. We also do not promise that the fixit is going to compile; in some cases we plan to leave placeholders for the user to fill in, such as the size parameter for std::span
. Later we may try to improve our machinery to discover the size of the span automatically but we believe that even without that feature the fixits are going to save our users a lot of time.
Static Analyzer Checker
We are also considering a path-sensitive clang static analyzer checker that warns if std::span
is constructed from a container that has smaller size than specified in the span’s constructor. Such checker is self-contained and useful on its own, if everything goes well it’ll be enabled by default for all users. But it will play nicely with this project as it’ll protect the users from accidentally introducing new bugs while refactoring their code.
Comments and suggestions are welcome!